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>, + pub(super) param_defaults: Vec>, + pub(super) param_by_ref: Vec, + pub(super) variadic_index: Option, + pub(super) return_type: Option, + pub(super) bridge_supported: bool, +} + +impl NativeCallableSignature { + /// Creates signature metadata with the visible positional parameter count. + pub const fn new(param_count: usize) -> Self { + Self { + param_count, + param_names: Vec::new(), + param_types: Vec::new(), + param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, + return_type: None, + bridge_supported: true, + } + } + + /// Returns the visible positional parameter count accepted by this callable. + pub const fn param_count(&self) -> usize { + self.param_count + } + + /// Records the PHP parameter name for one positional callable slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Records the PHP declared type metadata for one positional callable slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + + /// Records a PHP scalar default value for one positional callable slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + + /// Records whether one positional callable parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callable parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + + /// Records the PHP declared return type metadata for this callable. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + + /// Records whether eval may dispatch this callable through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + + /// Returns the PHP-visible parameter names registered for this callable. + pub fn param_names(&self) -> &[String] { + &self.param_names + } + + /// Returns PHP declared parameter types registered for this callable. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + + /// Returns the PHP-visible scalar parameter defaults registered for this callable. + pub fn param_defaults(&self) -> &[Option] { + &self.param_defaults + } + + /// Returns the registered scalar default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns whether eval may dispatch this callable through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + + /// Returns the minimum number of arguments required by registered defaults. + pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } + + /// Returns the registered declared return type metadata, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } +} diff --git a/crates/elephc-magician/src/context/native_function.rs b/crates/elephc-magician/src/context/native_function.rs new file mode 100644 index 0000000000..b07ead6117 --- /dev/null +++ b/crates/elephc-magician/src/context/native_function.rs @@ -0,0 +1,180 @@ +//! Purpose: +//! Defines registered native function descriptors and their callable signatures. +//! +//! Called from: +//! - FFI registration and native function binding/dispatch. +//! +//! Key details: +//! - Parameter metadata, bridge support, required arity, and return type travel with the invoker. + +use super::*; + +/// Native AOT function callback metadata visible to runtime eval fragments. +#[derive(Clone)] +pub struct NativeFunction { + pub(super) descriptor: *mut c_void, + pub(super) invoker: NativeFunctionInvoker, + pub(super) param_count: usize, + pub(super) param_names: Vec, + pub(super) param_types: Vec>, + pub(super) param_defaults: Vec>, + pub(super) param_by_ref: Vec, + pub(super) variadic_index: Option, + pub(super) return_type: Option, + pub(super) bridge_supported: bool, +} + +impl NativeFunction { + /// Creates callback metadata for a descriptor-compatible AOT function. + pub fn new( + descriptor: *mut c_void, + invoker: NativeFunctionInvoker, + param_count: usize, + ) -> Self { + Self { + descriptor, + invoker, + param_count, + param_names: Vec::new(), + param_types: Vec::new(), + param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, + return_type: None, + bridge_supported: true, + } + } + + /// Returns the visible positional parameter count accepted by this callback. + pub const fn param_count(&self) -> usize { + self.param_count + } + + /// Records the PHP parameter name for one positional callback slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Returns the PHP-visible parameter names registered for this callback. + pub fn param_names(&self) -> &[String] { + &self.param_names + } + + /// Records the PHP declared type metadata for one positional callback slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + + /// Returns PHP declared parameter types registered for this callback. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + + /// Records a PHP default value for one positional callback slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + + /// Returns the registered default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Records whether one positional callback parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callback parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + + /// Records the PHP declared return type metadata for this callback. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + + /// Records whether eval may dispatch this callback through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns the registered declared return type, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns whether eval may dispatch this callback through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + + /// Returns the minimum number of required parameters implied by defaults. + pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } + + /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. + /// + /// # Safety + /// `arg_array` must be a boxed Mixed indexed array whose elements are boxed + /// Mixed cells following the descriptor-invoker ABI. + pub unsafe fn call(&self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { + RuntimeCellHandle::from_raw((self.invoker)(self.descriptor, arg_array.as_ptr())) + } +} diff --git a/crates/elephc-magician/src/context/native_metadata.rs b/crates/elephc-magician/src/context/native_metadata.rs new file mode 100644 index 0000000000..b0be4a30c6 --- /dev/null +++ b/crates/elephc-magician/src/context/native_metadata.rs @@ -0,0 +1,263 @@ +//! Purpose: +//! Stores generated class hierarchy, attributes, properties, and abstract contract metadata. +//! +//! Called from: +//! - FFI registration, declaration validation, Reflection, and property access. +//! +//! Key details: +//! - Native member keys are normalized consistently with eval class-like lookups. + +use super::*; + +impl ElephcEvalContext { + /// Defines generated AOT parent metadata for eval `parent::` resolution. + pub fn define_native_class_parent(&mut self, class_name: &str, parent_name: &str) -> bool { + let class_key = normalize_class_name(class_name); + let parent_name = parent_name.trim_start_matches('\\'); + if class_key.is_empty() || parent_name.is_empty() { + return false; + } + self.native_class_parents + .insert(class_key, parent_name.to_string()) + .is_none() + } + + /// Returns generated AOT parent metadata by PHP class name. + pub fn native_class_parent(&self, class_name: &str) -> Option<&str> { + self.native_class_parents + .get(&normalize_class_name(class_name)) + .map(String::as_str) + } + + /// Appends generated AOT class attribute metadata for eval reflection. + pub fn define_native_class_attribute( + &mut self, + class_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = normalize_class_name(class_name); + if key.is_empty() { + return false; + } + self.native_class_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class attribute metadata by PHP class name. + pub fn native_class_attributes(&self, class_name: &str) -> Vec { + self.native_class_attributes + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + + /// Appends generated AOT method attribute metadata for eval reflection. + pub fn define_native_method_attribute( + &mut self, + class_name: &str, + method_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_method_key(class_name, method_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_method_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT method attribute metadata by PHP class and method name. + pub fn native_method_attributes( + &self, + class_name: &str, + method_name: &str, + ) -> Vec { + self.native_method_attributes + .get(&native_method_key(class_name, method_name)) + .cloned() + .unwrap_or_default() + } + + /// Appends generated AOT class-constant attribute metadata for eval reflection. + pub fn define_native_constant_attribute( + &mut self, + class_name: &str, + constant_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_constant_key(class_name, constant_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_constant_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class-constant attribute metadata by PHP class and constant name. + pub fn native_constant_attributes( + &self, + class_name: &str, + constant_name: &str, + ) -> Vec { + self.native_constant_attributes + .get(&native_constant_key(class_name, constant_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT interface property-hook metadata for eval validation. + pub fn define_native_interface_property_requirement( + &mut self, + interface_name: &str, + declaring_interface_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(interface_name); + let owner = declaring_interface_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { + return false; + } + let requirements = self.native_interface_properties.entry(key).or_default(); + if requirements.iter().any(|(_, existing)| existing.name() == property.name()) { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT interface property-hook metadata by interface name. + pub fn native_interface_property_requirements( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_interface_properties + .get(&normalize_class_name(interface_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT abstract class property-hook metadata for eval validation. + pub fn define_native_abstract_property_requirement( + &mut self, + class_name: &str, + declaring_class_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(class_name); + let owner = declaring_class_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { + return false; + } + let requirements = self.native_abstract_properties.entry(key).or_default(); + if requirements + .iter() + .any(|(_, existing)| existing.name() == property.name()) + { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT abstract class property-hook metadata by class name. + pub fn native_abstract_property_requirements( + &self, + class_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_abstract_properties + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT property type metadata for eval reflection. + pub fn define_native_property_type( + &mut self, + class_name: &str, + property_name: &str, + property_type: EvalParameterType, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_types + .insert(key, property_type) + .is_none() + } + + /// Returns generated AOT property type metadata by PHP class and property name. + pub fn native_property_type( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_types + .get(&native_property_key(class_name, property_name)) + .cloned() + } + + /// Defines generated AOT property default metadata for eval reflection. + pub fn define_native_property_default( + &mut self, + class_name: &str, + property_name: &str, + default: NativeCallableDefault, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_defaults.insert(key, default).is_none() + } + + /// Returns generated AOT property default metadata by PHP class and property name. + pub fn native_property_default( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_defaults + .get(&native_property_key(class_name, property_name)) + .cloned() + } + + /// Appends generated AOT property attribute metadata for eval reflection. + pub fn define_native_property_attribute( + &mut self, + class_name: &str, + property_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT property attribute metadata by PHP class and property name. + pub fn native_property_attributes( + &self, + class_name: &str, + property_name: &str, + ) -> Vec { + self.native_property_attributes + .get(&native_property_key(class_name, property_name)) + .cloned() + .unwrap_or_default() + } +} diff --git a/crates/elephc-magician/src/context/native_signatures.rs b/crates/elephc-magician/src/context/native_signatures.rs new file mode 100644 index 0000000000..82a9b3a123 --- /dev/null +++ b/crates/elephc-magician/src/context/native_signatures.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! Registers generated instance, static, and constructor callable signatures. +//! +//! Called from: +//! - FFI registration and native argument binding. +//! +//! Key details: +//! - Parameter names, types, defaults, by-ref flags, variadics, and return types share one shape. + +use super::*; + +impl ElephcEvalContext { + /// Defines native AOT instance-method signature metadata for eval named-argument binding. + pub fn define_native_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT static-method signature metadata for eval named-argument binding. + pub fn define_native_static_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_static_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT constructor signature metadata for eval named-argument binding. + pub fn define_native_constructor_signature( + &mut self, + class_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_constructors + .insert(normalize_class_name(class_name), signature) + .is_none() + } + + /// Records one parameter name for registered native AOT instance-method metadata. + pub fn define_native_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT instance-method metadata. + pub fn define_native_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT instance-method metadata. + pub fn define_native_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT instance-method parameter is by-reference. + pub fn define_native_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT instance-method parameter is variadic. + pub fn define_native_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT instance method. + pub fn define_native_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Records one return type for registered native AOT instance-method metadata. + pub fn define_native_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + + /// Records one parameter name for registered native AOT static-method metadata. + pub fn define_native_static_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT static-method metadata. + pub fn define_native_static_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT static-method metadata. + pub fn define_native_static_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT static-method parameter is by-reference. + pub fn define_native_static_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT static-method parameter is variadic. + pub fn define_native_static_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT static method. + pub fn define_native_static_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Records one return type for registered native AOT static-method metadata. + pub fn define_native_static_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + + /// Records one parameter name for registered native AOT constructor metadata. + pub fn define_native_constructor_param( + &mut self, + class_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT constructor metadata. + pub fn define_native_constructor_param_type( + &mut self, + class_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT constructor metadata. + pub fn define_native_constructor_param_default( + &mut self, + class_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT constructor parameter is by-reference. + pub fn define_native_constructor_param_by_ref( + &mut self, + class_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT constructor parameter is variadic. + pub fn define_native_constructor_variadic_param( + &mut self, + class_name: &str, + index: usize, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT constructor. + pub fn define_native_constructor_bridge_supported( + &mut self, + class_name: &str, + supported: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Returns native AOT instance-method signature metadata by PHP class and method name. + pub fn native_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT static-method signature metadata by PHP class and method name. + pub fn native_static_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_static_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT constructor signature metadata by PHP class name. + pub fn native_constructor_signature( + &self, + class_name: &str, + ) -> Option { + self.native_constructors + .get(&normalize_class_name(class_name)) + .cloned() + } +} diff --git a/crates/elephc-magician/src/context/normalization.rs b/crates/elephc-magician/src/context/normalization.rs new file mode 100644 index 0000000000..a8b4583e4e --- /dev/null +++ b/crates/elephc-magician/src/context/normalization.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Provides default context construction and normalized registry-key helpers. +//! +//! Called from: +//! - Context registries and public `Default` construction. +//! +//! Key details: +//! - PHP class, method, property, constant, and enum-case names use their required casing rules. + +use super::*; + +impl Default for ElephcEvalContext { + /// Creates the default process-level eval context. + fn default() -> Self { + Self::new() + } +} + +/// Normalizes PHP class names for the eval dynamic class registry. +pub(super) fn normalize_class_name(name: &str) -> String { + name.trim_start_matches('\\').to_ascii_lowercase() +} + +/// Adds an external declaration name once while preserving PHP-visible spelling. +pub(super) fn push_external_declared_name(names: &mut Vec, name: &str) -> bool { + let visible_name = name.trim_start_matches('\\'); + let key = normalize_class_name(visible_name); + if key.is_empty() { + return false; + } + if !names + .iter() + .any(|existing| normalize_class_name(existing) == key) + { + names.push(visible_name.to_string()); + } + true +} + +/// Normalizes PHP enum case names for case-sensitive eval enum lookup. +pub(super) fn normalize_enum_case_name(name: &str) -> String { + name.to_string() +} + +/// Normalizes PHP method names for case-insensitive native metadata lookup. +pub(super) fn normalize_method_name(name: &str) -> String { + name.to_ascii_lowercase() +} + +/// Builds the folded native method metadata key used for eval argument binding. +pub(super) fn native_method_key(class_name: &str, method_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + normalize_method_name(method_name), + ) +} + +/// Builds the folded native property metadata key used for eval reflection. +pub(super) fn native_property_key(class_name: &str, property_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + property_name.trim_start_matches('$').to_string(), + ) +} + +/// Builds the case-sensitive native class-constant metadata key used for eval reflection. +pub(super) fn native_constant_key(class_name: &str, constant_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + constant_name.to_string(), + ) +} + +/// Pushes a PHP class-like name once, preserving the first visible spelling. +pub(super) fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_class_name(name); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); + } +} + +/// Returns whether two PHP class-like names resolve to the same normalized spelling. +pub(super) fn same_class_name(left: &str, right: &str) -> bool { + normalize_class_name(left) == normalize_class_name(right) +} + +/// Returns whether a class-like name is one of PHP's native enum marker interfaces. +pub(super) fn is_php_enum_marker_interface(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + +/// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. +pub(super) fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_method_name(name); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); + } +} + +/// Pushes a case-sensitive PHP property name once for ReflectionClass metadata. +pub(super) fn push_unique_property_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Pushes a case-sensitive PHP class constant name once for ReflectionClass metadata. +pub(super) fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Normalizes PHP constant names for case-sensitive eval dynamic probes. +pub(super) fn normalize_constant_name(name: &str) -> String { + name.trim_start_matches('\\').to_string() +} diff --git a/crates/elephc-magician/src/context/reference_metadata.rs b/crates/elephc-magician/src/context/reference_metadata.rs new file mode 100644 index 0000000000..378eb748c5 --- /dev/null +++ b/crates/elephc-magician/src/context/reference_metadata.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Defines callable ABI aliases, execution-scope snapshots, and reference target shapes. +//! +//! Called from: +//! - Argument binding, reference writeback, object properties, and native invokers. +//! +//! Key details: +//! - Reference targets retain the exact caller-side storage and access scope needed for writeback. + +use super::*; + +/// Native descriptor-invoker ABI registered by generated code for AOT functions. +pub type NativeFunctionInvoker = + unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; + +/// Snapshot of eval execution stacks used to restore caller-sensitive access checks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElephcEvalExecutionScope { + pub(super) function_stack: Vec, + pub(super) class_stack: Vec, + pub(super) called_class_stack: Vec, +} + +/// PHP-visible magic-constant names for the current eval execution frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct EvalMagicScope { + pub(super) function_name: String, + pub(super) method_name: String, + pub(super) class_name: String, + pub(super) trait_name: String, +} + +/// Caller-side storage target that can remain linked to an eval object property. +#[derive(Clone)] +pub enum EvalReferenceTarget { + Variable { + scope: *mut ElephcEvalScope, + name: String, + }, + ArrayElement { + scope: *mut ElephcEvalScope, + array_name: String, + index: RuntimeCellHandle, + }, + NestedArrayElement { + array_target: Box, + index: RuntimeCellHandle, + }, + ObjectProperty { + object: RuntimeCellHandle, + property: String, + access_scope: ElephcEvalExecutionScope, + }, + StaticProperty { + class_name: String, + property: String, + access_scope: ElephcEvalExecutionScope, + }, + Cell { + cell: RuntimeCellHandle, + }, + InvokerSlot { + slot: usize, + source_tag: u64, + }, +} + +/// Normalized PHP array key used for eval-side reference metadata. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum EvalArrayReferenceKey { + Int(i64), + String(Vec), +} + +/// Late-static dispatch metadata attached to eval-created static callable arrays. +#[derive(Clone)] +pub(super) struct EvalStaticCallableMetadata { + pub(super) class_name: String, + pub(super) method: String, + pub(super) called_class: String, + pub(super) native_class: Option, + pub(super) bridge_scope: Option, +} + +/// Native instance-method dispatch metadata attached to eval-created method callables. +#[derive(Clone)] +pub(super) struct EvalObjectCallableMetadata { + pub(super) object: usize, + pub(super) method: String, + pub(super) called_class: String, + pub(super) native_class: String, + pub(super) bridge_scope: String, +} diff --git a/crates/elephc-magician/src/context/reflection_registry.rs b/crates/elephc-magician/src/context/reflection_registry.rs new file mode 100644 index 0000000000..34a4caf066 --- /dev/null +++ b/crates/elephc-magician/src/context/reflection_registry.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Registers synthetic Reflection owner identities and their eval metadata targets. +//! +//! Called from: +//! - Reflection object construction and reflected method dispatch. +//! +//! Key details: +//! - Class, function, method, property, constant, and closure targets use stable runtime identities. + +use super::*; + +impl ElephcEvalContext { + /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. + pub fn register_eval_reflection_attribute( + &mut self, + identity: u64, + attribute: EvalAttribute, + target: u64, + repeated: bool, + ) { + self.eval_reflection_attributes.insert( + identity, + EvalReflectionAttributeMetadata::new(attribute, target, repeated), + ); + } + + /// Returns eval-declared attribute metadata attached to a synthetic ReflectionAttribute. + pub fn eval_reflection_attribute( + &self, + identity: u64, + ) -> Option<&EvalReflectionAttributeMetadata> { + self.eval_reflection_attributes.get(&identity) + } + + /// Records reflected class metadata for one synthetic ReflectionClass object. + pub fn register_eval_reflection_class(&mut self, identity: u64, class_name: &str) { + self.eval_reflection_classes + .insert(identity, class_name.trim_start_matches('\\').to_string()); + } + + /// Returns the reflected class name attached to a synthetic ReflectionClass. + pub fn eval_reflection_class_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_classes + .get(&identity) + .map(String::as_str) + } + + /// Records reflected function metadata for one synthetic ReflectionFunction object. + pub fn register_eval_reflection_function(&mut self, identity: u64, function_name: &str) { + self.eval_reflection_functions + .insert(identity, function_name.trim_start_matches('\\').to_string()); + } + + /// Returns the reflected function name attached to a synthetic ReflectionFunction. + pub fn eval_reflection_function_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_functions + .get(&identity) + .map(String::as_str) + } + + /// Records the callable target behind a `Closure` reflected as a function. + pub fn register_eval_reflection_function_closure_target( + &mut self, + identity: u64, + target: EvalClosureObjectTarget, + ) { + self.eval_reflection_function_closure_targets + .insert(identity, target); + } + + /// Returns the callable target retained for a reflected `Closure` object. + pub fn eval_reflection_function_closure_target( + &self, + identity: u64, + ) -> Option<&EvalClosureObjectTarget> { + self.eval_reflection_function_closure_targets + .get(&identity) + } + + /// Records reflected method metadata for one synthetic ReflectionMethod object. + pub fn register_eval_reflection_method( + &mut self, + identity: u64, + declaring_class: &str, + method_name: &str, + ) { + self.eval_reflection_methods.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + method_name.to_string(), + ), + ); + } + + /// Returns the declaring class and method name attached to a synthetic ReflectionMethod. + pub fn eval_reflection_method(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_methods + .get(&identity) + .map(|(class, method)| (class.as_str(), method.as_str())) + } + + /// Records reflected property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.eval_reflection_properties.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + property_name.to_string(), + ), + ); + self.eval_dynamic_reflection_properties.remove(&identity); + } + + /// Records reflected dynamic-property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_dynamic_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.register_eval_reflection_property(identity, declaring_class, property_name); + self.eval_dynamic_reflection_properties.insert(identity); + } + + /// Returns the declaring class and property name attached to a synthetic ReflectionProperty. + pub fn eval_reflection_property(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_properties + .get(&identity) + .map(|(class, property)| (class.as_str(), property.as_str())) + } + + /// Returns whether a synthetic ReflectionProperty represents a dynamic property. + pub fn eval_reflection_property_is_dynamic(&self, identity: u64) -> bool { + self.eval_dynamic_reflection_properties.contains(&identity) + } + + /// Records reflected class constant or enum case metadata for one synthetic object. + pub fn register_eval_reflection_class_constant( + &mut self, + identity: u64, + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + ) { + self.eval_reflection_class_constants.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + constant_name.to_string(), + owner_kind, + ), + ); + } + + /// Returns the declaring class, name, and reflection owner kind for a synthetic constant. + pub fn eval_reflection_class_constant(&self, identity: u64) -> Option<(&str, &str, u64)> { + self.eval_reflection_class_constants + .get(&identity) + .map(|(class, constant, owner_kind)| (class.as_str(), constant.as_str(), *owner_kind)) + } +} diff --git a/crates/elephc-magician/src/context/runtime_state.rs b/crates/elephc-magician/src/context/runtime_state.rs new file mode 100644 index 0000000000..10f4d62111 --- /dev/null +++ b/crates/elephc-magician/src/context/runtime_state.rs @@ -0,0 +1,425 @@ +//! Purpose: +//! Manages mutable eval runtime state, execution scopes, resources, and call-site metadata. +//! +//! Called from: +//! - Interpreter execution and builtins requiring process-level eval state. +//! +//! Key details: +//! - Static cells, include state, scope stacks, errors, timezone, HTTP status, and magic paths live here. + +use super::*; + +impl ElephcEvalContext { + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. + pub fn has_function(&self, name: &str) -> bool { + self.functions.contains_key(name) || self.native_functions.contains_key(name) + } + + /// Returns true when the context has a closure registered under this synthetic name. + pub fn has_closure(&self, name: &str) -> bool { + self.closures.contains_key(name) + } + + /// Returns a stored static local cell for an eval-declared function. + pub fn static_local(&self, function_name: &str, name: &str) -> Option { + self.static_locals + .get(&(function_name.to_string(), name.to_string())) + .copied() + } + + /// Stores one static local cell and returns any replaced distinct cell. + pub fn set_static_local( + &mut self, + function_name: impl Into, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_locals + .insert((function_name.into(), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Returns a stored static property cell for an eval-declared class. + pub fn static_property(&self, class_name: &str, name: &str) -> Option { + self.static_properties + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval static property cell and returns any replaced distinct cell. + pub fn set_static_property( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_properties + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Binds one eval static property slot to a persistent PHP reference target. + pub fn bind_static_property_alias( + &mut self, + class_name: &str, + name: &str, + target: EvalReferenceTarget, + ) -> Option { + self.static_property_aliases + .insert((normalize_class_name(class_name), name.to_string()), target) + } + + /// Returns the persistent reference target bound to one eval static property slot. + pub fn static_property_alias( + &self, + class_name: &str, + name: &str, + ) -> Option<&EvalReferenceTarget> { + self.static_property_aliases + .get(&(normalize_class_name(class_name), name.to_string())) + } + + /// Returns a materialized eval class constant cell. + pub fn class_constant_cell(&self, class_name: &str, name: &str) -> Option { + self.class_constants + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval class constant cell and returns any replaced distinct cell. + pub fn set_class_constant_cell( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .class_constants + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Returns true when an eval include key was already loaded by this context. + pub fn has_included_file(&self, path: &str) -> bool { + self.included_files.contains(path) + } + + /// Records one successfully loaded eval include key for include_once/require_once. + pub fn mark_included_file(&mut self, path: impl Into) { + self.included_files.insert(path.into()); + } + + /// Stores the non-owned global scope handle used by eval `global` aliases. + pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { + if scope.is_null() { + self.global_scope = None; + false + } else { + self.global_scope = Some(scope); + true + } + } + + /// Returns the non-owned global scope handle for eval `global` aliases. + pub fn global_scope_ptr(&self) -> Option<*mut ElephcEvalScope> { + self.global_scope + } + + /// Pushes an eval-executed function name for magic-constant resolution. + pub fn push_function(&mut self, name: impl Into) { + self.function_stack.push(name.into()); + } + + /// Pops the current eval-executed function name after its body completes. + pub fn pop_function(&mut self) { + self.function_stack.pop(); + } + + /// Returns the current eval-executed function name, if execution is inside one. + pub fn current_function(&self) -> Option<&str> { + self.function_stack.last().map(String::as_str) + } + + /// Pushes the eval class whose method is currently executing. + pub fn push_class_scope(&mut self, name: impl Into) { + self.class_stack.push(name.into()); + } + + /// Pops the current eval class method scope. + pub fn pop_class_scope(&mut self) { + self.class_stack.pop(); + } + + /// Returns the current eval class scope, if execution is inside a method. + pub fn current_class_scope(&self) -> Option<&str> { + self.class_stack.last().map(String::as_str) + } + + /// Pushes the class name used to dispatch the current eval method call. + pub fn push_called_class_scope(&mut self, name: impl Into) { + self.called_class_stack.push(name.into()); + } + + /// Pops the current late-static-bound eval class scope. + pub fn pop_called_class_scope(&mut self) { + self.called_class_stack.pop(); + } + + /// Returns the current late-static-bound eval class scope, if execution is inside a method. + pub fn current_called_class_scope(&self) -> Option<&str> { + self.called_class_stack.last().map(String::as_str) + } + + /// Returns a dynamic called-class override for a generated/AOT frame entering eval. + pub fn native_frame_called_class_override( + &self, + class_name: &str, + called_class_name: &str, + ) -> Option { + let class_name = class_name.trim_start_matches('\\'); + let called_class_name = called_class_name.trim_start_matches('\\'); + if class_name.is_empty() || !called_class_name.eq_ignore_ascii_case(class_name) { + return None; + } + if let Some(called_class) = + native_frame_called_class_override(class_name, called_class_name) + { + return Some(called_class); + } + let active = self.current_called_class_scope()?.trim_start_matches('\\'); + if active.is_empty() || active.eq_ignore_ascii_case(class_name) { + return None; + } + let active = self + .resolve_class_name(active) + .unwrap_or_else(|| active.to_string()); + self.class_parent_names(&active) + .iter() + .any(|parent| parent.eq_ignore_ascii_case(class_name)) + .then_some(active) + } + + /// Pushes PHP-visible method magic constants for the current eval method frame. + pub fn push_method_magic_scope(&mut self, class_name: &str, method: &EvalClassMethod) { + self.magic_stack.push(EvalMagicScope { + function_name: method.magic_function_name().to_string(), + method_name: method.magic_method_name(class_name), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: method + .trait_origin() + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pushes PHP-visible class-like member magic constants for default expressions. + pub fn push_class_like_member_magic_scope( + &mut self, + class_name: &str, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: String::new(), + method_name: String::new(), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pushes PHP-visible callable magic constants for reflected parameter defaults. + pub fn push_callable_magic_scope( + &mut self, + function_name: &str, + method_name: &str, + class_name: Option<&str>, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: class_name + .map(|class_name| class_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pops the current PHP-visible eval magic-constant scope. + pub fn pop_magic_scope(&mut self) { + self.magic_stack.pop(); + } + + /// Returns the PHP `__FUNCTION__` value for the current eval frame. + pub fn current_magic_function(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.function_name.as_str()) + } + + /// Returns the PHP `__METHOD__` value for the current eval method frame. + pub fn current_magic_method(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.method_name.as_str()) + } + + /// Returns the PHP `__CLASS__` value for the current eval method frame. + pub fn current_magic_class(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.class_name.as_str()) + } + + /// Returns the PHP `__TRAIT__` value for the current eval method frame. + pub fn current_magic_trait(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.trait_name.as_str()) + } + + /// Captures the current eval execution stacks for later caller-context-sensitive work. + pub fn execution_scope(&self) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: self.function_stack.clone(), + class_stack: self.class_stack.clone(), + called_class_stack: self.called_class_stack.clone(), + } + } + + /// Replaces eval execution stacks and returns the previous stacks for restoration. + pub fn replace_execution_scope( + &mut self, + scope: ElephcEvalExecutionScope, + ) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: std::mem::replace(&mut self.function_stack, scope.function_stack), + class_stack: std::mem::replace(&mut self.class_stack, scope.class_stack), + called_class_stack: std::mem::replace( + &mut self.called_class_stack, + scope.called_class_stack, + ), + } + } + + /// Records a Throwable cell that escaped from an eval-executed function call. + pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { + self.pending_throw = Some(value); + } + + /// Returns and clears the Throwable cell currently escaping through eval. + pub fn take_pending_throw(&mut self) -> Option { + self.pending_throw.take() + } + + /// Returns the eval-local SPL autoload extension list. + pub fn spl_autoload_extensions(&self) -> &str { + &self.spl_autoload_extensions + } + + /// Replaces the eval-local SPL autoload extension list. + pub fn set_spl_autoload_extensions(&mut self, extensions: impl Into) { + self.spl_autoload_extensions = extensions.into(); + } + + /// Returns the eval-local stream resource table. + pub(crate) fn stream_resources(&self) -> &EvalStreamResources { + &self.streams + } + + /// Returns mutable access to the eval-local stream resource table. + pub(crate) fn stream_resources_mut(&mut self) -> &mut EvalStreamResources { + &mut self.streams + } + + /// Clears the eval-local JSON error state after a successful JSON operation. + pub fn clear_json_error(&mut self) { + self.json_last_error = 0; + self.json_last_error_msg.clear(); + self.json_last_error_msg.push_str("No error"); + } + + /// Records the eval-local JSON error state for `json_last_error*()` calls. + pub fn set_json_error(&mut self, code: i64, message: impl Into) { + self.json_last_error = code; + self.json_last_error_msg = message.into(); + } + + /// Returns the PHP `JSON_ERROR_*` code for the last eval JSON operation. + pub const fn json_last_error(&self) -> i64 { + self.json_last_error + } + + /// Returns the PHP message for the last eval JSON operation. + pub fn json_last_error_msg(&self) -> &str { + &self.json_last_error_msg + } + + /// Returns the eval-local PHP default timezone identifier. + pub fn default_timezone(&self) -> &str { + &self.default_timezone + } + + /// Replaces the eval-local PHP default timezone identifier. + pub fn set_default_timezone(&mut self, timezone: impl Into) { + self.default_timezone = timezone.into(); + } + + /// Returns the eval-local HTTP response code used by web-facing builtins. + pub const fn http_response_code(&self) -> i64 { + self.http_response_code + } + + /// Applies a new eval-local HTTP response code and returns the previous one. + pub fn replace_http_response_code(&mut self, response_code: i64) -> i64 { + let previous = self.http_response_code; + if response_code > 0 { + self.http_response_code = response_code; + } + previous + } + + /// Updates the source file, directory, and line for the current eval call site. + pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { + self.call_file = file.into(); + self.call_dir = dir.into(); + self.call_line = line; + self.file_magic_override = None; + } + + /// Returns a copy of the current call-site metadata for temporary overrides. + pub fn call_site(&self) -> (String, String, i64, Option) { + ( + self.call_file.clone(), + self.call_dir.clone(), + self.call_line, + self.file_magic_override.clone(), + ) + } + + /// Overrides `__FILE__` while executing an actual file through eval include. + pub fn set_file_magic_override(&mut self, file: Option) { + self.file_magic_override = file; + } + + /// Returns the source directory associated with the current eval call site. + pub fn call_dir(&self) -> &str { + &self.call_dir + } + + /// Returns PHP's `__FILE__` string for code currently running inside eval. + pub fn eval_file_magic(&self) -> String { + if let Some(file) = &self.file_magic_override { + return file.clone(); + } + if self.call_file.is_empty() { + return String::new(); + } + format!("{}({}) : eval()'d code", self.call_file, self.call_line) + } +} diff --git a/crates/elephc-magician/src/errors.rs b/crates/elephc-magician/src/errors.rs new file mode 100644 index 0000000000..08460ef7c7 --- /dev/null +++ b/crates/elephc-magician/src/errors.rs @@ -0,0 +1,85 @@ +//! Purpose: +//! Defines stable integer status codes returned by the eval bridge ABI. +//! Keeps Rust error shapes internal while exposing C-compatible outcomes. +//! +//! Called from: +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - Numeric values are part of the ABI contract and must remain stable. + +/// Stable eval bridge status codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalStatus { + Ok, + ParseError, + RuntimeFatal, + UncaughtThrowable, + UnsupportedConstruct, + AbiMismatch, +} + +impl EvalStatus { + /// Returns the C ABI integer code for this status. + pub const fn code(self) -> i32 { + match self { + Self::Ok => 0, + Self::ParseError => 1, + Self::RuntimeFatal => 2, + Self::UncaughtThrowable => 3, + Self::UnsupportedConstruct => 4, + Self::AbiMismatch => 5, + } + } +} + +/// Parse failures detected before lowering a runtime eval fragment. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvalParseError { + PhpOpenTag, + InvalidUtf8, + UnsupportedConstruct, + UnexpectedToken, + UnexpectedEof, + InvalidNumber, + UnterminatedString, + UnterminatedComment, + ExpectedVariable, + ExpectedSemicolon, +} + +impl EvalParseError { + /// Returns the ABI status that should be reported for this parse failure. + pub const fn status(self) -> EvalStatus { + match self { + Self::UnsupportedConstruct => EvalStatus::UnsupportedConstruct, + Self::PhpOpenTag + | Self::InvalidUtf8 + | Self::UnexpectedToken + | Self::UnexpectedEof + | Self::InvalidNumber + | Self::UnterminatedString + | Self::UnterminatedComment + | Self::ExpectedVariable + | Self::ExpectedSemicolon => EvalStatus::ParseError, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies only known unsupported syntax maps to the unsupported ABI status. + #[test] + fn parse_error_status_distinguishes_unsupported_constructs() { + assert_eq!( + EvalParseError::UnsupportedConstruct.status(), + EvalStatus::UnsupportedConstruct + ); + assert_eq!( + EvalParseError::UnexpectedToken.status(), + EvalStatus::ParseError + ); + } +} diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs new file mode 100644 index 0000000000..d88dd49c29 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Defines and re-exports the dynamic EvalIR used by runtime `eval()` fragments. +//! Node families live in focused child modules without changing the public model. +//! +//! Called from: +//! - `crate::parser::parse_fragment()` +//! - `crate::interpreter` execution. +//! +//! Key details: +//! - Runtime execution must turn constants into elephc runtime cells through +//! value-bridge hooks; EvalIR constants are syntax data, not owned PHP values. + +mod attributes; +mod callable; +mod classes; +mod enums; +mod expressions; +mod interfaces; +mod methods; +mod program; +mod properties; +mod statements; +mod traits; + +pub use attributes::*; +pub use callable::*; +pub use classes::*; +pub use enums::*; +pub use expressions::*; +pub use interfaces::*; +pub use methods::*; +pub use program::*; +pub use properties::*; +pub use statements::*; +pub use traits::*; diff --git a/crates/elephc-magician/src/eval_ir/attributes.rs b/crates/elephc-magician/src/eval_ir/attributes.rs new file mode 100644 index 0000000000..fdbb271ed3 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/attributes.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Defines eval attribute declarations and literal argument metadata. +//! +//! Called from: +//! - Class-like/callable parsing, validation, context registration, and Reflection. +//! +//! Key details: +//! - Attribute arguments remain syntax values until explicitly materialized by the interpreter. + +/// Literal attribute argument metadata retained by eval declarations. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalAttributeArg { + String(String), + Int(i64), + Float(u64), + Bool(bool), + Null, + Array(Vec), + Named { + name: String, + value: Box, + }, + IntKeyed { + key: i64, + value: Box, + }, +} + +impl EvalAttributeArg { + /// Returns the PHP named-argument key when this attribute arg is named. + pub fn name(&self) -> Option<&str> { + match self { + EvalAttributeArg::Named { name, .. } => Some(name), + _ => None, + } + } + + /// Returns the PHP integer array key when this attribute arg is int-keyed. + pub fn int_key(&self) -> Option { + match self { + EvalAttributeArg::IntKeyed { key, .. } => Some(*key), + _ => None, + } + } + + /// Returns the scalar payload, unwrapping a named or int-keyed wrapper. + pub fn value(&self) -> &EvalAttributeArg { + match self { + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + value + } + _ => self, + } + } +} + +/// Attribute metadata retained for eval class-like declarations. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalAttribute { + name: String, + args: Option>, +} + +impl EvalAttribute { + /// Creates one eval attribute metadata entry. + pub fn new(name: impl Into, args: Option>) -> Self { + Self { + name: name.into(), + args, + } + } + + /// Returns the resolved PHP-visible attribute class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns supported literal positional args, or `None` for unsupported metadata. + pub fn args(&self) -> Option<&[EvalAttributeArg]> { + self.args.as_deref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/callable.rs b/crates/elephc-magician/src/eval_ir/callable.rs new file mode 100644 index 0000000000..2ed0e18046 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/callable.rs @@ -0,0 +1,266 @@ +//! Purpose: +//! Defines closure captures, functions, and callable parameter type metadata. +//! +//! Called from: +//! - Function/closure parsing, dynamic binding, type checks, and Reflection. +//! +//! Key details: +//! - Parameter names, types, defaults, by-ref flags, and variadics remain index-aligned. + +use super::*; + +/// One lexical variable captured by a runtime eval closure literal. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClosureCapture { + name: String, + by_ref: bool, +} + +impl EvalClosureCapture { + /// Creates one closure capture with its source variable name and reference mode. + pub fn new(name: impl Into, by_ref: bool) -> Self { + Self { + name: name.into(), + by_ref, + } + } + + /// Returns the captured variable name without the leading `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns whether this capture was declared as `use (&$name)`. + pub const fn by_ref(&self) -> bool { + self.by_ref + } +} + +/// Runtime user function declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalFunction { + name: String, + source_location: Option, + attributes: Vec, + params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, +} + +impl PartialEq for EvalFunction { + /// Compares function metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + +impl EvalFunction { + /// Creates a dynamic eval function with source-order parameters and body. + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + body, + } + } + + /// Returns a copy of this function with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this function with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this function with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this function with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this function with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this function with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this function with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this function with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns the original source spelling of this eval-declared function name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this eval function. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the function declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns the dynamic EvalIR statements that form the function body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} + +/// One supported eval method parameter type atom. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvalParameterTypeVariant { + Array, + Bool, + Callable, + Class(String), + Float, + Int, + Iterable, + Mixed, + Never, + Object, + String, + Void, +} + +/// How multiple eval parameter type atoms combine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalParameterTypeKind { + Union, + Intersection, +} + +/// Type metadata retained for one eval method parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvalParameterType { + variants: Vec, + allows_null: bool, + kind: EvalParameterTypeKind, +} + +impl EvalParameterType { + /// Creates one eval method parameter type from union variants and nullability. + pub fn new(variants: Vec, allows_null: bool) -> Self { + Self { + variants, + allows_null, + kind: EvalParameterTypeKind::Union, + } + } + + /// Creates one eval method parameter type from intersection variants. + pub fn intersection(variants: Vec) -> Self { + Self { + variants, + allows_null: false, + kind: EvalParameterTypeKind::Intersection, + } + } + + /// Returns the non-null type atoms in source order. + pub fn variants(&self) -> &[EvalParameterTypeVariant] { + &self.variants + } + + /// Returns whether the type explicitly accepts PHP null. + pub const fn allows_null(&self) -> bool { + self.allows_null + } + + /// Returns whether all variants must match the value. + pub const fn is_intersection(&self) -> bool { + matches!(self.kind, EvalParameterTypeKind::Intersection) + } +} diff --git a/crates/elephc-magician/src/eval_ir/classes.rs b/crates/elephc-magician/src/eval_ir/classes.rs new file mode 100644 index 0000000000..3f11e8e8e1 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/classes.rs @@ -0,0 +1,507 @@ +//! Purpose: +//! Defines eval classes, trait adaptations, and class constants. +//! +//! Called from: +//! - Class parser, declaration validation, context lookup, object construction, and Reflection. +//! +//! Key details: +//! - Parent/interface/trait composition and member lists remain one class declaration model. + +use super::*; + +/// Runtime class declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalClass { + name: String, + source_location: Option, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + is_anonymous: bool, + parent: Option, + interfaces: Vec, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalClass { + /// Compares class metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.is_readonly_class == other.is_readonly_class + && self.is_anonymous == other.is_anonymous + && self.parent == other.parent + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalClass { + /// Creates a dynamic eval class with public properties and methods, and no relations. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_modifiers(name, false, false, None, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval class with optional parent and implemented interfaces. + pub fn with_relations( + name: impl Into, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_modifiers(name, false, false, parent, interfaces, properties, methods) + } + + /// Creates a dynamic eval class with optional modifiers, parent, and interfaces. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, optional parent, and interfaces. + pub fn with_class_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_and_traits( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with optional modifiers, relations, and trait uses. + pub fn with_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_and_traits( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, and trait uses. + pub fn with_class_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + traits, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait uses, constants, and members. + pub fn with_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, trait uses, constants, and members. + pub fn with_class_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + traits, + Vec::new(), + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait adaptations, constants, and members. + pub fn with_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + trait_adaptations, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with all class modifiers, relations, adaptations, constants, and members. + pub fn with_class_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + is_abstract, + is_final, + is_readonly_class, + is_anonymous: false, + parent, + interfaces, + attributes: Vec::new(), + traits, + trait_adaptations, + constants, + properties, + methods, + } + } + + /// Returns a copy of this class with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this class with optional source-location metadata attached. + pub const fn with_source_location_option( + mut self, + source_location: Option, + ) -> Self { + self.source_location = source_location; + self + } + + /// Returns a copy of this class with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Marks this eval class metadata as an anonymous class expression result. + pub fn with_anonymous(mut self) -> Self { + self.is_anonymous = true; + self + } + + /// Marks instance properties readonly when this metadata represents a `readonly class`. + pub fn with_readonly_properties(mut self) -> Self { + if self.is_readonly_class { + for property in &mut self.properties { + if !property.is_static { + property.is_readonly = true; + } + } + } + self + } + + /// Returns the original source spelling of this eval-declared class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns whether this eval-declared class was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared class was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns whether this eval-declared class was declared `readonly`. + pub const fn is_readonly_class(&self) -> bool { + self.is_readonly_class + } + + /// Returns whether this eval class came from a `new class {}` expression. + pub const fn is_anonymous(&self) -> bool { + self.is_anonymous + } + + /// Returns the parent class name declared by this eval class, when present. + pub fn parent(&self) -> Option<&str> { + self.parent.as_deref() + } + + /// Returns interface names implemented directly by this eval class. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + + /// Returns attributes declared directly on this eval class. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval class. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared on this eval class. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns class constants declared directly by this eval class. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns public properties declared directly by this eval class. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval class. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Returns a public method by PHP case-insensitive method name. + pub fn method(&self, name: &str) -> Option<&EvalClassMethod> { + self.methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(name)) + } + + /// Returns a class constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } +} + +/// Adaptation rule declared in a runtime eval class `use Trait { ... }` block. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalTraitAdaptation { + Alias { + trait_name: Option, + method: String, + alias: Option, + visibility: Option, + }, + InsteadOf { + trait_name: Option, + method: String, + instead_of: Vec, + }, +} + +/// Constant metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassConstant { + name: String, + trait_origin: Option, + attributes: Vec, + visibility: EvalVisibility, + is_final: bool, + value: EvalExpr, +} + +impl EvalClassConstant { + /// Creates a public eval class constant with a value expression. + pub fn new(name: impl Into, value: EvalExpr) -> Self { + Self::with_visibility(name, EvalVisibility::Public, value) + } + + /// Creates an eval class constant with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + value: EvalExpr, + ) -> Self { + Self::with_visibility_and_final(name, visibility, false, value) + } + + /// Creates an eval class constant with explicit PHP visibility and finality. + pub fn with_visibility_and_final( + name: impl Into, + visibility: EvalVisibility, + is_final: bool, + value: EvalExpr, + ) -> Self { + Self { + name: name.into(), + trait_origin: None, + attributes: Vec::new(), + visibility, + is_final, + value, + } + } + + /// Returns a copy of this class constant with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this constant with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + + /// Returns the PHP-visible class constant name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the trait that originally declared this imported constant, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns attributes declared directly on this class constant. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns the PHP visibility declared for this constant. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns whether this class constant was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns the constant initializer expression. + pub fn value(&self) -> &EvalExpr { + &self.value + } +} diff --git a/crates/elephc-magician/src/eval_ir/enums.rs b/crates/elephc-magician/src/eval_ir/enums.rs new file mode 100644 index 0000000000..6ac858c8f2 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/enums.rs @@ -0,0 +1,236 @@ +//! Purpose: +//! Defines eval enum declarations, backing types, and case metadata. +//! +//! Called from: +//! - Enum parser, declaration execution, context lookup, and Reflection. +//! +//! Key details: +//! - Unit/backed cases, traits, interfaces, methods, and source metadata stay together. + +use super::*; + +/// Runtime enum declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalEnum { + name: String, + source_location: Option, + backing_type: Option, + interfaces: Vec, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + cases: Vec, + constants: Vec, + methods: Vec, +} + +impl PartialEq for EvalEnum { + /// Compares enum metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.backing_type == other.backing_type + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.cases == other.cases + && self.constants == other.constants + && self.methods == other.methods + } +} + +impl EvalEnum { + /// Creates a dynamic eval enum with cases and optional backing type. + pub fn new( + name: impl Into, + backing_type: Option, + cases: Vec, + ) -> Self { + Self::with_members( + name, + backing_type, + Vec::new(), + cases, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with interfaces, cases, constants, and methods. + pub fn with_members( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + ) -> Self { + Self::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with traits, adaptations, cases, constants, and methods. + pub fn with_members_traits_adaptations( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + backing_type, + interfaces, + attributes: Vec::new(), + traits, + trait_adaptations, + cases, + constants, + methods, + } + } + + /// Returns a copy of this enum with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this enum with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared enum name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns the optional scalar backing type for this enum. + pub const fn backing_type(&self) -> Option { + self.backing_type + } + + /// Returns interface names implemented directly by this eval enum. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + + /// Returns attributes declared directly on this eval enum. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval enum. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval enum. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns cases declared directly by this eval enum. + pub fn cases(&self) -> &[EvalEnumCase] { + &self.cases + } + + /// Returns one enum case by PHP case-sensitive case name. + pub fn case(&self, name: &str) -> Option<&EvalEnumCase> { + self.cases().iter().find(|case| case.name() == name) + } + + /// Returns constants declared directly by this eval enum. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns methods declared directly by this eval enum. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Builds class-shaped metadata used for enum method and relation dispatch. + pub fn as_class_metadata(&self) -> EvalClass { + EvalClass::with_modifiers_traits_adaptations_and_constants( + self.name.clone(), + false, + true, + None, + self.interfaces.clone(), + self.traits.clone(), + self.trait_adaptations.clone(), + self.constants.clone(), + Vec::new(), + self.methods.clone(), + ) + .with_attributes(self.attributes.clone()) + .with_source_location_option(self.source_location) + } +} + +/// Scalar backing type for a runtime eval enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalEnumBackingType { + Int, + String, +} + +/// One case declared by a runtime eval enum. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalEnumCase { + name: String, + attributes: Vec, + value: Option, +} + +impl EvalEnumCase { + /// Creates an eval enum case with an optional backing value expression. + pub fn new(name: impl Into, value: Option) -> Self { + Self { + name: name.into(), + attributes: Vec::new(), + value, + } + } + + /// Returns a copy of this enum case with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the PHP-visible enum case name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns attributes declared directly on this enum case. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns the optional backing value expression. + pub fn value(&self) -> Option<&EvalExpr> { + self.value.as_ref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/expressions.rs b/crates/elephc-magician/src/eval_ir/expressions.rs new file mode 100644 index 0000000000..f1ea0c7179 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/expressions.rs @@ -0,0 +1,343 @@ +//! Purpose: +//! Defines EvalIR expressions, calls, arrays, constants, operators, and match/switch values. +//! +//! Called from: +//! - Expression parser, statement nodes, optimizer-free eval execution, and default metadata. +//! +//! Key details: +//! - Expression nodes describe syntax and evaluation order without owning runtime cells. + +use super::*; + +/// Dynamic eval expressions evaluated by the interpreter against runtime cells. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalExpr { + Array(Vec), + ArrayGet { + array: Box, + index: Box, + }, + Call { + name: String, + args: Vec, + }, + Cast { + target: EvalCastType, + expr: Box, + }, + Const(EvalConst), + ConstFetch(String), + Closure { + function: EvalFunction, + captures: Vec, + is_static: bool, + }, + FunctionCallable { + name: String, + fallback_name: Option, + }, + InvokableCallable { + object: Box, + }, + MethodCallable { + object: Box, + method: Box, + }, + StaticMethodCallable { + class_name: String, + method: Box, + }, + DynamicStaticMethodCallable { + class_name: Box, + method: Box, + }, + DynamicCall { + callee: Box, + args: Vec, + }, + DynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, + DynamicNewObject { + class_name: Box, + args: Vec, + }, + DynamicPropertyGet { + object: Box, + property: Box, + }, + DynamicStaticMethodCall { + class_name: Box, + method: Box, + args: Vec, + }, + DynamicStaticPropertyGet { + class_name: Box, + property: String, + }, + DynamicStaticPropertyNameGet { + class_name: Box, + property: Box, + }, + DynamicClassConstantFetch { + class_name: Box, + constant: String, + }, + DynamicClassConstantNameFetch { + class_name: Box, + constant: Box, + }, + DynamicClassNameFetch { + class_name: Box, + }, + Include { + path: Box, + required: bool, + once: bool, + }, + InstanceOf { + value: Box, + target: EvalInstanceOfTarget, + }, + LoadVar(String), + Match { + subject: Box, + arms: Vec, + default: Option>, + }, + Clone(Box), + NamespacedCall { + name: String, + fallback_name: String, + args: Vec, + }, + NamespacedConstFetch { + name: String, + fallback_name: String, + }, + MethodCall { + object: Box, + method: String, + args: Vec, + }, + NullsafeMethodCall { + object: Box, + method: String, + args: Vec, + }, + NullsafeDynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, + Magic(EvalMagicConst), + NewObject { + class_name: String, + args: Vec, + }, + NewAnonymousClass { + class: EvalClass, + args: Vec, + }, + StaticMethodCall { + class_name: String, + method: String, + args: Vec, + }, + StaticPropertyGet { + class_name: String, + property: String, + }, + ClassConstantFetch { + class_name: String, + constant: String, + }, + ClassNameFetch { + class_name: String, + }, + NullCoalesce { + value: Box, + default: Box, + }, + NullsafePropertyGet { + object: Box, + property: String, + }, + NullsafeDynamicPropertyGet { + object: Box, + property: Box, + }, + PropertyGet { + object: Box, + property: String, + }, + Print(Box), + Ternary { + condition: Box, + then_branch: Option>, + else_branch: Box, + }, + Unary { + op: EvalUnaryOp, + expr: Box, + }, + Binary { + op: EvalBinOp, + left: Box, + right: Box, + }, +} + +/// The right-hand side accepted by PHP's `instanceof` operator. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalInstanceOfTarget { + ClassName(String), + Expr(Box), +} + +/// One source-order function or method call argument parsed from eval code. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCallArg { + name: Option, + spread: bool, + value: EvalExpr, +} + +impl EvalCallArg { + /// Creates a positional call argument from a value expression. + pub fn positional(value: EvalExpr) -> Self { + Self { + name: None, + spread: false, + value, + } + } + + /// Creates a named call argument from a parameter name and value expression. + pub fn named(name: impl Into, value: EvalExpr) -> Self { + Self { + name: Some(name.into()), + spread: false, + value, + } + } + + /// Creates an unpacking call argument from an array expression. + pub fn spread(value: EvalExpr) -> Self { + Self { + name: None, + spread: true, + value, + } + } + + /// Returns the source argument name without `$`, if the argument was named. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns true when this argument came from `...expr` unpacking syntax. + pub const fn is_spread(&self) -> bool { + self.spread + } + + /// Returns the expression that computes this argument's runtime value. + pub const fn value(&self) -> &EvalExpr { + &self.value + } +} + +/// One element in a PHP array literal parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalArrayElement { + Value(EvalExpr), + Reference(EvalExpr), + KeyValue { key: EvalExpr, value: EvalExpr }, + KeyReference { key: EvalExpr, value: EvalExpr }, +} + +/// One ordered arm in a PHP `match` expression parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalMatchArm { + pub patterns: Vec, + pub value: EvalExpr, +} + +/// One ordered case arm in a PHP switch parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalSwitchCase { + pub condition: Option, + pub body: Vec, +} + +/// Literal syntax supported by the initial EvalIR parser. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalConst { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +/// PHP magic constants supported by runtime eval fragments. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalMagicConst { + File, + Dir, + Line(i64), + Function, + Class, + Method, + Namespace, + Trait, +} + +/// Binary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalBinOp { + Add, + Sub, + Mul, + Div, + Mod, + Pow, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, + Concat, + LogicalAnd, + LogicalOr, + LogicalXor, + LooseEq, + LooseNotEq, + StrictEq, + StrictNotEq, + Lt, + LtEq, + Gt, + GtEq, + Spaceship, +} + +/// Scalar cast targets supported by runtime eval expressions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalCastType { + Int, + Float, + String, + Bool, +} + +/// Unary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalUnaryOp { + Plus, + Negate, + LogicalNot, + BitNot, +} diff --git a/crates/elephc-magician/src/eval_ir/interfaces.rs b/crates/elephc-magician/src/eval_ir/interfaces.rs new file mode 100644 index 0000000000..f982a5892d --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/interfaces.rs @@ -0,0 +1,444 @@ +//! Purpose: +//! Defines eval interfaces, methods, properties, and hook contracts. +//! +//! Called from: +//! - Interface parser, declaration validation, context lookup, and Reflection. +//! +//! Key details: +//! - Parent interfaces and member contracts retain types, visibility, and source metadata. + +use super::*; + +/// Runtime interface declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalInterface { + name: String, + source_location: Option, + parents: Vec, + attributes: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalInterface { + /// Compares interface metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.parents == other.parents + && self.attributes == other.attributes + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalInterface { + /// Creates a dynamic eval interface with optional parent interfaces and methods. + pub fn new( + name: impl Into, + parents: Vec, + methods: Vec, + ) -> Self { + Self::with_constants(name, parents, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with optional parent interfaces, constants, and methods. + pub fn with_constants( + name: impl Into, + parents: Vec, + constants: Vec, + methods: Vec, + ) -> Self { + Self::with_constants_and_properties(name, parents, constants, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with constants, property contracts, and methods. + pub fn with_constants_and_properties( + name: impl Into, + parents: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + parents, + attributes: Vec::new(), + constants, + properties, + methods, + } + } + + /// Returns a copy of this interface with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this interface with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared interface name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns interface names extended directly by this eval interface. + pub fn parents(&self) -> &[String] { + &self.parents + } + + /// Returns attributes declared directly on this eval interface. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns constants declared directly by this eval interface. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one interface constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + + /// Returns property hook contracts declared directly by this eval interface. + pub fn properties(&self) -> &[EvalInterfaceProperty] { + &self.properties + } + + /// Returns method signatures declared directly by this eval interface. + pub fn methods(&self) -> &[EvalInterfaceMethod] { + &self.methods + } +} + +/// Property hook contract metadata for a runtime eval interface. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalInterfaceProperty { + name: String, + attributes: Vec, + property_type: Option, + set_visibility: Option, + requires_get: bool, + requires_set: bool, +} + +impl EvalInterfaceProperty { + /// Creates one eval interface property contract. + pub fn new(name: impl Into, requires_get: bool, requires_set: bool) -> Self { + Self { + name: name.into(), + attributes: Vec::new(), + property_type: None, + set_visibility: None, + requires_get, + requires_set, + } + } + + /// Returns a copy of this interface property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + + /// Returns a copy of this interface property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + + /// Returns a copy of this interface property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the PHP-visible property name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns attributes declared directly on this interface property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns retained PHP type metadata for this interface property contract. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + + /// Returns the PHP asymmetric write visibility declared by this contract, if any. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility required for writes by this property contract. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => EvalVisibility::Public, + } + } + + /// Returns whether the interface requires the property to be readable. + pub const fn requires_get(&self) -> bool { + self.requires_get + } + + /// Returns whether the interface requires the property to be writable. + pub const fn requires_set(&self) -> bool { + self.requires_set + } + + /// Returns a merged contract containing either side's get/set requirements. + pub fn merged_with(&self, other: &Self) -> Self { + Self { + name: self.name.clone(), + attributes: self.attributes.clone(), + property_type: self + .property_type + .clone() + .or_else(|| other.property_type.clone()), + set_visibility: merge_eval_property_set_visibility( + self.set_visibility, + other.set_visibility, + ), + requires_get: self.requires_get || other.requires_get, + requires_set: self.requires_set || other.requires_set, + } + } +} + +/// Merges interface property set-visibility contracts by keeping the stricter write requirement. +const fn merge_eval_property_set_visibility( + left: Option, + right: Option, +) -> Option { + let left = match left { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let right = match right { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let merged = if eval_visibility_rank(left) < eval_visibility_rank(right) { + left + } else { + right + }; + match merged { + EvalVisibility::Public => None, + visibility => Some(visibility), + } +} + +/// Returns a comparable visibility rank where smaller means more restrictive. +const fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + +/// Method signature metadata for a runtime eval interface. +#[derive(Debug, Clone)] +pub struct EvalInterfaceMethod { + name: String, + source_location: Option, + attributes: Vec, + is_static: bool, + params: Vec, + parameter_attributes: Vec>, + parameter_has_types: Vec, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, +} + +impl PartialEq for EvalInterfaceMethod { + /// Compares interface method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.is_static == other.is_static + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + } +} + +impl EvalInterfaceMethod { + /// Creates one dynamic eval interface method signature. + pub fn new(name: impl Into, params: Vec) -> Self { + let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + is_static: false, + params, + parameter_attributes, + parameter_has_types, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + } + } + + /// Returns a copy of this interface method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this interface method with its static modifier flag set. + pub fn with_static(mut self, is_static: bool) -> Self { + self.is_static = is_static; + self + } + + /// Returns a copy of this interface method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this interface method with parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + + /// Returns a copy of this interface method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this interface method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this interface method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this interface method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this interface method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this interface method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this interface method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns whether this interface method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/methods.rs b/crates/elephc-magician/src/eval_ir/methods.rs new file mode 100644 index 0000000000..2a796ff8d9 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/methods.rs @@ -0,0 +1,301 @@ +//! Purpose: +//! Defines eval class methods and their complete callable metadata. +//! +//! Called from: +//! - Class-like parsing, validation, dynamic invocation, closures, and Reflection. +//! +//! Key details: +//! - Parameters, statics, visibility, hook bodies, return types, and source metadata remain aligned. + +use super::*; + +/// Public method metadata for a runtime eval class. +#[derive(Debug, Clone)] +pub struct EvalClassMethod { + name: String, + trait_origin: Option, + trait_origin_method: Option, + source_location: Option, + attributes: Vec, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + params: Vec, + parameter_attributes: Vec>, + parameter_has_types: Vec, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, +} + +impl PartialEq for EvalClassMethod { + /// Compares class method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.trait_origin == other.trait_origin + && self.trait_origin_method == other.trait_origin_method + && self.attributes == other.attributes + && self.visibility == other.visibility + && self.is_static == other.is_static + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + +impl EvalClassMethod { + /// Creates a public eval class method with source-order parameters and body. + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + Self::with_modifiers(name, false, false, params, body) + } + + /// Creates a public eval class method with optional abstract/final modifiers. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, + ) -> Self { + Self::with_visibility_and_modifiers( + name, + EvalVisibility::Public, + false, + is_abstract, + is_final, + params, + body, + ) + } + + /// Creates an eval class method with explicit visibility and optional modifiers. + pub fn with_visibility_and_modifiers( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, + ) -> Self { + let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + trait_origin: None, + trait_origin_method: None, + source_location: None, + attributes: Vec::new(), + visibility, + is_static, + is_abstract, + is_final, + params, + parameter_attributes, + parameter_has_types, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + body, + } + } + + /// Returns a copy of this method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns a copy of this method with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + self.trait_origin_method = Some(self.name.clone()); + } + self + } + + /// Returns the trait that originally declared this imported method, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns the PHP `__FUNCTION__` value for this method body. + pub fn magic_function_name(&self) -> &str { + self.trait_origin_method.as_deref().unwrap_or(&self.name) + } + + /// Returns the PHP `__METHOD__` value for this method body. + pub fn magic_method_name(&self, class_name: &str) -> String { + let owner = self.trait_origin().unwrap_or(class_name); + format!( + "{}::{}", + owner.trim_start_matches('\\'), + self.magic_function_name() + ) + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns a copy of this method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this method with source-order parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + + /// Returns a copy of this method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns attributes declared directly on this class method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns a copy of this method with a different PHP-visible name. + pub fn renamed(&self, name: impl Into) -> Self { + let mut method = self.clone(); + method.name = name.into(); + method + } + + /// Returns a copy of this method with a different PHP visibility. + pub fn with_visibility_override(&self, visibility: EvalVisibility) -> Self { + let mut method = self.clone(); + method.visibility = visibility; + method + } + + /// Returns the PHP visibility declared for this method. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns whether this method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns whether this eval-declared method was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared method was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns the dynamic EvalIR statements that form the method body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} diff --git a/crates/elephc-magician/src/eval_ir/program.rs b/crates/elephc-magician/src/eval_ir/program.rs new file mode 100644 index 0000000000..3f1aa4cba8 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/program.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Defines parsed EvalIR programs and source-location metadata. +//! +//! Called from: +//! - Parser entry points, declaration metadata, Reflection, and interpreter execution. +//! +//! Key details: +//! - Source offsets and file/line ranges remain syntax metadata, not runtime cells. + +use super::*; + +/// Parsed eval fragment lowered into dynamic by-name statements. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalProgram { + source_len: usize, + statements: Vec, +} + +impl EvalProgram { + /// Creates an EvalIR program for a source fragment and statement list. + pub fn new(source_len: usize, statements: Vec) -> Self { + Self { + source_len, + statements, + } + } + + /// Returns the byte length of the parsed eval fragment. + pub const fn source_len(&self) -> usize { + self.source_len + } + + /// Returns the ordered EvalIR statements in source order. + pub fn statements(&self) -> &[EvalStmt] { + &self.statements + } + + /// Consumes the program and returns its statement list. + pub fn into_statements(self) -> Vec { + self.statements + } +} + +/// One source range inside the current eval fragment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EvalSourceLocation { + start_line: i64, + end_line: i64, +} + +impl EvalSourceLocation { + /// Creates a source range using one-based eval-fragment line numbers. + pub const fn new(start_line: i64, end_line: i64) -> Self { + Self { + start_line, + end_line, + } + } + + /// Creates a single-line source range. + pub const fn single_line(line: i64) -> Self { + Self::new(line, line) + } + + /// Returns the one-based line where the declaration starts. + pub const fn start_line(&self) -> i64 { + self.start_line + } + + /// Returns the one-based line where the declaration ends. + pub const fn end_line(&self) -> i64 { + self.end_line + } +} diff --git a/crates/elephc-magician/src/eval_ir/properties.rs b/crates/elephc-magician/src/eval_ir/properties.rs new file mode 100644 index 0000000000..8a87e48a3d --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/properties.rs @@ -0,0 +1,284 @@ +//! Purpose: +//! Defines eval class properties, hook metadata, defaults, and visibility. +//! +//! Called from: +//! - Class-like parsing, validation, object storage, property access, and Reflection. +//! +//! Key details: +//! - Readonly, promotion, asymmetric set visibility, backing slots, and hooks stay coherent. + +use super::*; + +/// Public property metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassProperty { + name: String, + trait_origin: Option, + attributes: Vec, + property_type: Option, + set_hook_type: Option, + visibility: EvalVisibility, + set_visibility: Option, + pub(super) is_static: bool, + is_final: bool, + pub(super) is_readonly: bool, + is_promoted: bool, + is_abstract: bool, + has_get_hook: bool, + has_set_hook: bool, + requires_get_hook: bool, + requires_set_hook: bool, + is_virtual: bool, + default: Option, +} + +impl EvalClassProperty { + /// Creates a public eval class property with an optional initializer. + pub fn new(name: impl Into, default: Option) -> Self { + Self::with_visibility(name, EvalVisibility::Public, default) + } + + /// Creates an eval class property with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + default: Option, + ) -> Self { + Self::with_visibility_and_static(name, visibility, false, default) + } + + /// Creates an eval class property with explicit PHP visibility and static metadata. + pub fn with_visibility_and_static( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + default: Option, + ) -> Self { + Self::with_visibility_static_and_readonly(name, visibility, is_static, false, default) + } + + /// Creates an eval class property with explicit storage and readonly metadata. + pub fn with_visibility_static_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_readonly: bool, + default: Option, + ) -> Self { + Self::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + false, + is_readonly, + default, + ) + } + + /// Creates an eval class property with explicit storage and modifier metadata. + pub fn with_visibility_static_final_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_readonly: bool, + default: Option, + ) -> Self { + Self { + name: name.into(), + trait_origin: None, + attributes: Vec::new(), + property_type: None, + set_hook_type: None, + visibility, + set_visibility: None, + is_static, + is_final, + is_readonly, + is_promoted: false, + is_abstract: false, + has_get_hook: false, + has_set_hook: false, + requires_get_hook: false, + requires_set_hook: false, + is_virtual: false, + default, + } + } + + /// Returns a copy of this property marked with concrete get/set hook metadata. + pub const fn with_hooks(mut self, has_get_hook: bool, has_set_hook: bool) -> Self { + self.has_get_hook = has_get_hook; + self.has_set_hook = has_set_hook; + self.is_virtual = has_get_hook || has_set_hook; + self + } + + /// Returns a copy of this property with explicit hook virtuality metadata. + pub const fn with_virtual(mut self, is_virtual: bool) -> Self { + self.is_virtual = is_virtual; + self + } + + /// Returns a copy of this property marked as an abstract hook contract. + pub const fn with_abstract_hook_contract( + mut self, + requires_get_hook: bool, + requires_set_hook: bool, + ) -> Self { + self.is_abstract = true; + self.requires_get_hook = requires_get_hook; + self.requires_set_hook = requires_set_hook; + self.is_virtual = true; + self + } + + /// Returns a copy of this property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this property with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + + /// Returns a copy of this property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + + /// Returns a copy of this property with retained explicit set-hook parameter type metadata. + pub fn with_set_hook_type(mut self, set_hook_type: Option) -> Self { + self.set_hook_type = set_hook_type; + self + } + + /// Returns a copy of this property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + + /// Returns a copy of this property marked as coming from constructor promotion. + pub const fn with_promoted(mut self) -> Self { + self.is_promoted = true; + self + } + + /// Returns the PHP-visible property name without `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the trait that originally declared this imported property, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns attributes declared directly on this class property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns retained PHP type metadata for this property. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + + /// Returns retained PHP type metadata for an explicit set-hook parameter. + pub fn set_hook_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type.as_ref() + } + + /// Returns the PHP-visible type accepted by property writes. + pub fn settable_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type().or_else(|| self.property_type()) + } + + /// Returns the PHP visibility declared for this property. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns the PHP asymmetric write visibility, if it differs from read visibility. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility that applies to writes for this property. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => self.visibility, + } + } + + /// Returns whether this property was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns whether this property was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns whether this property was declared `readonly`. + pub const fn is_readonly(&self) -> bool { + self.is_readonly + } + + /// Returns whether this property came from constructor property promotion. + pub const fn is_promoted(&self) -> bool { + self.is_promoted + } + + /// Returns whether this property is an abstract property hook contract. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this property has a concrete get hook accessor. + pub const fn has_get_hook(&self) -> bool { + self.has_get_hook + } + + /// Returns whether this property has a concrete set hook accessor. + pub const fn has_set_hook(&self) -> bool { + self.has_set_hook + } + + /// Returns whether this abstract property contract requires read access. + pub const fn requires_get_hook(&self) -> bool { + self.requires_get_hook + } + + /// Returns whether this abstract property contract requires write access. + pub const fn requires_set_hook(&self) -> bool { + self.requires_set_hook + } + + /// Returns whether this property is virtual instead of backed by object storage. + pub const fn is_virtual(&self) -> bool { + self.is_virtual + } + + /// Returns the property initializer expression, when one was declared. + pub fn default(&self) -> Option<&EvalExpr> { + self.default.as_ref() + } +} + +/// PHP visibility for eval-declared object members. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalVisibility { + Public, + Protected, + Private, +} diff --git a/crates/elephc-magician/src/eval_ir/statements.rs b/crates/elephc-magician/src/eval_ir/statements.rs new file mode 100644 index 0000000000..3750af8502 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/statements.rs @@ -0,0 +1,278 @@ +//! Purpose: +//! Defines EvalIR statement and catch-clause variants. +//! +//! Called from: +//! - Statement parser and interpreter statement dispatcher. +//! +//! Key details: +//! - Statements encode explicit mutation and structured control flow without runtime ownership. + +use super::*; + +/// Dynamic eval statements that operate on a materialized activation scope. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalStmt { + ArrayAppendVar { + name: String, + value: EvalExpr, + }, + ArraySetVar { + name: String, + index: EvalExpr, + value: EvalExpr, + }, + Break, + Continue, + DoWhile { + body: Vec, + condition: EvalExpr, + }, + Echo(EvalExpr), + For { + init: Vec, + condition: Option, + update: Vec, + body: Vec, + }, + ClassDecl(EvalClass), + EnumDecl(EvalEnum), + InterfaceDecl(EvalInterface), + TraitDecl(EvalTrait), + Foreach { + array: EvalExpr, + key_name: Option, + value_name: String, + body: Vec, + }, + FunctionDecl { + name: String, + source_location: Option, + attributes: Vec, + params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, + }, + Global { + vars: Vec, + }, + If { + condition: EvalExpr, + then_branch: Vec, + else_branch: Vec, + }, + Return(Option), + ReferenceAssign { + target: String, + source: String, + }, + PropertyReferenceBind { + object: EvalExpr, + property: String, + source: String, + }, + DynamicPropertyReferenceBind { + object: EvalExpr, + property: EvalExpr, + source: String, + }, + DynamicPropertySet { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicPropertyArrayAppend { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicPropertyArraySet { + object: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicPropertyCompoundAssign { + object: EvalExpr, + property: EvalExpr, + op: EvalBinOp, + value: EvalExpr, + }, + DynamicPropertyIncDec { + object: EvalExpr, + property: EvalExpr, + increment: bool, + }, + PropertySet { + object: EvalExpr, + property: String, + value: EvalExpr, + }, + PropertyArrayAppend { + object: EvalExpr, + property: String, + value: EvalExpr, + }, + PropertyArraySet { + object: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + PropertyCompoundAssign { + object: EvalExpr, + property: String, + op: EvalBinOp, + value: EvalExpr, + }, + PropertyIncDec { + object: EvalExpr, + property: String, + increment: bool, + }, + StaticPropertySet { + class_name: String, + property: String, + value: EvalExpr, + }, + StaticPropertyReferenceBind { + class_name: String, + property: String, + source: String, + }, + StaticPropertyArrayAppend { + class_name: String, + property: String, + value: EvalExpr, + }, + StaticPropertyArraySet { + class_name: String, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + StaticPropertyIncDec { + class_name: String, + property: String, + increment: bool, + }, + DynamicStaticPropertySet { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, + DynamicStaticPropertyReferenceBind { + class_name: EvalExpr, + property: String, + source: String, + }, + DynamicStaticPropertyArrayAppend { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, + DynamicStaticPropertyArraySet { + class_name: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicStaticPropertyIncDec { + class_name: EvalExpr, + property: String, + increment: bool, + }, + DynamicStaticPropertyNameSet { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr, + property: EvalExpr, + source: String, + }, + DynamicStaticPropertyNameArrayAppend { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameArraySet { + class_name: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicStaticPropertyNameIncDec { + class_name: EvalExpr, + property: EvalExpr, + increment: bool, + }, + StaticVar { + name: String, + init: EvalExpr, + }, + StoreVar { + name: String, + value: EvalExpr, + }, + Switch { + expr: EvalExpr, + cases: Vec, + }, + Throw(EvalExpr), + Try { + body: Vec, + catches: Vec, + finally_body: Vec, + }, + UnsetArrayElement { + array: EvalExpr, + index: EvalExpr, + }, + UnsetProperty { + object: EvalExpr, + property: String, + }, + UnsetDynamicProperty { + object: EvalExpr, + property: EvalExpr, + }, + UnsetStaticProperty { + class_name: String, + property: String, + }, + UnsetDynamicStaticProperty { + class_name: EvalExpr, + property: String, + }, + UnsetDynamicStaticPropertyName { + class_name: EvalExpr, + property: EvalExpr, + }, + UnsetVar { + name: String, + }, + While { + condition: EvalExpr, + body: Vec, + }, + Expr(EvalExpr), +} + +/// One `catch` block attached to an eval `try` statement. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCatch { + pub class_names: Vec, + pub var_name: Option, + pub body: Vec, +} diff --git a/crates/elephc-magician/src/eval_ir/traits.rs b/crates/elephc-magician/src/eval_ir/traits.rs new file mode 100644 index 0000000000..7ffd2f14f8 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/traits.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! Defines eval trait declarations and composed member metadata. +//! +//! Called from: +//! - Trait parser, trait composition, context lookup, and Reflection. +//! +//! Key details: +//! - Nested traits, adaptations, methods, properties, constants, and attributes stay grouped. + +use super::*; + +/// Runtime trait declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalTrait { + name: String, + source_location: Option, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalTrait { + /// Compares trait metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalTrait { + /// Creates a dynamic eval trait with public properties and methods. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_constants(name, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval trait with constants, properties, and methods. + pub fn with_constants( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval trait with trait uses, adaptations, constants, properties, and methods. + pub fn with_constants_traits_adaptations( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + traits, + trait_adaptations, + constants, + properties, + methods, + } + } + + /// Returns a copy of this trait with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this trait with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared trait name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this eval trait. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval trait. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval trait. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns constants declared directly by this eval trait. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one trait constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + + /// Returns public properties declared directly by this eval trait. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval trait. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } +} diff --git a/crates/elephc-magician/src/ffi/callables.rs b/crates/elephc-magician/src/ffi/callables.rs new file mode 100644 index 0000000000..77235b2e14 --- /dev/null +++ b/crates/elephc-magician/src/ffi/callables.rs @@ -0,0 +1,113 @@ +//! Purpose: +//! Exports post-barrier callable dispatch and probes for callback values that +//! may reference eval-declared functions, methods, or objects. Generated code +//! uses this ABI when native descriptor metadata cannot answer dynamically. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_callable_call_array`. +//! - Generated EIR backend assembly through `__elephc_eval_is_callable`. +//! +//! Key details: +//! - Callback and argument containers are boxed Mixed cells owned by generated +//! code. Dispatch results and uncaught throwables are returned through +//! `ElephcEvalResult`; probe failures fail closed as `false`. + +use super::util::{clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Checks whether a callback value is callable in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle and `callback` must point at a +/// boxed runtime cell. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_is_callable( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_is_callable_inner(ctx, callback) }).unwrap_or(0) +} + +/// Dispatches a callback value with a PHP argument array through the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `callback` and `arg_array` must +/// point at boxed runtime cells, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_callable_call_array( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_callable_call_array_inner(ctx, callback, arg_array, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval callable-probe ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_is_callable`; invalid handles fail closed as false. +#[cfg(not(test))] +unsafe fn eval_is_callable_inner( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || callback.is_null() { + return 0; + } + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_is_callable( + context, + RuntimeCellHandle::from_raw(callback), + &mut values, + ) { + Ok(callable) => i32::from(callable), + Err(_) => 0, + } +} + +/// Runs the eval callable-array ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_callable_call_array`; callers must provide a valid +/// context and boxed callback/argument-array cells. +#[cfg(not(test))] +unsafe fn eval_callable_call_array_inner( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if callback.is_null() || arg_array.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_callable_call_array_outcome( + context, + RuntimeCellHandle::from_raw(callback), + RuntimeCellHandle::from_raw(arg_array), + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs new file mode 100644 index 0000000000..77ee8ef37d --- /dev/null +++ b/crates/elephc-magician/src/ffi/context.rs @@ -0,0 +1,286 @@ +//! Purpose: +//! Exports eval context handle allocation and context metadata setters. +//! These functions manage process-level eval state and call-site/global/class +//! scope metadata used while executing fragments. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_context_*` symbols. +//! +//! Key details: +//! - Context handles are opaque across the ABI. +//! - Call-site metadata is UTF-8 and is validated before storing. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ElephcEvalScope, ABI_VERSION}; +use crate::context::native_frame_called_class_override_bytes; +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::ffi::dynamic_destructors::install_dynamic_object_destructor_hook; +use std::ptr; + +/// Returns the ABI version expected by generated elephc eval call sites. +#[no_mangle] +pub extern "C" fn __elephc_eval_abi_version() -> u32 { + ABI_VERSION +} + +/// Allocates a process-level eval context handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { + #[cfg(not(test))] + install_dynamic_object_destructor_hook(); + Box::into_raw(Box::new(ElephcEvalContext::new())) +} + +/// Frees a process-level eval context handle allocated by the eval bridge. +/// +/// # Safety +/// `ctx` must be null or a pointer returned by `__elephc_eval_context_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) { + if !ctx.is_null() { + if let Some(context) = unsafe { ctx.as_ref() } { + context.unregister_dynamic_object_context(); + } + drop(Box::from_raw(ctx)); + } +} + +/// Records source metadata for the next eval fragment executed in this context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `file_ptr` and `dir_ptr` must be +/// readable for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_call_site( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_context_set_call_site_inner(ctx, file_ptr, file_len, dir_ptr, dir_len, line) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Records the materialized program-global eval scope for `global` aliases. +/// +/// # Safety +/// `ctx` and `scope` must be valid handles allocated by the eval bridge. The +/// context does not own `scope`; generated code must keep the scope alive for +/// as long as the context can execute eval fragments that reference globals. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_global_scope( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_context_set_global_scope_inner(ctx, scope) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Enters a generated caller's class scope for the next eval fragment. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name pointers must be +/// readable UTF-8 slices for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_push_class_scope( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + called_class_ptr: *const u8, + called_class_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_context_push_class_scope_inner( + ctx, + class_ptr, + class_len, + called_class_ptr, + called_class_len, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Leaves a generated caller class scope after an eval fragment returns. +/// +/// # Safety +/// `ctx` must be a valid eval context handle previously passed to +/// `__elephc_eval_context_push_class_scope`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_pop_class_scope(ctx: *mut ElephcEvalContext) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_context_pop_class_scope_inner(ctx) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Reads the late-static override currently installed for a generated/AOT frame. +/// +/// # Safety +/// `class_ptr` must be a readable UTF-8 slice for `class_len` bytes. `out_ptr` +/// and `out_len` must be valid writable out-parameters. Returned bytes are +/// owned by eval thread-local state and remain valid until the native frame +/// override guard is dropped. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_called_class_override( + class_ptr: *const u8, + class_len: u64, + out_ptr: *mut *const u8, + out_len: *mut u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_called_class_override_inner(class_ptr, class_len, out_ptr, out_len) + }) + .unwrap_or(0) +} + +/// Runs the call-site metadata setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_call_site`; callers must pass a valid +/// context and readable UTF-8 file/directory byte slices. +unsafe fn eval_context_set_call_site_inner( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(file) = abi_name_to_string(file_ptr, file_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(dir) = abi_name_to_string(dir_ptr, dir_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(line) = i64::try_from(line) else { + return EvalStatus::RuntimeFatal.code(); + }; + context.set_call_site(file, dir, line); + EvalStatus::Ok.code() +} + +/// Runs the global-scope setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_global_scope`; callers must pass valid +/// context and scope handles owned by generated code. +unsafe fn eval_context_set_global_scope_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if !context.set_global_scope(scope) { + return EvalStatus::RuntimeFatal.code(); + } + EvalStatus::Ok.code() +} + +/// Runs the class-scope push ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_push_class_scope`; callers must pass a valid +/// context and readable UTF-8 class-name byte slices. +unsafe fn eval_context_push_class_scope_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + called_class_ptr: *const u8, + called_class_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(called_class_name) = abi_name_to_string(called_class_ptr, called_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let class_name = class_name.trim_start_matches('\\').to_string(); + if class_name.is_empty() { + return EvalStatus::RuntimeFatal.code(); + } + let called_class_name = called_class_name.trim_start_matches('\\'); + let called_class_name = if called_class_name.is_empty() { + class_name.clone() + } else { + called_class_name.to_string() + }; + let called_class_name = context + .native_frame_called_class_override(&class_name, &called_class_name) + .unwrap_or(called_class_name); + context.push_class_scope(class_name); + context.push_called_class_scope(called_class_name); + EvalStatus::Ok.code() +} + +/// Runs the class-scope pop ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_pop_class_scope`; callers must pass a valid +/// context handle created by the eval bridge. +unsafe fn eval_context_pop_class_scope_inner(ctx: *mut ElephcEvalContext) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + context.pop_called_class_scope(); + context.pop_class_scope(); + EvalStatus::Ok.code() +} + +/// Runs the native-frame called-class lookup after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_called_class_override`; generated code +/// passes writable stack slots for both out-parameters. +unsafe fn eval_native_frame_called_class_override_inner( + class_ptr: *const u8, + class_len: u64, + out_ptr: *mut *const u8, + out_len: *mut u64, +) -> i32 { + if out_ptr.is_null() || out_len.is_null() { + return 0; + } + unsafe { + *out_ptr = ptr::null(); + *out_len = 0; + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return 0; + }; + let Some((called_ptr, called_len)) = native_frame_called_class_override_bytes(&class_name) + else { + return 0; + }; + let Ok(called_len) = u64::try_from(called_len) else { + return 0; + }; + unsafe { + *out_ptr = called_ptr; + *out_len = called_len; + } + 1 +} diff --git a/crates/elephc-magician/src/ffi/declared_symbols.rs b/crates/elephc-magician/src/ffi/declared_symbols.rs new file mode 100644 index 0000000000..267e5700d3 --- /dev/null +++ b/crates/elephc-magician/src/ffi/declared_symbols.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Exports registration of generated declaration-name metadata into eval. +//! The interpreter uses these lists to answer `get_declared_*()` calls without +//! treating AOT class-like symbols as eval-owned declarations. +//! +//! Called from: +//! - Generated EIR backend assembly when a persistent eval context is created. +//! +//! Key details: +//! - Invalid context handles, ABI versions, or names fail closed as `false`. +//! - Duplicate names are ignored case-insensitively while preserving first spelling. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; + +/// Class-like declaration list selected by one registration entry point. +#[derive(Clone, Copy)] +enum DeclaredSymbolKind { + Class, + Interface, + Trait, +} + +/// Registers one generated class or enum name for eval `get_declared_classes()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_class_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Class) + }) + .unwrap_or(0) +} + +/// Registers one generated interface name for eval `get_declared_interfaces()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_interface_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Interface) + }) + .unwrap_or(0) +} + +/// Registers one generated trait name for eval `get_declared_traits()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_trait_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Trait) + }) + .unwrap_or(0) +} + +/// Runs declared-name registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors the exported registration functions; invalid handles or unreadable +/// name storage fail closed as `false`. +unsafe fn register_declared_symbol_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + kind: DeclaredSymbolKind, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let registered = match kind { + DeclaredSymbolKind::Class => context.define_external_declared_class_name(&name), + DeclaredSymbolKind::Interface => context.define_external_declared_interface_name(&name), + DeclaredSymbolKind::Trait => context.define_external_declared_trait_name(&name), + }; + i32::from(registered) +} diff --git a/crates/elephc-magician/src/ffi/dynamic_destructors.rs b/crates/elephc-magician/src/ffi/dynamic_destructors.rs new file mode 100644 index 0000000000..862fbad004 --- /dev/null +++ b/crates/elephc-magician/src/ffi/dynamic_destructors.rs @@ -0,0 +1,146 @@ +//! Purpose: +//! Owns the process-local registry that lets native object release call back +//! into eval-declared `__destruct()` methods for dynamic classes. +//! +//! Called from: +//! - `crate::context::ElephcEvalContext` when dynamic objects are registered or freed. +//! - The generated runtime through the installed destructor hook function pointer. +//! +//! Key details: +//! - The runtime owns object storage and calls this hook while the object is still +//! intact but already in the final-release path. +//! - Registry values are stored as integer addresses so the global mutex remains +//! `Sync`; every use revalidates null pointers and ABI version. + +#[cfg(not(test))] +use crate::abi::ABI_VERSION; +use crate::abi::ElephcEvalContext; +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::interpreter::eval_dynamic_destructor_for_object_cell; +#[cfg(not(test))] +use crate::interpreter::RuntimeValueOps; +#[cfg(not(test))] +use crate::runtime_hooks::{self, ElephcRuntimeOps}; +#[cfg(not(test))] +use crate::value::RuntimeCell; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +static DYNAMIC_DESTRUCTOR_CONTEXTS: OnceLock>> = OnceLock::new(); + +/// Returns the process-local dynamic object to eval context registry. +fn dynamic_destructor_contexts() -> &'static Mutex> { + DYNAMIC_DESTRUCTOR_CONTEXTS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Installs the eval dynamic object destructor callback into the generated runtime. +#[cfg(not(test))] +pub(crate) fn install_dynamic_object_destructor_hook() { + unsafe { + runtime_hooks::install_dynamic_object_destructor_hook( + __elephc_eval_dynamic_object_destruct as *const () as usize, + ); + } +} + +/// Records which eval context owns one dynamic object's eval class metadata. +pub(crate) fn register_dynamic_object_context(identity: u64, context: *mut ElephcEvalContext) { + if identity == 0 || context.is_null() { + return; + } + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.insert(identity, context as usize); + } +} + +/// Removes one dynamic object from the process-local destructor registry. +pub(crate) fn unregister_dynamic_object(identity: u64) { + if identity == 0 { + return; + } + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.remove(&identity); + } +} + +/// Removes every dynamic object currently associated with a soon-to-be-freed context. +pub(crate) fn unregister_dynamic_objects_for_context(context: *mut ElephcEvalContext) { + if context.is_null() { + return; + } + let context = context as usize; + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.retain(|_, owner| *owner != context); + } +} + +/// Looks up the eval context that owns one dynamic object identity. +#[cfg(not(test))] +pub(crate) fn dynamic_object_owner_context(identity: u64) -> Option<*mut ElephcEvalContext> { + let contexts = dynamic_destructor_contexts().lock().ok()?; + let context = *contexts.get(&identity)?; + Some(context as *mut ElephcEvalContext) +} + +/// Runs an eval dynamic object destructor from the native object free path. +/// +/// # Safety +/// `object` must be null or a live elephc runtime object pointer. The runtime +/// calls this only while its object destruction guard bit is set, so boxing the +/// borrowed object for `$this` cannot recursively free the same storage. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_dynamic_object_destruct( + object: *mut RuntimeCell, +) -> u64 { + std::panic::catch_unwind(|| unsafe { dynamic_object_destruct_inner(object) }).unwrap_or(0) +} + +/// Executes the callback body after the exported ABI shim has installed a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_dynamic_object_destruct`; callers must pass a live raw +/// object pointer whose refcount guard already marks destruction as active. +#[cfg(not(test))] +unsafe fn dynamic_object_destruct_inner(object: *mut RuntimeCell) -> u64 { + if object.is_null() { + return 0; + } + let identity = object as u64; + let Some(context) = dynamic_object_owner_context(identity) else { + return 0; + }; + let Some(context) = (unsafe { context.as_mut() }) else { + unregister_dynamic_object(identity); + return 0; + }; + if context.abi_version() != ABI_VERSION { + unregister_dynamic_object(identity); + return 0; + } + if context.dynamic_object_class(identity).is_none() { + unregister_dynamic_object(identity); + return 0; + } + + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + let object_cell = match ElephcRuntimeOps::object_from_raw(object) { + Ok(object_cell) => object_cell, + Err(_) => { + context.forget_dynamic_object(identity); + return 1; + } + }; + let destruct_result = + eval_dynamic_destructor_for_object_cell(identity, object_cell, context, &mut values); + let release_result = values.release(object_cell); + context.forget_dynamic_object(identity); + match (destruct_result, release_result) { + (Ok(true), Ok(())) => 1, + (Ok(false), Ok(())) => 0, + (Err(EvalStatus::UnsupportedConstruct), _) => 1, + (Err(_), _) | (_, Err(_)) => 1, + } +} diff --git a/crates/elephc-magician/src/ffi/execute.rs b/crates/elephc-magician/src/ffi/execute.rs new file mode 100644 index 0000000000..c481a31505 --- /dev/null +++ b/crates/elephc-magician/src/ffi/execute.rs @@ -0,0 +1,128 @@ +//! Purpose: +//! Exports eval fragment execution through the optional bridge. +//! This layer validates ABI pointers, parses fragment bytes, and dispatches +//! parsed EvalIR to the interpreter with runtime hooks in production builds. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_execute`. +//! +//! Key details: +//! - Tests keep a controlled unsupported stub because generated runtime wrappers +//! are not linked into the crate unit-test binary. + +use super::util::clear_result; +#[cfg(not(test))] +use super::util::write_outcome; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::eval_ir; +#[cfg(not(test))] +use crate::interpreter; +use crate::parse_cache; +#[cfg(not(test))] +use crate::runtime_hooks::ElephcRuntimeOps; +use std::slice; + +/// Executes an eval fragment against a materialized caller scope. +/// +/// The FFI shape is final for the initial bridge: context/scope are opaque +/// runtime handles, `code_ptr`/`code_len` identify the PHP fragment bytes, and +/// `out` receives the eval return cell when provided. Non-test builds execute +/// the current EvalIR subset; test builds return `UnsupportedConstruct` because +/// they do not link elephc's generated runtime value wrappers. +/// +/// # Safety +/// Callers must pass valid pointers for any non-null handle and ensure +/// `code_ptr` is readable for `code_len` bytes when `code_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_execute( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { execute_eval_inner(ctx, scope, code_ptr, code_len, out) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval ABI body after the exported wrapper has installed a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_execute`; callers must provide valid handles and code +/// storage for every non-null pointer argument. +unsafe fn execute_eval_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + if !ctx.is_null() && (*ctx).abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if code_len > 0 && code_ptr.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(code_len) = usize::try_from(code_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let code = if code_len == 0 { + &[] + } else { + slice::from_raw_parts(code_ptr, code_len) + }; + let program = match parse_cache::parse_fragment_cached(code) { + Ok(program) => program, + Err(err) => return err.status().code(), + }; + clear_result(out); + execute_parsed_eval(ctx, scope, program.as_ref(), out) +} + +/// Executes a parsed eval program in production builds using elephc runtime hooks. +/// +/// # Safety +/// `scope` and `out` must be null or valid pointers supplied by generated code. +#[cfg(not(test))] +unsafe fn execute_parsed_eval( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + program: &eval_ir::EvalProgram, + out: *mut ElephcEvalResult, +) -> i32 { + let mut fallback_context; + let context = if let Some(ctx) = ctx.as_mut() { + ctx + } else { + fallback_context = ElephcEvalContext::new(); + &mut fallback_context + }; + let mut fallback_scope; + let scope = if let Some(scope) = scope.as_mut() { + scope + } else { + fallback_scope = ElephcEvalScope::new(); + &mut fallback_scope + }; + context.sync_global_eval_classes(); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Keeps crate unit tests independent from generated runtime assembly wrappers. +/// +/// # Safety +/// `out` must be null or valid result storage supplied by the test caller. +#[cfg(test)] +unsafe fn execute_parsed_eval( + _ctx: *mut ElephcEvalContext, + _scope: *mut ElephcEvalScope, + _program: &eval_ir::EvalProgram, + _out: *mut ElephcEvalResult, +) -> i32 { + EvalStatus::UnsupportedConstruct.code() +} diff --git a/crates/elephc-magician/src/ffi/function_calls.rs b/crates/elephc-magician/src/ffi/function_calls.rs new file mode 100644 index 0000000000..5ab1ecf30e --- /dev/null +++ b/crates/elephc-magician/src/ffi/function_calls.rs @@ -0,0 +1,170 @@ +//! Purpose: +//! Exports post-barrier calls into functions declared by eval fragments. +//! Generated code can call zero-argument, positional, or argument-array forms +//! after probing dynamic function existence. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_call_function*`. +//! +//! Key details: +//! - Calls return either a value cell or an uncaught throwable cell through +//! `ElephcEvalResult`. +//! - Argument pointer arrays are borrowed from generated code and not released here. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::slice; + +/// Calls a zero-argument function previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_inner(ctx, name_ptr, name_len, std::ptr::null(), 0, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a function previously declared through `eval()` with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a function previously declared through `eval()` with an argument array/hash. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `arg_array` must be a boxed Mixed +/// indexed or associative array cell, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_array( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_array_inner(ctx, name_ptr, name_len, arg_array, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the dynamic function-call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function`; callers must provide a valid context, +/// readable function-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn call_eval_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_function_outcome( + context, + &name.to_ascii_lowercase(), + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the dynamic function-call-array ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function_array`; callers must provide a valid +/// context, readable function-name bytes, and a boxed array/hash argument cell. +#[cfg(not(test))] +unsafe fn call_eval_function_array_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_array.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_function_call_array_outcome( + context, + &name.to_ascii_lowercase(), + RuntimeCellHandle::from_raw(arg_array), + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs new file mode 100644 index 0000000000..7f7e834c56 --- /dev/null +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Groups the exported C ABI entry points for the optional eval bridge. +//! Submodules are organized by the handle or operation family they expose. +//! +//! Called from: +//! - `crate` root re-exports for Rust tests and generated-linkage symbols. +//! +//! Key details: +//! - Every exported function installs a panic boundary before touching bridge state. +//! - Helper routines stay private to the FFI layer unless shared across families. + +#[cfg(not(test))] +pub mod callables; +pub mod context; +pub mod declared_symbols; +pub(crate) mod dynamic_destructors; +pub mod execute; +#[cfg(not(test))] +pub mod function_calls; +pub mod native_functions; +pub mod native_methods; +#[cfg(not(test))] +pub mod object_construction; +#[cfg(not(test))] +pub mod object_introspection; +pub mod scope; +#[cfg(not(test))] +pub mod static_members; +pub mod symbols; +pub(crate) mod util; + +#[cfg(not(test))] +pub use callables::*; +pub use context::*; +pub use declared_symbols::*; +pub use execute::*; +#[cfg(not(test))] +pub use function_calls::*; +pub use native_functions::*; +pub use native_methods::*; +#[cfg(not(test))] +pub use object_construction::*; +#[cfg(not(test))] +pub use object_introspection::*; +pub use scope::*; +#[cfg(not(test))] +pub use static_members::*; +pub use symbols::*; + +#[cfg(test)] +mod tests; diff --git a/crates/elephc-magician/src/ffi/native_functions.rs b/crates/elephc-magician/src/ffi/native_functions.rs new file mode 100644 index 0000000000..a2fbf76b11 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_functions.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Exports registration of generated native PHP callbacks into an eval context. +//! Eval fragments use this metadata to call AOT functions through descriptor +//! invokers while preserving PHP-visible parameter names and defaults. +//! +//! Called from: +//! - Generated EIR backend assembly before fragments can call AOT functions. +//! +//! Key details: +//! - Invalid names, handles, descriptors, or indexes fail closed as `false`. +//! - Function names are stored under their PHP case-insensitive folded key. + +use super::native_methods::{ + native_callable_array_default, native_callable_object_default, native_callable_scalar_default, + native_callable_type_from_abi, NativeCallableTypePosition, +}; +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +use crate::context::{NativeCallableDefault, NativeFunction, NativeFunctionInvoker}; +use std::ffi::c_void; + +mod public_abi; +mod registration; + +pub use public_abi::*; +use registration::*; diff --git a/crates/elephc-magician/src/ffi/native_functions/public_abi.rs b/crates/elephc-magician/src/ffi/native_functions/public_abi.rs new file mode 100644 index 0000000000..fa0d8304db --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_functions/public_abi.rs @@ -0,0 +1,269 @@ +//! Purpose: +//! Exposes C ABI entry points for generated native PHP function signatures, +//! types, flags, bridge support, and defaults. +//! +//! Called from: +//! - Generated EIR backend assembly during native function registration. +//! +//! Key details: +//! - Every entry point installs a panic boundary before internal validation. + +use super::*; + +/// Registers a generated native PHP function callback in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow +/// the descriptor-invoker ABI emitted by generated code. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers whether a generated native PHP function can be invoked through its bridge. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_bridge_support( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_bridge_support_inner( + ctx, + function_name_ptr, + function_name_len, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter's by-ref and variadic flags. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_flags( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_flags_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_type_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_return_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_return_type_inner( + ctx, + function_name_ptr, + function_name_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_scalar( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_scalar_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_string( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_string_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_object( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_object_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_array( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_array_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} diff --git a/crates/elephc-magician/src/ffi/native_functions/registration.rs b/crates/elephc-magician/src/ffi/native_functions/registration.rs new file mode 100644 index 0000000000..f5c1fcc701 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_functions/registration.rs @@ -0,0 +1,350 @@ +//! Purpose: +//! Validates and records generated native function callbacks and callable +//! metadata after the public ABI panic boundary. +//! +//! Called from: +//! - `super::public_abi` registration entry points. +//! +//! Key details: +//! - Invalid handles, names, descriptors, types, defaults, or indexes fail closed. + +use super::*; + +/// Runs the native registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or +/// callback pointers fail closed as `false`. +pub(super) unsafe fn register_native_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || descriptor.is_null() { + return 0; + } + let Some(invoker) = invoker else { + return 0; + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let function = NativeFunction::new(descriptor, invoker, param_count); + i32::from( + context + .define_native_function(name.to_ascii_lowercase(), function) + .is_ok(), + ) +} + +/// Runs the native parameter-name registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param( + &function_name.to_ascii_lowercase(), + param_index, + param_name, + )) +} + +/// Runs native function bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_function_bridge_support_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + i32::from(context.define_native_function_bridge_supported( + &function_name.to_ascii_lowercase(), + supported != 0, + )) +} + +/// Runs native function parameter-flags registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_flags_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let function_name = function_name.to_ascii_lowercase(); + if !context.define_native_function_param_by_ref(&function_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic != 0 { + return i32::from(context.define_native_function_variadic_param( + &function_name, + param_index, + )); + } + 1 +} + +/// Runs native function parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_function_param_type( + &function_name.to_ascii_lowercase(), + param_index, + param_type, + )) +} + +/// Runs native function return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_function_return_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + i32::from(context.define_native_function_return_type( + &function_name.to_ascii_lowercase(), + return_type, + )) +} + +/// Runs native function scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_string_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native function object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_object_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_array_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Records a native function parameter default by folded function name. +/// +/// # Safety +/// `ctx` and `function_name_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +pub(super) unsafe fn register_native_function_param_default_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param_default( + &function_name.to_ascii_lowercase(), + param_index, + default, + )) +} diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs new file mode 100644 index 0000000000..5849488e1d --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Exports registration of generated native PHP method signatures and related +//! metadata into an eval context so runtime fragments can bind AOT calls and +//! validate generated interface contracts. +//! +//! Called from: +//! - Generated EIR backend assembly before fragments can call AOT methods. +//! +//! Key details: +//! - Invalid names, handles, or indexes fail closed as `false`. +//! - The metadata records parameter names and supported defaults; generated user +//! helpers still perform the actual method, static method, and constructor calls. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, + NativeCallableObjectDefaultArg, NativeCallableSignature, +}; +use crate::eval_ir::{ + EvalAttribute, EvalAttributeArg, EvalInterfaceProperty, EvalParameterType, + EvalParameterTypeVariant, +}; + +mod attribute_decoder; +mod callable_metadata; +mod constructor_registration; +mod method_registration; +mod property_registration; +mod public_abi; + +use attribute_decoder::*; +use callable_metadata::*; +use constructor_registration::*; +use method_registration::*; +use property_registration::*; +pub use public_abi::*; + +pub(in crate::ffi) use callable_metadata::{ + native_callable_array_default, native_callable_object_default, native_callable_scalar_default, + native_callable_type_from_abi, +}; + +const NATIVE_DEFAULT_NULL: u64 = 0; +const NATIVE_DEFAULT_BOOL: u64 = 1; +const NATIVE_DEFAULT_INT: u64 = 2; +const NATIVE_DEFAULT_FLOAT: u64 = 3; +pub(crate) const NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; +const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; +const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; +const NATIVE_MEMBER_ATTRIBUTE_CLASS: u8 = 3; +const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; +const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; +const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; +const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; +const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; +const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; +const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; +const NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; +const NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; + +#[derive(Clone, Copy)] +pub(super) enum NativeCallableTypePosition { + Parameter, + Return, +} diff --git a/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs b/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs new file mode 100644 index 0000000000..cf3a6709e7 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs @@ -0,0 +1,131 @@ +//! Purpose: +//! Decodes bounded binary member-attribute records emitted by generated native +//! code into eval attribute metadata. +//! +//! Called from: +//! - `super::property_registration` while registering class-member attributes. +//! +//! Key details: +//! - Every read is bounds checked and malformed or trailing data is rejected. + +use super::*; + +/// Decoded native member-attribute metadata record. +pub(super) struct NativeMemberAttributeRecord { + pub(super) owner_kind: u8, + pub(super) member_key: String, + pub(super) attribute: EvalAttribute, +} + +/// Decodes one generated native member-attribute metadata record. +pub(super) fn native_member_attribute_record_from_abi( + record_ptr: *const u8, + record_len: u64, +) -> Option { + if record_ptr.is_null() || record_len == 0 { + return None; + } + let record_len = usize::try_from(record_len).ok()?; + let bytes = unsafe { std::slice::from_raw_parts(record_ptr, record_len) }; + let mut offset = 0usize; + let owner_kind = native_attribute_take_u8(bytes, &mut offset)?; + let member_key = native_attribute_take_string(bytes, &mut offset)?; + let attribute_name = native_attribute_take_string(bytes, &mut offset)?; + let args = native_attribute_take_args(bytes, &mut offset)?; + (offset == bytes.len()).then_some(NativeMemberAttributeRecord { + owner_kind, + member_key, + attribute: EvalAttribute::new(attribute_name, args), + }) +} + +/// Decodes the optional argument vector from a native attribute record. +fn native_attribute_take_args( + bytes: &[u8], + offset: &mut usize, +) -> Option>> { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED => Some(None), + NATIVE_ATTRIBUTE_ARGS_SUPPORTED => { + let count = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut args = Vec::with_capacity(count); + for _ in 0..count { + args.push(native_attribute_take_arg(bytes, offset)?); + } + Some(Some(args)) + } + _ => None, + } +} + +/// Decodes one literal argument from a native attribute record. +fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARG_NULL => Some(EvalAttributeArg::Null), + NATIVE_ATTRIBUTE_ARG_BOOL => Some(EvalAttributeArg::Bool( + native_attribute_take_u8(bytes, offset)? != 0, + )), + NATIVE_ATTRIBUTE_ARG_INT => Some(EvalAttributeArg::Int(native_attribute_take_i64( + bytes, offset, + )?)), + NATIVE_ATTRIBUTE_ARG_FLOAT => Some(EvalAttributeArg::Float( + native_attribute_take_u64(bytes, offset)?, + )), + NATIVE_ATTRIBUTE_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) + } + NATIVE_ATTRIBUTE_ARG_NAMED => { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_attribute_take_arg(bytes, offset)?; + Some(EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } + NATIVE_ATTRIBUTE_ARG_ARRAY => { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_attribute_take_arg(bytes, offset)?); + } + Some(EvalAttributeArg::Array(elements)) + } + _ => None, + } +} + +/// Reads one UTF-8 string with a little-endian u32 byte length prefix. +pub(super) fn native_attribute_take_string(bytes: &[u8], offset: &mut usize) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let chunk = native_attribute_take_bytes(bytes, offset, len)?; + std::str::from_utf8(chunk).ok().map(str::to_string) +} + +/// Reads one little-endian i64 from a native attribute record. +pub(super) fn native_attribute_take_i64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(i64::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one little-endian u32 from a native attribute record. +pub(super) fn native_attribute_take_u32(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u32::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one byte from a native attribute record. +pub(super) fn native_attribute_take_u8(bytes: &[u8], offset: &mut usize) -> Option { + native_attribute_take_bytes(bytes, offset, 1).map(|chunk| chunk[0]) +} + +/// Reads one bounded byte slice and advances the decode offset. +pub(super) fn native_attribute_take_bytes<'a>( + bytes: &'a [u8], + offset: &mut usize, + len: usize, +) -> Option<&'a [u8]> { + let end = offset.checked_add(len)?; + let chunk = bytes.get(*offset..end)?; + *offset = end; + Some(chunk) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs b/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs new file mode 100644 index 0000000000..b7ba197da6 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Decodes generated callable type and default-value ABI metadata and splits +//! class-member registration keys. +//! +//! Called from: +//! - Native method and constructor registration helpers in this module tree. +//! - `crate::ffi::native_functions` for shared callable metadata decoding. +//! +//! Key details: +//! - Nested object and array defaults are decoded recursively with strict bounds. + +use super::*; + +/// Decodes tagged default kind/payload ABI fields into native callable metadata. +pub(in crate::ffi) fn native_callable_scalar_default( + default_kind: u64, + default_payload: u64, +) -> Option { + match default_kind { + NATIVE_DEFAULT_NULL => Some(NativeCallableDefault::Null), + NATIVE_DEFAULT_BOOL => Some(NativeCallableDefault::Bool(default_payload != 0)), + NATIVE_DEFAULT_INT => Some(NativeCallableDefault::Int(default_payload as i64)), + NATIVE_DEFAULT_FLOAT => Some(NativeCallableDefault::Float(f64::from_bits( + default_payload, + ))), + NATIVE_DEFAULT_EMPTY_ARRAY => Some(NativeCallableDefault::EmptyArray), + _ => None, + } +} + +/// Decodes an object-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +pub(in crate::ffi) unsafe fn native_callable_object_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let default = native_callable_object_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + +/// Decodes an array-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +pub(in crate::ffi) unsafe fn native_callable_array_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let default = native_callable_array_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + +/// Decodes an object-valued native callable default from a generated binary spec slice. +fn native_callable_object_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let class_name = native_attribute_take_string(bytes, offset)?; + let arg_count = usize::from(native_attribute_take_u8(bytes, offset)?); + if arg_count > MAX_NATIVE_OBJECT_DEFAULT_ARGS { + return None; + } + let mut args = Vec::with_capacity(arg_count); + for _ in 0..arg_count { + args.push(native_callable_object_default_arg(bytes, offset)?); + } + Some(NativeCallableDefault::Object { class_name, args }) +} + +/// Decodes an array-valued native callable default from a generated binary spec slice. +fn native_callable_array_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_callable_array_default_element(bytes, offset)?); + } + Some(NativeCallableDefault::Array(elements)) +} + +/// Decodes one array-default element and its optional static key. +fn native_callable_array_default_element( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let key = match native_attribute_take_u8(bytes, offset)? { + NATIVE_ARRAY_DEFAULT_KEY_AUTO => None, + NATIVE_ARRAY_DEFAULT_KEY_INT => { + Some(NativeCallableArrayDefaultKey::Int(native_attribute_take_i64( + bytes, offset, + )?)) + } + NATIVE_ARRAY_DEFAULT_KEY_STRING => Some(NativeCallableArrayDefaultKey::String( + native_attribute_take_string(bytes, offset)?, + )), + _ => return None, + }; + let value = native_callable_object_default_arg_value(bytes, offset)?; + Some(NativeCallableArrayDefaultElement { key, value }) +} + +/// Decodes one object-default constructor argument from a generated binary spec. +fn native_callable_object_default_arg( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let tag = native_attribute_take_u8(bytes, offset)?; + if tag == NATIVE_OBJECT_DEFAULT_ARG_NAMED { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_callable_object_default_arg_value(bytes, offset)?; + return Some(NativeCallableObjectDefaultArg::named(name, value)); + } + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) + .map(NativeCallableObjectDefaultArg::positional) +} + +/// Decodes one object-default constructor argument value from a generated binary spec. +fn native_callable_object_default_arg_value( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let tag = native_attribute_take_u8(bytes, offset)?; + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) +} + +/// Decodes one tagged object-default constructor argument value. +fn native_callable_object_default_arg_value_for_tag( + tag: u8, + bytes: &[u8], + offset: &mut usize, +) -> Option { + match tag { + NATIVE_OBJECT_DEFAULT_ARG_SCALAR => { + let kind = native_attribute_take_u64(bytes, offset)?; + let payload = native_attribute_take_u64(bytes, offset)?; + native_callable_scalar_default(kind, payload) + } + NATIVE_OBJECT_DEFAULT_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) + } + NATIVE_OBJECT_DEFAULT_ARG_OBJECT => native_callable_object_default_from_bytes(bytes, offset), + NATIVE_OBJECT_DEFAULT_ARG_ARRAY => native_callable_array_default_from_bytes(bytes, offset), + _ => None, + } +} + +/// Reads one little-endian u64 from a native binary metadata record. +pub(super) fn native_attribute_take_u64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u64::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Decodes one generated type-spec string into eval Reflection type metadata. +pub(in crate::ffi) fn native_callable_type_from_abi( + type_spec_ptr: *const u8, + type_spec_len: u64, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = abi_name_to_string(type_spec_ptr, type_spec_len).ok()?; + native_callable_type_from_spec(&type_spec, position) +} + +/// Parses the compact generated type syntax used by native signature registration. +fn native_callable_type_from_spec( + type_spec: &str, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = type_spec.trim(); + if type_spec.is_empty() { + return None; + } + let nullable_shorthand = type_spec.strip_prefix('?'); + let (type_spec, mut allows_null) = match nullable_shorthand { + Some(inner) => (inner, true), + None => (type_spec, false), + }; + if type_spec.contains('&') { + if allows_null || type_spec.contains('|') { + return None; + } + let variants = type_spec + .split('&') + .map(|member| native_callable_type_variant(member, position)) + .collect::>>()?; + if variants.iter().any(Option::is_none) { + return None; + } + return Some(EvalParameterType::intersection( + variants.into_iter().flatten().collect(), + )); + } + let mut variants = Vec::new(); + for member in type_spec.split('|') { + match native_callable_type_variant(member, position)? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } + } + if variants.is_empty() { + return None; + } + Some(EvalParameterType::new(variants, allows_null)) +} + +/// Converts one generated type member name into eval type metadata. +fn native_callable_type_variant( + member: &str, + position: NativeCallableTypePosition, +) -> Option> { + let member = member.trim(); + if member.is_empty() { + return None; + } + let lower = member.trim_start_matches('\\').to_ascii_lowercase(); + let variant = match lower.as_str() { + "array" => EvalParameterTypeVariant::Array, + "bool" => EvalParameterTypeVariant::Bool, + "callable" => EvalParameterTypeVariant::Callable, + "float" => EvalParameterTypeVariant::Float, + "int" => EvalParameterTypeVariant::Int, + "iterable" => EvalParameterTypeVariant::Iterable, + "mixed" => EvalParameterTypeVariant::Mixed, + "never" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Never + } + "null" => return Some(None), + "object" => EvalParameterTypeVariant::Object, + "string" => EvalParameterTypeVariant::String, + "void" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Void + } + "void" | "never" => return None, + "self" | "parent" | "static" => EvalParameterTypeVariant::Class(lower), + _ => EvalParameterTypeVariant::Class(member.trim_start_matches('\\').to_string()), + }; + Some(Some(variant)) +} + +/// Splits one generated `ClassName::methodName` metadata key into class and method pieces. +pub(super) fn split_method_key(method_key: &str) -> Option<(&str, &str)> { + let (class_name, method_name) = method_key.rsplit_once("::")?; + (!class_name.is_empty() && !method_name.is_empty()).then_some((class_name, method_name)) +} + +/// Splits one generated `ClassName::propertyName` metadata key into class and property pieces. +pub(super) fn split_property_key(property_key: &str) -> Option<(&str, &str)> { + split_method_key(property_key) +} + +/// Splits `ClassLike::DeclaringClassLike::propertyName` property metadata keys. +pub(super) fn split_three_part_property_key( + property_key: &str, +) -> Option<(&str, &str, &str)> { + let (owner_key, property_name) = property_key.rsplit_once("::")?; + let (class_like_name, declaring_class_like_name) = owner_key.rsplit_once("::")?; + (!class_like_name.is_empty() + && !declaring_class_like_name.is_empty() + && !property_name.is_empty()) + .then_some((class_like_name, declaring_class_like_name, property_name)) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs b/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs new file mode 100644 index 0000000000..28ff90acec --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs @@ -0,0 +1,291 @@ +//! Purpose: +//! Validates and records generated constructor signatures, parameter metadata, +//! bridge support, and default values. +//! +//! Called from: +//! - `super::public_abi` constructor registration entry points. +//! +//! Key details: +//! - Registration rejects malformed names, unsupported defaults, and indexes +//! outside the declared constructor signature. + +use super::*; + +/// Runs native constructor registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor`; invalid handles, names, +/// or counts fail closed as `false`. +pub(super) unsafe fn register_native_constructor_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + i32::from(context.define_native_constructor_signature( + &class_name, + NativeCallableSignature::new(param_count), + )) +} + +/// Runs native constructor parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) +} + +/// Runs native constructor parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_flags_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if !context.define_native_constructor_param_by_ref(&class_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(context.define_native_constructor_variadic_param(&class_name, param_index)) +} + +/// Runs native constructor bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_constructor_bridge_support_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + i32::from(context.define_native_constructor_bridge_supported(&class_name, supported != 0)) +} + +/// Runs native constructor parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_type`; invalid +/// handles, names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_type_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_constructor_param_type(&class_name, param_index, param_type)) +} + +/// Runs native constructor scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_scalar`; +/// invalid handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Runs native constructor string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_string`; +/// invalid handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_string_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native constructor object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_object`; +/// invalid handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_object_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Runs native constructor array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_array`; +/// invalid handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_array_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Records a native constructor parameter default in the constructor signature table. +/// +/// # Safety +/// `ctx` and `class_name_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +pub(super) unsafe fn register_native_constructor_param_default_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param_default(&class_name, param_index, default)) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/method_registration.rs b/crates/elephc-magician/src/ffi/native_methods/method_registration.rs new file mode 100644 index 0000000000..5af44e38f9 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/method_registration.rs @@ -0,0 +1,532 @@ +//! Purpose: +//! Validates and records generated method, interface-property, and abstract- +//! property metadata after the public ABI panic boundary. +//! +//! Called from: +//! - `super::public_abi` registration entry points. +//! +//! Key details: +//! - Invalid handles, names, types, defaults, or parameter indexes fail closed. + +use super::*; + +/// Runs native method registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method`; invalid handles, names, or +/// counts fail closed as `false`. +pub(super) unsafe fn register_native_method_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let signature = NativeCallableSignature::new(param_count); + if is_static { + i32::from(context.define_native_static_method_signature(class_name, method_name, signature)) + } else { + i32::from(context.define_native_method_signature(class_name, method_name, signature)) + } +} + +/// Runs native interface property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_interface_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_interface_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((interface_name, declaring_interface_name, property_name)) = + split_three_part_property_key(&property_key) + else { + return 0; + }; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_interface_property_requirement( + interface_name, + declaring_interface_name, + property, + )) +} + +/// Runs native abstract property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_abstract_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_abstract_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, declaring_class_name, property_name)) = + split_three_part_property_key(&property_key) + else { + return 0; + }; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_abstract_property_requirement( + class_name, + declaring_class_name, + property, + )) +} + +/// Runs native method parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param`; invalid handles, names, +/// or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } else { + i32::from(context.define_native_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } +} + +/// Runs native method parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_flags`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_flags_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let by_ref_registered = if is_static { + context.define_native_static_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + } else { + context.define_native_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + }; + if !by_ref_registered { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(if is_static { + context.define_native_static_method_variadic_param(class_name, method_name, param_index) + } else { + context.define_native_method_variadic_param(class_name, method_name, param_index) + }) +} + +/// Runs native method bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_method_bridge_support_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + i32::from(if is_static { + context.define_native_static_method_bridge_supported( + class_name, + method_name, + supported != 0, + ) + } else { + context.define_native_method_bridge_supported(class_name, method_name, supported != 0) + }) +} + +/// Runs native method parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } else { + i32::from(context.define_native_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } +} + +/// Runs native method return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_method_return_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_return_type( + class_name, + method_name, + return_type, + )) + } else { + i32::from(context.define_native_method_return_type(class_name, method_name, return_type)) + } +} + +/// Runs native method scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Runs native method string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_string_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native method object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_object_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Runs native method array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_array_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Records a native method parameter default in the selected instance/static table. +/// +/// # Safety +/// `ctx` and `method_key_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +pub(super) unsafe fn register_native_method_param_default_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } else { + i32::from(context.define_native_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } +} diff --git a/crates/elephc-magician/src/ffi/native_methods/property_registration.rs b/crates/elephc-magician/src/ffi/native_methods/property_registration.rs new file mode 100644 index 0000000000..fc6a7d2152 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/property_registration.rs @@ -0,0 +1,210 @@ +//! Purpose: +//! Validates and records native class parents, property contracts and defaults, +//! and generated class-member attributes. +//! +//! Called from: +//! - `super::public_abi` property and class metadata registration entry points. +//! +//! Key details: +//! - Property keys are decoded into their declaring class-like components before +//! context mutation. + +use super::*; + +/// Runs native parent-class registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_class_parent`; invalid handles or +/// names fail closed as `false`. +pub(super) unsafe fn register_native_class_parent_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(parent_name) = abi_name_to_string(parent_name_ptr, parent_name_len) else { + return 0; + }; + i32::from(context.define_native_class_parent(&class_name, &parent_name)) +} + +/// Runs native property-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_property_type_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_property_type(class_name, property_name, property_type)) +} + +/// Runs native property scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_scalar`; invalid +/// handles, names, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_property_default_scalar_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + +/// Runs native property string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_string`; invalid +/// handles, names, or string buffers fail closed as `false`. +pub(super) unsafe fn register_native_property_default_string_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_property_default_inner( + ctx, + property_key_ptr, + property_key_len, + NativeCallableDefault::String(default), + ) +} + +/// Runs native property array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_array`; invalid +/// handles, names, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_property_default_array_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + +/// Records a native property default in the property metadata table. +/// +/// # Safety +/// `ctx` and `property_key_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +pub(super) unsafe fn register_native_property_default_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + i32::from(context.define_native_property_default(class_name, property_name, default)) +} + +/// Runs native member-attribute registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_member_attribute`; invalid handles or +/// binary records fail closed as `false`. +pub(super) unsafe fn register_native_member_attribute_inner( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Some(record) = native_member_attribute_record_from_abi(record_ptr, record_len) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_CLASS => { + i32::from(context.define_native_class_attribute(&record.member_key, record.attribute)) + } + NATIVE_MEMBER_ATTRIBUTE_METHOD + | NATIVE_MEMBER_ATTRIBUTE_PROPERTY + | NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + let Some((class_name, member_name)) = split_method_key(&record.member_key) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( + class_name, + member_name, + record.attribute, + )), + NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from( + context.define_native_property_attribute(class_name, member_name, record.attribute), + ), + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + i32::from(context.define_native_constant_attribute( + class_name, + member_name, + record.attribute, + )) + } + _ => 0, + } + } + _ => 0, + } +} diff --git a/crates/elephc-magician/src/ffi/native_methods/public_abi.rs b/crates/elephc-magician/src/ffi/native_methods/public_abi.rs new file mode 100644 index 0000000000..2e9bb9c38a --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/public_abi.rs @@ -0,0 +1,969 @@ +//! Purpose: +//! Exposes the C ABI entry points that register generated method, constructor, +//! property, interface, and attribute metadata in an eval context. +//! +//! Called from: +//! - Generated EIR backend assembly during native fragment registration. +//! +//! Key details: +//! - Every entry point installs a panic boundary and delegates validation to a +//! focused registration helper. + +use super::*; + +/// Registers a generated native PHP method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, false, param_count) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP static-method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, true, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP interface property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `InterfaceName::DeclaringInterface::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_interface_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP abstract class property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `ClassName::DeclaringClass::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_abstract_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_abstract_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP static-method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + false, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP static method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + true, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method parameter declared type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method declared return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method declared return type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP constructor signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `class_name_ptr` must be readable +/// for `class_name_len` bytes. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_inner(ctx, class_name_ptr, class_name_len, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parameter name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP constructor parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_flags( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_flags_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP constructor. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_bridge_support( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_bridge_support_inner( + ctx, + class_name_ptr, + class_name_len, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and type-spec pointers must +/// be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_type( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_type_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_scalar( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_scalar_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_string( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_string_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_object( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_object_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_array( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_array_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP parent-class metadata in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parent name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_class_parent( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_class_parent_inner( + ctx, + class_name_ptr, + class_name_len, + parent_name_ptr, + parent_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string, and the type spec must be a +/// readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_type( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_type_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property scalar default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_scalar( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_scalar_inner( + ctx, + property_key_ptr, + property_key_len, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property string default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and default +/// string pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_string_inner( + ctx, + property_key_ptr, + property_key_len, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property array default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and encoded +/// default pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_array( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_array_inner( + ctx, + property_key_ptr, + property_key_len, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP class/member attribute in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `record_ptr` must point to one +/// readable binary member-attribute metadata record. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_member_attribute( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_member_attribute_inner(ctx, record_ptr, record_len) + }) + .unwrap_or(0) +} diff --git a/crates/elephc-magician/src/ffi/object_construction.rs b/crates/elephc-magician/src/ffi/object_construction.rs new file mode 100644 index 0000000000..05a2f2c924 --- /dev/null +++ b/crates/elephc-magician/src/ffi/object_construction.rs @@ -0,0 +1,389 @@ +//! Purpose: +//! Exports post-barrier construction and method calls for classes declared by +//! eval fragments. Generated code uses this ABI when native object operations +//! may target a dynamic eval class instead of an AOT class. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_new_object`. +//! - Generated EIR backend assembly through `__elephc_eval_try_new_object`. +//! - Generated EIR backend assembly through `__elephc_eval_method_call`. +//! - Generated EIR backend assembly through `__elephc_eval_static_method_call`. +//! +//! Key details: +//! - The ABI currently accepts positional arguments already boxed as Mixed cells. +//! - Calls return either a value cell or an uncaught throwable cell through +//! `ElephcEvalResult`. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::context::native_frame_called_class_override_context; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::slice; + +/// Constructs a class previously declared through `eval()` with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_new_object( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_new_object_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Attempts to construct an eval-declared class with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +/// Returns -1 when the class name is not declared in the eval context. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_try_new_object( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_try_new_object_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a method on a value that may be an eval-created object. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` must point at a boxed +/// runtime cell. `method_ptr` must be readable for `method_len` bytes when +/// `method_len > 0`. `arg_pack` points at a native-word count followed by that +/// many runtime-cell pointers, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_method_call( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_method_call_inner(ctx, object, method_ptr, method_len, arg_pack, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a static method on a class previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target_ptr` must be readable +/// for `target_len` bytes and contain `ClassName::method`. `arg_pack` points at +/// a native-word count followed by that many runtime-cell pointers, and `out` +/// may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_method_call( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_method_call_inner(ctx, target_ptr, target_len, arg_pack, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a static method through the current eval late-static AOT-frame override. +/// +/// # Safety +/// `frame_class_ptr` and `method_ptr` must be readable UTF-8 byte ranges. +/// `arg_pack` points at a native-word count followed by that many runtime-cell +/// pointers, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_method_call( + frame_class_ptr: *const u8, + frame_class_len: u64, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_method_call_inner( + frame_class_ptr, + frame_class_len, + method_ptr, + method_len, + arg_pack, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the dynamic object-construction ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_new_object`; callers must provide a valid context, +/// readable class-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn eval_new_object_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_new_object_outcome(context, &name, args, &mut values) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the dynamic object-construction probe ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_try_new_object`; callers must provide a valid context, +/// readable class-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn eval_try_new_object_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_try_new_object_outcome(context, &name, args, &mut values) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => -1, + Err(status) => status.code(), + } +} + +/// Runs the dynamic static-method call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_method_call`; callers must provide a valid +/// context, readable `ClassName::method` bytes, and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_static_method_call_inner( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(target) = abi_name_to_string(target_ptr, target_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((class_name, method)) = target.rsplit_once("::") else { + return EvalStatus::RuntimeFatal.code(); + }; + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_method_call_outcome( + context, class_name, method, args, &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs a late-static eval override call from a generated/AOT frame. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_method_call`; callers must +/// provide readable frame/method names and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_native_frame_static_method_call_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + if arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(method) = abi_name_to_string(method_ptr, method_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_method_call_outcome( + context, + &called_class, + &method, + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the dynamic method-call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_method_call`; callers must provide a valid context, +/// boxed object cell, readable method-name bytes, and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_method_call_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if object.is_null() || arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(method) = abi_name_to_string(method_ptr, method_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_method_call_outcome( + context, + RuntimeCellHandle::from_raw(object), + &method, + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/ffi/object_introspection.rs b/crates/elephc-magician/src/ffi/object_introspection.rs new file mode 100644 index 0000000000..02cb1e83ea --- /dev/null +++ b/crates/elephc-magician/src/ffi/object_introspection.rs @@ -0,0 +1,311 @@ +//! Purpose: +//! Exports post-barrier object introspection for values that may have been +//! created from eval-declared classes. Generated code uses this ABI when native +//! type metadata must consult the persistent eval context. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_object_class_name`. +//! - Generated EIR backend assembly through `__elephc_eval_object_is_a`. +//! - Generated EIR backend assembly through `__elephc_eval_object_is_a_dynamic`. +//! - Generated EIR backend assembly through `__elephc_eval_member_exists`. +//! - Generated EIR backend assembly through `__elephc_eval_class_relation`. +//! +//! Key details: +//! - Class-name and relation lookups return boxed Mixed cells through `ElephcEvalResult`. +//! - Named predicate probes fail closed as false; dynamic `instanceof` invalid +//! targets return -1 so generated code can raise PHP's fatal diagnostic. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter::{self, EvalOutcome}; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +const CLASS_LOOKUP_GET_CLASS: u64 = 0; +const CLASS_LOOKUP_GET_PARENT_CLASS: u64 = 1; +const MEMBER_LOOKUP_METHOD_EXISTS: u64 = 0; +const MEMBER_LOOKUP_PROPERTY_EXISTS: u64 = 1; +const CLASS_RELATION_IMPLEMENTS: u64 = 0; +const CLASS_RELATION_PARENTS: u64 = 1; +const CLASS_RELATION_USES: u64 = 2; + +/// Resolves `get_class()` or `get_parent_class()` against eval dynamic objects. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object_or_class` must point at a +/// boxed runtime cell, `lookup_kind` must be a supported lookup discriminator, +/// and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_class_name( + ctx: *mut ElephcEvalContext, + object_or_class: *mut RuntimeCell, + lookup_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_class_name_inner(ctx, object_or_class, lookup_kind, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Tests whether an object satisfies a class/interface relation in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` must point at a boxed +/// runtime cell. `target_ptr` must be readable for `target_len` bytes when +/// `target_len > 0`. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_is_a( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_is_a_inner(ctx, object, target_ptr, target_len, exclude_self) + }) + .unwrap_or(0) +} + +/// Tests whether an object satisfies a runtime string/object class target. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` and `target` must point +/// at boxed runtime cells. Returns -1 when the target is invalid for +/// `instanceof`. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_is_a_dynamic( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target: *mut RuntimeCell, + exclude_self: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_is_a_dynamic_inner(ctx, object, target, exclude_self) + }) + .unwrap_or(-1) +} + +/// Tests whether a method or property exists in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target` and `member` must point +/// at boxed runtime cells. `lookup_kind` must be a supported discriminator. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_member_exists( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + member: *mut RuntimeCell, + lookup_kind: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_member_exists_inner(ctx, target, member, lookup_kind) + }) + .unwrap_or(0) +} + +/// Resolves class/interface/trait relation metadata in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target` must point at a boxed +/// runtime cell. `relation_kind` must be a supported discriminator, and `out` +/// may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_class_relation( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + relation_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_class_relation_inner(ctx, target, relation_kind, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval object class-name ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_class_name`; callers must provide a valid +/// context, a boxed runtime cell, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_object_class_name_inner( + ctx: *mut ElephcEvalContext, + object_or_class: *mut RuntimeCell, + lookup_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if object_or_class.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let lookup = match lookup_kind { + CLASS_LOOKUP_GET_CLASS => "get_class", + CLASS_LOOKUP_GET_PARENT_CLASS => "get_parent_class", + _ => return EvalStatus::RuntimeFatal.code(), + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_class_name( + context, + lookup, + RuntimeCellHandle::from_raw(object_or_class), + &mut values, + ) { + Ok(result) => write_outcome(EvalOutcome::Value(result), out).code(), + Err(status) => status.code(), + } +} + +/// Runs the eval class-relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_class_relation`; callers must provide a valid context, +/// a boxed runtime cell target, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_class_relation_inner( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + relation_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if target.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let lookup = match relation_kind { + CLASS_RELATION_IMPLEMENTS => "class_implements", + CLASS_RELATION_PARENTS => "class_parents", + CLASS_RELATION_USES => "class_uses", + _ => return EvalStatus::RuntimeFatal.code(), + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_class_relation( + context, + lookup, + RuntimeCellHandle::from_raw(target), + &mut values, + ) { + Ok(result) => write_outcome(EvalOutcome::Value(result), out).code(), + Err(status) => status.code(), + } +} + +/// Runs the eval object relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_is_a`; invalid handles or unreadable target +/// storage fail closed as false. +#[cfg(not(test))] +unsafe fn eval_object_is_a_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || object.is_null() { + return 0; + } + let Ok(target) = abi_name_to_string(target_ptr, target_len) else { + return 0; + }; + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_is_a( + context, + RuntimeCellHandle::from_raw(object), + &target, + exclude_self != 0, + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => 0, + } +} + +/// Runs the eval dynamic object relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_is_a_dynamic`; invalid target cells report -1 +/// so generated code can use the normal dynamic-`instanceof` fatal path. +#[cfg(not(test))] +unsafe fn eval_object_is_a_dynamic_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target: *mut RuntimeCell, + exclude_self: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return -1; + }; + if context.abi_version() != ABI_VERSION || object.is_null() || target.is_null() { + return -1; + } + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_is_a_dynamic( + context, + RuntimeCellHandle::from_raw(object), + RuntimeCellHandle::from_raw(target), + exclude_self != 0, + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => -1, + } +} + +/// Runs the eval member-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_member_exists`; invalid handles fail closed as false. +#[cfg(not(test))] +unsafe fn eval_member_exists_inner( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + member: *mut RuntimeCell, + lookup_kind: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || target.is_null() || member.is_null() { + return 0; + } + let lookup = match lookup_kind { + MEMBER_LOOKUP_METHOD_EXISTS => "method_exists", + MEMBER_LOOKUP_PROPERTY_EXISTS => "property_exists", + _ => return 0, + }; + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_member_exists( + context, + lookup, + RuntimeCellHandle::from_raw(target), + RuntimeCellHandle::from_raw(member), + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => 0, + } +} diff --git a/crates/elephc-magician/src/ffi/scope.rs b/crates/elephc-magician/src/ffi/scope.rs new file mode 100644 index 0000000000..4938adfa95 --- /dev/null +++ b/crates/elephc-magician/src/ffi/scope.rs @@ -0,0 +1,163 @@ +//! Purpose: +//! Exports materialized eval activation-scope handle operations. +//! Scope entries carry runtime cell pointers plus dirty, ownership, unset, and +//! by-reference metadata used by generated code around eval barriers. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_scope_*` symbols. +//! +//! Key details: +//! - Owned runtime cells are released when a scope is freed or overwritten. +//! - Global aliases store source-target names rather than copying values. + +use super::util::{ + abi_name_to_string, release_owned_scope_cells, release_scope_cell, scope_entry_abi_flags, +}; +use crate::abi::{ElephcEvalScope, SCOPE_FLAG_OWNED}; +use crate::errors::EvalStatus; +use crate::scope::ScopeCellOwnership; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Allocates a materialized activation scope handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { + Box::into_raw(Box::new(ElephcEvalScope::new())) +} + +/// Frees a materialized activation scope handle allocated by the eval bridge. +/// +/// # Safety +/// `scope` must be null or a pointer returned by `__elephc_eval_scope_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_free(scope: *mut ElephcEvalScope) { + if !scope.is_null() { + let mut scope = Box::from_raw(scope); + release_owned_scope_cells(&mut scope); + drop(scope); + } +} + +/// Stores a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_set( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + cell: *mut RuntimeCell, + flags: u32, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let ownership = if flags & SCOPE_FLAG_OWNED != 0 { + ScopeCellOwnership::Owned + } else { + ScopeCellOwnership::Borrowed + }; + if let Some(replaced) = scope.set(name, RuntimeCellHandle::from_raw(cell), ownership) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Looks up a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. Output pointers may be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_get( + scope: *const ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + out_cell: *mut *mut RuntimeCell, + out_flags: *mut u32, +) -> i32 { + let Some(scope) = scope.as_ref() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let entry = scope.entry(&name); + if !out_cell.is_null() { + *out_cell = entry + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.cell().as_ptr()) + .unwrap_or(std::ptr::null_mut()); + } + if !out_flags.is_null() { + *out_flags = entry.map(scope_entry_abi_flags).unwrap_or(0); + } + EvalStatus::Ok.code() +} + +/// Marks a named runtime cell as unset in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_unset( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if let Some(replaced) = scope.unset(name) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Marks a local eval-scope variable as an alias of a program-global variable. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. Name pointers must be readable +/// for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_mark_global_alias( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + global_name_ptr: *const u8, + global_name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(global_name) = abi_name_to_string(global_name_ptr, global_name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_global_alias_to(name, global_name); + EvalStatus::Ok.code() +} + +/// Clears dirty flags for every entry in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle allocated by the eval bridge. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_clear_dirty(scope: *mut ElephcEvalScope) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_all_clean(); + EvalStatus::Ok.code() +} diff --git a/crates/elephc-magician/src/ffi/static_members.rs b/crates/elephc-magician/src/ffi/static_members.rs new file mode 100644 index 0000000000..6b2504deff --- /dev/null +++ b/crates/elephc-magician/src/ffi/static_members.rs @@ -0,0 +1,374 @@ +//! Purpose: +//! Exports post-barrier static member operations for classes declared by eval +//! fragments. Generated code uses this ABI when native scoped-constant or +//! static-property access targets dynamic eval metadata. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_class_constant_fetch`. +//! - Generated EIR backend assembly through `__elephc_eval_static_property_get`. +//! - Generated EIR backend assembly through `__elephc_eval_static_property_set`. +//! - Generated AOT frames through the native-frame late-static property hooks. +//! +//! Key details: +//! - Returned value cells are retained before crossing back to generated code. +//! - Errors and thrown PHP objects use the shared `ElephcEvalResult` contract. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::context::native_frame_called_class_override_context; +use crate::errors::EvalStatus; +use crate::interpreter::{self, EvalOutcome, RuntimeValueOps}; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Fetches a class-like constant through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class and constant name +/// byte ranges must be readable when their lengths are non-zero, and `out` may +/// be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_class_constant_fetch( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_class_constant_fetch_inner(ctx, class_ptr, class_len, constant_ptr, constant_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Reads a static property through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class and property name byte +/// ranges must be readable when their lengths are non-zero, and `out` may be +/// null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_property_get( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_property_get_inner(ctx, class_ptr, class_len, property_ptr, property_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Reads a static property through an active eval late-static native-frame override. +/// +/// # Safety +/// The frame class and property name byte ranges must be readable when their +/// lengths are non-zero, and `out` may be null. Returns `-1` when no matching +/// native-frame override is active. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_property_get( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_property_get_inner( + frame_class_ptr, + frame_class_len, + property_ptr, + property_len, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Writes a static property through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The target byte range must be a +/// readable `Class::property` name when non-empty, `value` must be a valid +/// runtime cell, and `out` may be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_property_set( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_property_set_inner(ctx, target_ptr, target_len, value, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Writes a static property through an active eval late-static native-frame override. +/// +/// # Safety +/// The frame class and property name byte ranges must be readable when their +/// lengths are non-zero, `value` must be a valid runtime cell, and `out` may be +/// null. Returns `-1` when no matching native-frame override is active. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_property_set( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_property_set_inner( + frame_class_ptr, + frame_class_len, + property_ptr, + property_len, + value, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the class-constant fetch ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_class_constant_fetch`; callers must provide a valid +/// context, readable class and constant name bytes, and optional result storage. +unsafe fn eval_class_constant_fetch_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(constant_name) = abi_name_to_string(constant_ptr, constant_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_class_constant_fetch( + context, + &class_name, + &constant_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the static-property get ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_property_get`; callers must provide a valid +/// context, readable class and property name bytes, and optional result storage. +unsafe fn eval_static_property_get_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_get( + context, + &class_name, + &property_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the native-frame static-property get ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_property_get`; callers must +/// provide readable frame/property name bytes and optional result storage. +unsafe fn eval_native_frame_static_property_get_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_get( + context, + &called_class, + &property_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the static-property set ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_property_set`; callers must provide a valid +/// context, readable `Class::property` target bytes, a valid value cell, and +/// optional result storage for thrown PHP objects. +unsafe fn eval_static_property_set_inner( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if value.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(target_name) = abi_name_to_string(target_ptr, target_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok((class_name, property_name)) = split_static_property_target(&target_name) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_set( + context, + class_name, + property_name, + RuntimeCellHandle::from_raw(value), + &mut values, + ) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => EvalStatus::Ok.code(), + Err(status) => status.code(), + } +} + +/// Runs the native-frame static-property set ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_property_set`; callers must +/// provide readable frame/property name bytes, a valid value cell, and optional +/// result storage for thrown PHP objects. +unsafe fn eval_native_frame_static_property_set_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + if value.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_set( + context, + &called_class, + &property_name, + RuntimeCellHandle::from_raw(value), + &mut values, + ) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => EvalStatus::Ok.code(), + Err(status) => status.code(), + } +} + +/// Splits the packed static-property target used by the compact setter ABI. +fn split_static_property_target(target: &str) -> Result<(&str, &str), EvalStatus> { + let Some((class_name, property_name)) = target.rsplit_once("::") else { + return Err(EvalStatus::RuntimeFatal); + }; + if class_name.is_empty() || property_name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((class_name, property_name)) +} + +/// Retains value outcomes so generated code receives an owned boxed cell. +fn retain_value_outcome( + outcome: EvalOutcome, + values: &mut impl RuntimeValueOps, +) -> Result { + match outcome { + EvalOutcome::Value(value) => values.retain(value).map(EvalOutcome::Value), + EvalOutcome::Throwable(error) => Ok(EvalOutcome::Throwable(error)), + } +} diff --git a/crates/elephc-magician/src/ffi/symbols.rs b/crates/elephc-magician/src/ffi/symbols.rs new file mode 100644 index 0000000000..d69ec780d4 --- /dev/null +++ b/crates/elephc-magician/src/ffi/symbols.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Exports dynamic symbol probes and constant fetching for eval-created state. +//! Generated code calls these after eval barriers to observe dynamic functions, +//! constants, and classes registered by interpreted fragments. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_*exists` symbols. +//! +//! Key details: +//! - Existence probes fail closed as `false` on invalid ABI inputs. +//! - Constant fetch retains the boxed cell before handing it back to generated code. + +use super::util::abi_name_to_string; +#[cfg(not(test))] +use super::util::clear_result; +#[cfg(not(test))] +use crate::abi::ElephcEvalResult; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::interpreter::RuntimeValueOps; +#[cfg(not(test))] +use crate::runtime_hooks::ElephcRuntimeOps; + +/// Checks whether a function was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_function_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_function_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + +/// Checks whether a constant was previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_constant_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + +/// Checks whether a class was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_dynamic_class_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + +/// Fetches a constant previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_fetch( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_constant_fetch_inner(ctx, name_ptr, name_len, out) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval function-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_function_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_function_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_function(&name.to_ascii_lowercase())) +} + +/// Runs the eval constant-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_constant_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_constant(&name)) +} + +/// Runs the eval dynamic-class-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_dynamic_class_exists`; invalid handles or unreadable +/// name storage fail closed as `false`. +unsafe fn eval_dynamic_class_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_class(&name)) +} + +/// Runs the eval constant-fetch ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_fetch`; callers must provide a valid context, +/// readable constant-name bytes, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_constant_fetch_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let Some(value) = context.constant(&name) else { + return EvalStatus::RuntimeFatal.code(); + }; + if out.is_null() { + return EvalStatus::Ok.code(); + } + let mut values = ElephcRuntimeOps::new(); + match values.retain(value) { + Ok(result) => { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/ffi/tests/callable_defaults.rs b/crates/elephc-magician/src/ffi/tests/callable_defaults.rs new file mode 100644 index 0000000000..e83f4b94c8 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/callable_defaults.rs @@ -0,0 +1,228 @@ +//! Purpose: +//! Tests decoding nested object and array defaults from generated callable ABI +//! records. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Nested, named, and maximum-count records are covered with bounded encodings. + +use super::*; + +/// Verifies native AOT object defaults can carry nested object constructor args. +#[test] +fn register_native_object_default_decodes_nested_objects() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let nested_args = vec![NativeCallableObjectDefaultArg::positional( + NativeCallableDefault::Object { + class_name: "InnerDefault".to_string(), + args: vec![NativeCallableObjectDefaultArg::positional( + NativeCallableDefault::String("leaf".to_string()), + )], + }, + )]; + let expected_default = NativeCallableDefault::Object { + class_name: "OuterDefault".to_string(), + args: nested_args.clone(), + }; + let spec = native_object_default_record("OuterDefault", &nested_args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + +/// Verifies native AOT object defaults can carry named constructor args. +#[test] +fn register_native_object_default_decodes_named_args() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let args = vec![ + NativeCallableObjectDefaultArg::named( + "right", + NativeCallableDefault::String("R".to_string()), + ), + NativeCallableObjectDefaultArg::named( + "left", + NativeCallableDefault::String("L".to_string()), + ), + ]; + let expected_default = NativeCallableDefault::Object { + class_name: "NamedDefault".to_string(), + args: args.clone(), + }; + let spec = native_object_default_record("NamedDefault", &args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + +/// Verifies native AOT object defaults decode the full u8 constructor argument range. +#[test] +fn register_native_object_default_decodes_full_u8_arg_count() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let args = (0..TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS) + .map(|index| { + let value = NativeCallableDefault::String(format!("arg{}", index)); + NativeCallableObjectDefaultArg::positional(value) + }) + .collect::>(); + let expected_default = NativeCallableDefault::Object { + class_name: "LargeDefault".to_string(), + args: args.clone(), + }; + let spec = native_object_default_record("LargeDefault", &args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + +/// Verifies native AOT array defaults can carry keyed and nested default values. +#[test] +fn register_native_array_default_decodes_nested_values() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::items"; + let class = b"KnownClass"; + let property = b"KnownClass::items"; + let elements = vec![ + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::String("left".to_string()), + NativeCallableDefault::String("L".to_string()), + ), + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::Int(2), + NativeCallableDefault::Array(vec![NativeCallableArrayDefaultElement::positional( + NativeCallableDefault::Int(7), + )]), + ), + NativeCallableArrayDefaultElement::positional(NativeCallableDefault::Object { + class_name: "ArrayDefaultDep".to_string(), + args: vec![NativeCallableObjectDefaultArg::named( + "label", + NativeCallableDefault::String("dep".to_string()), + )], + }), + ]; + let expected_default = NativeCallableDefault::Array(elements.clone()); + let spec = native_array_default_record(&elements); + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 1) + }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_array( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_array( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let property_default_registered = unsafe { + __elephc_eval_register_native_property_default_array( + &mut ctx, + property.as_ptr(), + property.len() as u64, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_default_registered, 1); + assert_eq!(property_default_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "ITEMS") + .expect("method metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_property_default("KnownClass", "items"), + Some(expected_default) + ); +} diff --git a/crates/elephc-magician/src/ffi/tests/class_metadata.rs b/crates/elephc-magician/src/ffi/tests/class_metadata.rs new file mode 100644 index 0000000000..f4e751457e --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/class_metadata.rs @@ -0,0 +1,346 @@ +//! Purpose: +//! Tests native parent, property, interface, abstract-property, default, and +//! attribute metadata registration. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Every metadata family is read back through `ElephcEvalContext`. + +use super::*; + +/// Verifies native AOT parent metadata is available for eval static-scope resolution. +#[test] +fn register_native_class_parent_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownChild"; + let parent = b"KnownParent"; + + let registered = unsafe { + __elephc_eval_register_native_class_parent( + &mut ctx, + class.as_ptr(), + class.len() as u64, + parent.as_ptr(), + parent.len() as u64, + ) + }; + + assert_eq!(registered, 1); + assert_eq!(ctx.native_class_parent("knownchild"), Some("KnownParent")); +} + +/// Verifies native AOT property type metadata is available to eval reflection. +#[test] +fn register_native_property_type_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::name"; + let property_type = b"?KnownDep"; + let invalid_property = b"KnownClass::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + ) + }; + + assert_eq!(registered, 1); + let property_type = ctx + .native_property_type("knownclass", "name") + .expect("property type metadata"); + assert!(property_type.allows_null()); + assert_eq!( + property_type.variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); + assert_eq!(invalid_registered, 0); + assert!(ctx.native_property_type("KnownClass", "bad").is_none()); +} + +/// Verifies native AOT interface property contracts are available to eval validation. +#[test] +fn register_native_interface_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownContract::KnownParentContract::name"; + let property_type = b"string"; + let invalid_property = b"KnownContract::KnownParentContract::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_interface_property_requirements("knowncontract"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownParentContract"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_interface_property_requirements("KnownContract") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + +/// Verifies native AOT abstract class property contracts are available to eval validation. +#[test] +fn register_native_abstract_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::KnownParent::name"; + let property_type = b"string"; + let invalid_property = b"KnownClass::KnownParent::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_abstract_property_requirements("knownclass"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownParent"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_abstract_property_requirements("KnownClass") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + +/// Verifies native AOT property default metadata is available to eval reflection. +#[test] +fn register_native_property_default_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let count = b"KnownClass::count"; + let label = b"KnownClass::label"; + let invalid = b"KnownClass::invalid"; + let label_value = b"ok"; + + let scalar_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + count.as_ptr(), + count.len() as u64, + 2, + 42, + ) + }; + let string_registered = unsafe { + __elephc_eval_register_native_property_default_string( + &mut ctx, + label.as_ptr(), + label.len() as u64, + label_value.as_ptr(), + label_value.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + invalid.as_ptr(), + invalid.len() as u64, + 99, + 0, + ) + }; + + assert_eq!(scalar_registered, 1); + assert_eq!( + ctx.native_property_default("knownclass", "count"), + Some(NativeCallableDefault::Int(42)) + ); + assert_eq!(string_registered, 1); + assert_eq!( + ctx.native_property_default("KnownClass", "label"), + Some(NativeCallableDefault::String("ok".to_string())) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_property_default("KnownClass", "invalid") + .is_none()); +} + +/// Verifies native AOT member attributes are available to eval reflection. +#[test] +fn register_native_member_attribute_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method_record = native_member_attribute_record( + 0, + "KnownClass::run", + "Route", + Some(&[ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, + EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ]), + ); + let property_record = native_member_attribute_record(1, "KnownClass::id", "Column", None); + let constant_record = native_member_attribute_record( + 2, + "KnownClass::LIMIT", + "Limit", + Some(&[EvalAttributeArg::Int(100)]), + ); + let class_record = native_member_attribute_record( + 3, + "KnownClass", + "Entity", + Some(&[EvalAttributeArg::String("model".to_string())]), + ); + let invalid_record = [99, 0, 0, 0, 0]; + + let method_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + method_record.as_ptr(), + method_record.len() as u64, + ) + }; + let property_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + property_record.as_ptr(), + property_record.len() as u64, + ) + }; + let constant_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + constant_record.as_ptr(), + constant_record.len() as u64, + ) + }; + let class_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + class_record.as_ptr(), + class_record.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + invalid_record.as_ptr(), + invalid_record.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + let method_attributes = ctx.native_method_attributes("knownclass", "RUN"); + assert_eq!(method_attributes.len(), 1); + assert_eq!(method_attributes[0].name(), "Route"); + assert_eq!( + method_attributes[0].args(), + Some( + [ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, + EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ] + .as_slice() + ) + ); + assert_eq!(property_registered, 1); + let property_attributes = ctx.native_property_attributes("KnownClass", "id"); + assert_eq!(property_attributes.len(), 1); + assert_eq!(property_attributes[0].name(), "Column"); + assert!(property_attributes[0].args().is_none()); + assert_eq!(constant_registered, 1); + let constant_attributes = ctx.native_constant_attributes("KnownClass", "LIMIT"); + assert_eq!(constant_attributes.len(), 1); + assert_eq!(constant_attributes[0].name(), "Limit"); + assert_eq!( + constant_attributes[0].args(), + Some([EvalAttributeArg::Int(100)].as_slice()) + ); + assert_eq!(class_registered, 1); + let class_attributes = ctx.native_class_attributes("knownclass"); + assert_eq!(class_attributes.len(), 1); + assert_eq!(class_attributes[0].name(), "Entity"); + assert_eq!( + class_attributes[0].args(), + Some([EvalAttributeArg::String("model".to_string())].as_slice()) + ); + assert_eq!(invalid_registered, 0); +} diff --git a/crates/elephc-magician/src/ffi/tests/context_symbols.rs b/crates/elephc-magician/src/ffi/tests/context_symbols.rs new file mode 100644 index 0000000000..28c626eb88 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/context_symbols.rs @@ -0,0 +1,312 @@ +//! Purpose: +//! Tests eval ABI versioning, context state, call-site scopes, and declared +//! symbol visibility. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Context and symbol registration are exercised without generated assembly. + +use super::*; + +/// Verifies the exported version entry point reports the crate ABI constant. +#[test] +fn abi_version_matches_constant() { + assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); +} + +/// Verifies the initial execute stub clears result storage and returns the +/// documented unsupported status instead of panicking or succeeding. +#[test] +fn execute_stub_returns_unsupported_and_clears_result() { + let mut result = ElephcEvalResult { + kind: 99, + value_cell: 1usize as *mut std::ffi::c_void, + error: 2usize as *mut std::ffi::c_void, + }; + let status = unsafe { + __elephc_eval_execute( + std::ptr::null_mut(), + std::ptr::null_mut(), + b"$x = 1;".as_ptr(), + 7, + &mut result, + ) + }; + assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); + assert_eq!(result.kind, 0); + assert!(result.value_cell.is_null()); + assert!(result.error.is_null()); +} + +/// Verifies context allocation returns a current-version opaque handle. +#[test] +fn context_new_returns_current_version_handle() { + let ctx = __elephc_eval_context_new(); + assert!(!ctx.is_null()); + let version = unsafe { (*ctx).abi_version() }; + unsafe { + __elephc_eval_context_free(ctx); + } + assert_eq!(version, ABI_VERSION); +} + +/// Verifies call-site metadata can be set through the stable context ABI. +#[test] +fn context_set_call_site_records_file_dir_and_line() { + let mut ctx = ElephcEvalContext::new(); + let file = b"/tmp/source.php"; + let dir = b"/tmp"; + + let status = unsafe { + __elephc_eval_context_set_call_site( + &mut ctx, + file.as_ptr(), + file.len() as u64, + dir.as_ptr(), + dir.len() as u64, + 9, + ) + }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(ctx.call_dir(), "/tmp"); + assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); +} + +/// Verifies the context ABI records a non-owned global scope handle. +#[test] +fn context_set_global_scope_records_handle() { + let mut ctx = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + + let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!( + ctx.global_scope_ptr(), + Some(&mut scope as *mut ElephcEvalScope) + ); +} + +/// Verifies generated class scopes are pushed and popped through the context ABI. +#[test] +fn context_push_class_scope_records_self_and_called_class() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"AotBase"; + let called_class_name = b"AotChild"; + + let push_status = unsafe { + __elephc_eval_context_push_class_scope( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + called_class_name.as_ptr(), + called_class_name.len() as u64, + ) + }; + + assert_eq!(push_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), Some("AotBase")); + assert_eq!(ctx.current_called_class_scope(), Some("AotChild")); + + let pop_status = unsafe { __elephc_eval_context_pop_class_scope(&mut ctx) }; + + assert_eq!(pop_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), None); + assert_eq!(ctx.current_called_class_scope(), None); +} + +/// Verifies generated frames can query eval late-static overrides without a context handle. +#[test] +fn native_frame_called_class_override_reports_thread_local_scope() { + let class_name = b"AotBase"; + let mut out_ptr = std::ptr::null(); + let mut out_len = 0; + + let missing = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(missing, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); + + { + let _guard = push_native_frame_called_class_override( + std::ptr::null_mut(), + "AotBase", + "EvalChild", + ); + + let found = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(found, 1); + let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) }; + assert_eq!(bytes, b"EvalChild"); + } + + let after_drop = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(after_drop, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); +} + +/// Verifies generated declaration-name metadata is exposed through eval lists. +#[test] +fn register_declared_symbol_names_records_visible_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"\\AotDeclaredClass"; + let class_duplicate = b"aotdeclaredclass"; + let interface_name = b"AotDeclaredInterface"; + let trait_name = b"AotDeclaredTrait"; + let empty_name = b""; + + let class_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + ) + }; + let duplicate_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_duplicate.as_ptr(), + class_duplicate.len() as u64, + ) + }; + let interface_registered = unsafe { + __elephc_eval_register_declared_interface_name( + &mut ctx, + interface_name.as_ptr(), + interface_name.len() as u64, + ) + }; + let trait_registered = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + trait_name.as_ptr(), + trait_name.len() as u64, + ) + }; + let empty_rejected = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + empty_name.as_ptr(), + empty_name.len() as u64, + ) + }; + + assert_eq!(class_registered, 1); + assert_eq!(duplicate_registered, 1); + assert_eq!(interface_registered, 1); + assert_eq!(trait_registered, 1); + assert_eq!(empty_rejected, 0); + assert_eq!(ctx.declared_class_names(), &["AotDeclaredClass".to_string()]); + assert_eq!( + ctx.declared_interface_names(), + &["AotDeclaredInterface".to_string()] + ); + assert_eq!(ctx.declared_trait_names(), &["AotDeclaredTrait".to_string()]); +} + +/// Verifies the function-exists ABI probes eval-declared functions by folded name. +#[test] +fn function_exists_reports_declared_eval_function() { + let mut ctx = ElephcEvalContext::new(); + ctx.define_function( + "dyn_probe", + crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), + ) + .expect("first dynamic function declaration should succeed"); + let existing = b"DYN_PROBE"; + let missing = b"missing"; + + let existing_result = + unsafe { __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; + let missing_result = + unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(missing_result, 0); +} + +/// Verifies the constant-exists ABI probes eval-defined constants by PHP name. +#[test] +fn constant_exists_reports_defined_eval_constant() { + let mut ctx = ElephcEvalContext::new(); + let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); + assert!(ctx.define_constant("DynConstProbe", value)); + let existing = b"DynConstProbe"; + let qualified = b"\\DynConstProbe"; + let wrong_case = b"dynconstprobe"; + let missing = b"missing"; + + let existing_result = + unsafe { __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; + let qualified_result = + unsafe { __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) }; + let wrong_case_result = unsafe { + __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) + }; + let missing_result = + unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(wrong_case_result, 0); + assert_eq!(missing_result, 0); +} + +/// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. +#[test] +fn dynamic_class_exists_reports_declared_eval_class() { + let mut ctx = ElephcEvalContext::new(); + assert!(ctx.define_class(crate::eval_ir::EvalClass::new( + "DynClassProbe", + Vec::new(), + Vec::new() + ))); + let existing = b"DynClassProbe"; + let qualified = b"\\DynClassProbe"; + let folded = b"dynclassprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let folded_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) }; + let missing_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(folded_result, 1); + assert_eq!(missing_result, 0); +} diff --git a/crates/elephc-magician/src/ffi/tests/mod.rs b/crates/elephc-magician/src/ffi/tests/mod.rs new file mode 100644 index 0000000000..13aee70dbe --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/mod.rs @@ -0,0 +1,223 @@ +//! Purpose: +//! Unit tests for the eval C ABI layer. +//! They validate handle allocation, stable status codes, scope flags, and +//! dynamic symbol registration without requiring generated runtime assembly. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Execute remains a controlled unsupported stub in crate unit tests. +//! - Fake runtime-cell pointers are never dereferenced by these tests. + +use super::context::*; +use super::declared_symbols::*; +use super::execute::*; +use super::native_functions::*; +use super::native_methods::*; +use super::scope::*; +use super::symbols::*; +use crate::abi::{ + ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, + SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, +}; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, + NativeCallableObjectDefaultArg, push_native_frame_called_class_override, +}; +use crate::errors::EvalStatus; +use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::ffi::c_void; + +mod callable_defaults; +mod class_metadata; +mod context_symbols; +mod native_functions; +mod native_methods; +mod scope_execution; + +const TEST_NATIVE_DEFAULT_NULL: u64 = 0; +const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; +const TEST_NATIVE_DEFAULT_INT: u64 = 2; +const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; +const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; +const TEST_NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; +const TEST_NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; +const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; + +/// Test native invoker placeholder used only to validate ABI registration. +unsafe extern "C" fn fake_native_invoker( + _descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + std::ptr::null_mut() +} + +/// Builds one native member-attribute ABI record for registration tests. +fn native_member_attribute_record( + owner_kind: u8, + member_key: &str, + attribute_name: &str, + args: Option<&[EvalAttributeArg]>, +) -> Vec { + let mut record = Vec::new(); + record.push(owner_kind); + native_member_attribute_push_string(&mut record, member_key); + native_member_attribute_push_string(&mut record, attribute_name); + match args { + Some(args) => { + record.push(1); + record.extend_from_slice(&(args.len() as u32).to_le_bytes()); + for arg in args { + native_member_attribute_push_arg(&mut record, arg); + } + } + None => record.push(0), + } + record +} + +/// Appends one test attribute argument to a native member-attribute ABI record. +fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg) { + match arg { + EvalAttributeArg::Null => record.push(0), + EvalAttributeArg::Bool(value) => { + record.push(1); + record.push(u8::from(*value)); + } + EvalAttributeArg::Int(value) => { + record.push(2); + record.extend_from_slice(&value.to_le_bytes()); + } + EvalAttributeArg::Float(bits) => { + record.push(5); + record.extend_from_slice(&bits.to_le_bytes()); + } + EvalAttributeArg::String(value) => { + record.push(3); + native_member_attribute_push_string(record, value); + } + EvalAttributeArg::Named { name, value } => { + record.push(4); + native_member_attribute_push_string(record, name); + native_member_attribute_push_arg(record, value); + } + EvalAttributeArg::IntKeyed { .. } => { + panic!("native attribute test ABI does not encode int-keyed array arguments") + } + EvalAttributeArg::Array(elements) => { + record.push(6); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_member_attribute_push_arg(record, element); + } + } + } +} + +/// Appends one length-prefixed string to a native member-attribute ABI record. +fn native_member_attribute_push_string(record: &mut Vec, value: &str) { + record.extend_from_slice(&(value.len() as u32).to_le_bytes()); + record.extend_from_slice(value.as_bytes()); +} + +/// Builds one object-valued native parameter default ABI record for registration tests. +fn native_object_default_record( + class_name: &str, + args: &[NativeCallableObjectDefaultArg], +) -> Vec { + let mut record = Vec::new(); + native_member_attribute_push_string(&mut record, class_name); + record.push(args.len() as u8); + for arg in args { + native_object_default_push_arg(&mut record, arg); + } + record +} + +/// Builds one array-valued native parameter default ABI record for registration tests. +fn native_array_default_record(elements: &[NativeCallableArrayDefaultElement]) -> Vec { + let mut record = Vec::new(); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_array_default_push_element(&mut record, element); + } + record +} + +/// Appends one array-default element and optional static key to a default record. +fn native_array_default_push_element( + record: &mut Vec, + element: &NativeCallableArrayDefaultElement, +) { + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_INT); + record.extend_from_slice(&value.to_le_bytes()); + } + Some(NativeCallableArrayDefaultKey::String(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING); + native_member_attribute_push_string(record, value); + } + None => record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO), + } + native_object_default_push_arg_value(record, &element.value); +} + +/// Appends one object-default constructor argument to a native parameter default record. +fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableObjectDefaultArg) { + if let Some(name) = &arg.name { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED); + native_member_attribute_push_string(record, name); + } + native_object_default_push_arg_value(record, &arg.value); +} + +/// Appends one object-default constructor argument value to a native parameter default record. +fn native_object_default_push_arg_value(record: &mut Vec, value: &NativeCallableDefault) { + match value { + NativeCallableDefault::Null => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_NULL, 0) + } + NativeCallableDefault::Bool(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_BOOL, u64::from(*value)) + } + NativeCallableDefault::Int(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_INT, *value as u64) + } + NativeCallableDefault::Float(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_FLOAT, value.to_bits()) + } + NativeCallableDefault::String(value) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING); + native_member_attribute_push_string(record, value); + } + NativeCallableDefault::EmptyArray => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_EMPTY_ARRAY, 0) + } + NativeCallableDefault::Object { class_name, args } => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT); + record.extend_from_slice(&native_object_default_record(class_name, args)); + } + NativeCallableDefault::Array(elements) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY); + record.extend_from_slice(&native_array_default_record(elements)); + } + } +} + +/// Appends one scalar object-default constructor argument to a native parameter default record. +fn native_object_default_push_scalar(record: &mut Vec, kind: u64, payload: u64) { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR); + record.extend_from_slice(&kind.to_le_bytes()); + record.extend_from_slice(&payload.to_le_bytes()); +} diff --git a/crates/elephc-magician/src/ffi/tests/native_functions.rs b/crates/elephc-magician/src/ffi/tests/native_functions.rs new file mode 100644 index 0000000000..4f807d9d75 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/native_functions.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! Tests native function signature, parameter, type, and default registration +//! through the eval C ABI. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Registration metadata is read back from the same eval context. + +use super::*; + +/// Verifies native AOT registration records function parameter metadata and defaults. +#[test] +fn register_native_function_reports_function_exists() { + let mut ctx = ElephcEvalContext::new(); + let name = b"NATIVE_PROBE"; + let param = b"value"; + let variadic = b"items"; + let param_type = b"?string"; + let return_type = b"int"; + let descriptor = 1usize as *mut c_void; + + let registered = unsafe { + __elephc_eval_register_native_function( + &mut ctx, + name.as_ptr(), + name.len() as u64, + descriptor, + Some(fake_native_invoker), + 2, + ) + }; + let param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param.as_ptr(), + param.len() as u64, + ) + }; + let variadic_param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + variadic.as_ptr(), + variadic.len() as u64, + ) + }; + let bridge_support_registered = unsafe { + __elephc_eval_register_native_function_bridge_support( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + ) + }; + let param_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + 1, + 0, + ) + }; + let variadic_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + 0, + 1, + ) + }; + let param_type_registered = unsafe { + __elephc_eval_register_native_function_param_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param_type.as_ptr(), + param_type.len() as u64, + ) + }; + let return_type_registered = unsafe { + __elephc_eval_register_native_function_return_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; + let param_default_registered = unsafe { + __elephc_eval_register_native_function_param_default_scalar( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + TEST_NATIVE_DEFAULT_INT, + 42, + ) + }; + let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; + + assert_eq!(registered, 1); + let native = ctx + .native_function("native_probe") + .expect("native function should be registered"); + + assert_eq!(param_registered, 1); + assert_eq!(variadic_param_registered, 1); + assert_eq!(bridge_support_registered, 1); + assert_eq!(param_flags_registered, 1); + assert_eq!(variadic_flags_registered, 1); + assert_eq!(param_type_registered, 1); + assert_eq!(return_type_registered, 1); + assert_eq!(param_default_registered, 1); + assert_eq!(exists, 1); + assert_eq!( + native.param_names(), + &["value".to_string(), "items".to_string()] + ); + let native_type = native.param_type(0).expect("native parameter type"); + assert!(native_type.allows_null()); + assert_eq!(native_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(native.param_by_ref(0)); + assert!(!native.param_variadic(0)); + assert!(native.param_variadic(1)); + assert!(!native.bridge_supported()); + assert_eq!(native.required_param_count(), 0); + assert_eq!( + native.return_type().expect("native return type").variants(), + &[EvalParameterTypeVariant::Int] + ); + assert_eq!(native.param_default(0), Some(&NativeCallableDefault::Int(42))); +} diff --git a/crates/elephc-magician/src/ffi/tests/native_methods.rs b/crates/elephc-magician/src/ffi/tests/native_methods.rs new file mode 100644 index 0000000000..7ca141d9ac --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/native_methods.rs @@ -0,0 +1,296 @@ +//! Purpose: +//! Tests native instance, static, constructor, interface, and abstract member +//! signature registration through the eval C ABI. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parameters, flags, types, defaults, and bridge support are checked together. + +use super::*; + +/// Verifies native AOT method registration records instance/static/constructor parameters. +#[test] +fn register_native_methods_record_signature_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::join"; + let static_method = b"KnownClass::sum"; + let class = b"KnownClass"; + let left = b"left"; + let right = b"right"; + let value = b"value"; + let method_type = b"int|string|null"; + let static_type = b"?string"; + let constructor_type = b"KnownDep"; + let return_type = b"bool"; + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 2) + }; + let method_param_registered = unsafe { + __elephc_eval_register_native_method_param( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let method_param_type_registered = unsafe { + __elephc_eval_register_native_method_param_type( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + method_type.as_ptr(), + method_type.len() as u64, + ) + }; + let method_param_flags_registered = unsafe { + __elephc_eval_register_native_method_param_flags( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + 1, + 0, + ) + }; + let static_registered = unsafe { + __elephc_eval_register_native_static_method( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 2, + ) + }; + let static_param_registered = unsafe { + __elephc_eval_register_native_static_method_param( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + left.as_ptr(), + left.len() as u64, + ) + }; + let static_param_type_registered = unsafe { + __elephc_eval_register_native_static_method_param_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + static_type.as_ptr(), + static_type.len() as u64, + ) + }; + let static_param_flags_registered = unsafe { + __elephc_eval_register_native_static_method_param_flags( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + 0, + 1, + ) + }; + let static_return_type_registered = unsafe { + __elephc_eval_register_native_static_method_return_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_param_registered = unsafe { + __elephc_eval_register_native_constructor_param( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + value.as_ptr(), + value.len() as u64, + ) + }; + let constructor_param_type_registered = unsafe { + __elephc_eval_register_native_constructor_param_type( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + constructor_type.as_ptr(), + constructor_type.len() as u64, + ) + }; + let constructor_param_flags_registered = unsafe { + __elephc_eval_register_native_constructor_param_flags( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; + let constructor_bridge_support_registered = unsafe { + __elephc_eval_register_native_constructor_bridge_support( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + ) + }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_string( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let static_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + 2, + 42, + ) + }; + let static_empty_array_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + NATIVE_DEFAULT_EMPTY_ARRAY, + 0, + ) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_scalar( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_param_registered, 1); + assert_eq!(method_param_type_registered, 1); + assert_eq!(method_param_flags_registered, 1); + assert_eq!(static_registered, 1); + assert_eq!(static_param_registered, 1); + assert_eq!(static_param_type_registered, 1); + assert_eq!(static_param_flags_registered, 1); + assert_eq!(static_return_type_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_param_registered, 1); + assert_eq!(constructor_param_type_registered, 1); + assert_eq!(constructor_param_flags_registered, 1); + assert_eq!(constructor_bridge_support_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(static_default_registered, 1); + assert_eq!(static_empty_array_default_registered, 1); + assert_eq!(constructor_default_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_names(), + &["".to_string(), "right".to_string()] + ); + let method_signature = ctx + .native_method_signature("knownclass", "JOIN") + .expect("method metadata"); + let method_type = method_signature + .param_type(0) + .expect("method parameter type"); + assert!(method_type.allows_null()); + assert_eq!( + method_type.variants(), + &[ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ] + ); + assert!(method_signature.param_by_ref(1)); + assert!(!method_signature.param_variadic(1)); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_names(), + &["left".to_string(), "".to_string()] + ); + let static_signature = ctx + .native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata"); + let static_type = static_signature + .param_type(0) + .expect("static method parameter type"); + assert!(static_type.allows_null()); + assert_eq!(static_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(!static_signature.param_by_ref(1)); + assert!(static_signature.param_variadic(1)); + assert_eq!( + static_signature + .return_type() + .expect("static return type") + .variants(), + &[EvalParameterTypeVariant::Bool] + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_names(), + &["value".to_string()] + ); + let constructor_signature = ctx + .native_constructor_signature("knownclass") + .expect("constructor metadata"); + assert_eq!( + constructor_signature + .param_type(0) + .expect("constructor parameter type") + .variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); + assert!(constructor_signature.param_by_ref(0)); + assert!(constructor_signature.param_variadic(0)); + assert!(!constructor_signature.bridge_supported()); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_default(1), + Some(&NativeCallableDefault::String("right".to_string())) + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(0), + Some(&NativeCallableDefault::Int(42)) + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(1), + Some(&NativeCallableDefault::EmptyArray) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&NativeCallableDefault::Bool(true)) + ); +} diff --git a/crates/elephc-magician/src/ffi/tests/scope_execution.rs b/crates/elephc-magician/src/ffi/tests/scope_execution.rs new file mode 100644 index 0000000000..02eecdc1d5 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/scope_execution.rs @@ -0,0 +1,175 @@ +//! Purpose: +//! Tests eval scope allocation, execute validation, scope cell flags, aliases, +//! unset, and dirty-state handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Fake runtime-cell pointers are stored and compared but never dereferenced. + +use super::*; + +/// Verifies scope allocation returns an empty opaque activation scope handle. +#[test] +fn scope_new_returns_empty_handle() { + let scope = __elephc_eval_scope_new(); + assert!(!scope.is_null()); + let generation = unsafe { (*scope).generation() }; + unsafe { + __elephc_eval_scope_free(scope); + } + assert_eq!(generation, 0); +} + +/// Verifies execute rejects contexts whose ABI version no longer matches. +#[test] +fn execute_rejects_mismatched_context_version() { + let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); + let status = unsafe { + __elephc_eval_execute( + &mut ctx, + std::ptr::null_mut(), + std::ptr::null(), + 0, + std::ptr::null_mut(), + ) + }; + + assert_eq!(status, EvalStatus::AbiMismatch.code()); +} + +/// Verifies execute maps invalid eval fragments to the stable parse status. +#[test] +fn execute_rejects_php_opening_tags_as_parse_errors() { + let code = b" Result { + if name_len > 0 && name_ptr.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + let name_len = usize::try_from(name_len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if name_len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(name_ptr, name_len) } + }; + std::str::from_utf8(bytes) + .map(|name| name.to_string()) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts internal scope-entry flags into stable ABI bit flags. +pub(crate) fn scope_entry_abi_flags(entry: ScopeEntry) -> u32 { + let flags = entry.flags(); + let mut abi_flags = 0; + if flags.present { + abi_flags |= SCOPE_FLAG_PRESENT; + } + if flags.unset { + abi_flags |= SCOPE_FLAG_UNSET; + } + if flags.dirty { + abi_flags |= SCOPE_FLAG_DIRTY; + } + if flags.by_ref { + abi_flags |= SCOPE_FLAG_BY_REF; + } + if flags.ownership == ScopeCellOwnership::Owned { + abi_flags |= SCOPE_FLAG_OWNED; + } + abi_flags +} + +/// Releases every owned cell currently held by a scope. +pub(crate) fn release_owned_scope_cells(scope: &mut ElephcEvalScope) { + for cell in scope.drain_owned_cells() { + release_scope_cell(cell); + } +} + +/// Releases one scope-owned runtime cell through the generated runtime wrapper. +pub(crate) fn release_scope_cell(cell: RuntimeCellHandle) { + #[cfg(not(test))] + unsafe { + __elephc_eval_value_release(cell.as_ptr()); + } + #[cfg(test)] + { + let _ = cell; + } +} + +/// Clears optional result storage before an ABI call writes a result. +pub(crate) unsafe fn clear_result(out: *mut ElephcEvalResult) { + if !out.is_null() { + (*out).clear(); + } +} + +/// Writes an eval execution outcome into optional ABI result storage. +#[cfg(not(test))] +pub(crate) unsafe fn write_outcome(outcome: EvalOutcome, out: *mut ElephcEvalResult) -> EvalStatus { + match outcome { + EvalOutcome::Value(result) => { + if !out.is_null() { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + } + EvalStatus::Ok + } + EvalOutcome::Throwable(error) => { + if !out.is_null() { + (*out).kind = 3; + (*out).value_cell = std::ptr::null_mut(); + (*out).error = error.as_ptr(); + } + EvalStatus::UncaughtThrowable + } + } +} diff --git a/crates/elephc-magician/src/interpreter/array_literals.rs b/crates/elephc-magician/src/interpreter/array_literals.rs new file mode 100644 index 0000000000..8674032516 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/array_literals.rs @@ -0,0 +1,224 @@ +//! Purpose: +//! Builds EvalIR array literals and computes PHP-compatible next keys for mixed array construction. +//! +//! Called from: +//! - `crate::interpreter::eval_expr()` for indexed and associative array literal nodes. +//! +//! Key details: +//! - Explicit keys are normalized through runtime string conversion to match PHP array-key rules. +//! - Unkeyed elements continue from the next PHP integer key after explicit keys. + +use super::*; + +/// Evaluates an indexed array literal into a boxed runtime Mixed array. +pub(super) fn eval_indexed_array( + elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = values.array_new(elements.len())?; + for (index, element) in elements.iter().enumerate() { + let index = values.int(index as i64)?; + let (value, target) = match element { + EvalArrayElement::Value(element) => (eval_expr(element, context, scope, values)?, None), + EvalArrayElement::Reference(element) => { + let (value, target) = + eval_reference_array_element_value(element, context, scope, values)?; + (value, Some(target)) + } + EvalArrayElement::KeyValue { .. } | EvalArrayElement::KeyReference { .. } => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + array = values.array_set(array, index, value)?; + if let Some(target) = target { + bind_array_element_reference(context, array, index, target, values)?; + } + } + Ok(array) +} + +/// Evaluates an associative array literal into a boxed runtime Mixed hash. +pub(super) fn eval_assoc_array( + elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = values.assoc_new(elements.len())?; + let mut next_key = None; + for element in elements { + let (key, value, target) = match element { + EvalArrayElement::Value(value) => { + let key = match next_key { + Some(next_key) => next_key, + None => values.int(0)?, + }; + let one = values.int(1)?; + next_key = Some(values.add(key, one)?); + let value = eval_expr(value, context, scope, values)?; + (key, value, None) + } + EvalArrayElement::Reference(value) => { + let key = match next_key { + Some(next_key) => next_key, + None => values.int(0)?, + }; + let one = values.int(1)?; + next_key = Some(values.add(key, one)?); + let (value, target) = + eval_reference_array_element_value(value, context, scope, values)?; + (key, value, Some(target)) + } + EvalArrayElement::KeyValue { key, value } => { + let key = eval_expr(key, context, scope, values)?; + next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; + let value = eval_expr(value, context, scope, values)?; + (key, value, None) + } + EvalArrayElement::KeyReference { key, value } => { + let key = eval_expr(key, context, scope, values)?; + next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; + let (value, target) = + eval_reference_array_element_value(value, context, scope, values)?; + (key, value, Some(target)) + } + }; + array = values.array_set(array, key, value)?; + if let Some(target) = target { + bind_array_element_reference(context, array, key, target, values)?; + } + } + Ok(array) +} + +/// Evaluates a by-reference array literal element and captures its writable source target. +fn eval_reference_array_element_value( + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + let (value, target) = eval_call_arg_value(value, context, scope, values)?; + target + .map(|target| (value, target)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Records one by-reference array element on the eval context side table. +fn bind_array_element_reference( + context: &mut ElephcEvalContext, + array: RuntimeCellHandle, + key: RuntimeCellHandle, + target: EvalReferenceTarget, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let key = eval_array_reference_key(key, values)?.ok_or(EvalStatus::RuntimeFatal)?; + context.bind_array_element_alias(array, key, target); + Ok(()) +} + +/// Normalizes a PHP array key for eval reference metadata lookups. +pub(in crate::interpreter) fn eval_array_reference_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + Ok(Some(match values.type_tag(key)? { + EVAL_TAG_INT => EvalArrayReferenceKey::Int(eval_int_value(key, values)?), + EVAL_TAG_STRING => { + let bytes = values.string_bytes(key)?; + if let Some(key) = eval_numeric_string_array_key(&bytes) { + EvalArrayReferenceKey::Int(key) + } else { + EvalArrayReferenceKey::String(bytes) + } + } + EVAL_TAG_NULL => EvalArrayReferenceKey::String(Vec::new()), + EVAL_TAG_BOOL | EVAL_TAG_FLOAT => EvalArrayReferenceKey::Int(eval_int_value(key, values)?), + _ => return Ok(None), + })) +} + +/// Advances an array literal's automatic key after an integer-normalized explicit key. +fn eval_array_next_key_after_explicit_key( + key: RuntimeCellHandle, + current_next_key: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let key = match values.type_tag(key)? { + EVAL_TAG_INT => key, + EVAL_TAG_STRING => { + let bytes = values.string_bytes(key)?; + let Some(key) = eval_numeric_string_array_key(&bytes) else { + return Ok(current_next_key); + }; + values.int(key)? + } + EVAL_TAG_NULL => return Ok(current_next_key), + _ => values.cast_int(key)?, + }; + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current_next_key) = current_next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current_next_key)?; + values.truthy(is_greater)? + } else { + true + }; + Ok(if replace { + Some(candidate) + } else { + current_next_key + }) +} + +/// Parses PHP integer-string array keys that normalize to integer keys. +pub(in crate::interpreter) fn eval_numeric_string_array_key(bytes: &[u8]) -> Option { + if bytes.is_empty() { + return None; + } + + let (negative, digits) = if bytes[0] == b'-' { + if bytes.len() == 1 { + return None; + } + (true, &bytes[1..]) + } else { + (false, bytes) + }; + + if digits[0] == b'0' { + return if !negative && digits.len() == 1 { + Some(0) + } else { + None + }; + } + if digits.iter().any(|byte| !byte.is_ascii_digit()) { + return None; + } + + let limit = if negative { + i64::MAX as u128 + 1 + } else { + i64::MAX as u128 + }; + let mut value = 0u128; + for digit in digits { + value = (value * 10) + u128::from(digit - b'0'); + if value > limit { + return None; + } + } + + if negative { + if value == i64::MAX as u128 + 1 { + Some(i64::MIN) + } else { + Some(-(value as i64)) + } + } else { + Some(value as i64) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtin_interfaces.rs b/crates/elephc-magician/src/interpreter/builtin_interfaces.rs new file mode 100644 index 0000000000..4289e42667 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtin_interfaces.rs @@ -0,0 +1,223 @@ +//! Purpose: +//! Provides eval-side method contracts for PHP builtin interfaces. +//! This keeps runtime interface declaration checks aligned with the main checker catalog. +//! +//! Called from: +//! - `crate::interpreter::statements` while registering eval class-like declarations. +//! +//! Key details: +//! - `Traversable` remains marker-only, matching the current checker model. +//! - Child interfaces include their builtin parent method requirements. + +use super::*; + +/// Returns method requirements for one PHP builtin interface name. +pub(super) fn builtin_interface_method_requirements( + interface: &str, +) -> Vec<(String, EvalInterfaceMethod)> { + let interface = interface.trim_start_matches('\\'); + let mut requirements = Vec::new(); + if interface.eq_ignore_ascii_case("Iterator") { + append_iterator_interface_requirements(&mut requirements); + } else if interface.eq_ignore_ascii_case("IteratorAggregate") { + requirements.push(( + String::from("IteratorAggregate"), + builtin_interface_method( + "getIterator", + &[], + Some(builtin_class_type("Traversable")), + ), + )); + } else if interface.eq_ignore_ascii_case("ArrayAccess") { + append_array_access_interface_requirements(&mut requirements); + } else if interface.eq_ignore_ascii_case("Countable") { + requirements.push(( + String::from("Countable"), + builtin_interface_method( + "count", + &[], + Some(builtin_type(EvalParameterTypeVariant::Int)), + ), + )); + } else if interface.eq_ignore_ascii_case("OuterIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("OuterIterator"), + builtin_interface_method( + "getInnerIterator", + &[], + Some(nullable_builtin_class_type("Iterator")), + ), + )); + } else if interface.eq_ignore_ascii_case("RecursiveIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("RecursiveIterator"), + builtin_interface_method( + "getChildren", + &[], + Some(nullable_builtin_class_type("RecursiveIterator")), + ), + )); + requirements.push(( + String::from("RecursiveIterator"), + builtin_interface_method( + "hasChildren", + &[], + Some(builtin_type(EvalParameterTypeVariant::Bool)), + ), + )); + } else if interface.eq_ignore_ascii_case("SeekableIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("SeekableIterator"), + builtin_interface_method( + "seek", + &[("offset", builtin_type(EvalParameterTypeVariant::Int))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("SplObserver") { + requirements.push(( + String::from("SplObserver"), + builtin_interface_method( + "update", + &[("subject", builtin_class_type("SplSubject"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("SplSubject") { + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "attach", + &[("observer", builtin_class_type("SplObserver"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "detach", + &[("observer", builtin_class_type("SplObserver"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "notify", + &[], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("Stringable") { + requirements.push(( + String::from("Stringable"), + builtin_interface_method( + "__toString", + &[], + Some(builtin_type(EvalParameterTypeVariant::String)), + ), + )); + } else if interface.eq_ignore_ascii_case("JsonSerializable") { + requirements.push(( + String::from("JsonSerializable"), + builtin_interface_method( + "jsonSerialize", + &[], + Some(builtin_type(EvalParameterTypeVariant::Mixed)), + ), + )); + } + requirements +} + +/// Appends the five methods required by PHP's `Iterator` interface. +fn append_iterator_interface_requirements(requirements: &mut Vec<(String, EvalInterfaceMethod)>) { + for (method, return_type) in [ + ("current", EvalParameterTypeVariant::Mixed), + ("key", EvalParameterTypeVariant::Mixed), + ("next", EvalParameterTypeVariant::Void), + ("valid", EvalParameterTypeVariant::Bool), + ("rewind", EvalParameterTypeVariant::Void), + ] { + requirements.push(( + String::from("Iterator"), + builtin_interface_method(method, &[], Some(builtin_type(return_type))), + )); + } +} + +/// Appends the four methods required by PHP's `ArrayAccess` interface. +fn append_array_access_interface_requirements( + requirements: &mut Vec<(String, EvalInterfaceMethod)>, +) { + for (method, params, return_type) in [ + ( + "offsetExists", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Bool, + ), + ( + "offsetGet", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Mixed, + ), + ( + "offsetSet", + vec![ + ("offset", builtin_type(EvalParameterTypeVariant::Mixed)), + ("value", builtin_type(EvalParameterTypeVariant::Mixed)), + ], + EvalParameterTypeVariant::Void, + ), + ( + "offsetUnset", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Void, + ), + ] { + requirements.push(( + String::from("ArrayAccess"), + builtin_interface_method(method, ¶ms, Some(builtin_type(return_type))), + )); + } +} + +/// Builds one synthetic eval interface method requirement for a PHP builtin interface. +fn builtin_interface_method( + name: &str, + params: &[(&str, EvalParameterType)], + return_type: Option, +) -> EvalInterfaceMethod { + EvalInterfaceMethod::new( + name, + params + .iter() + .map(|(param_name, _)| (*param_name).to_string()) + .collect(), + ) + .with_parameter_types( + params + .iter() + .map(|(_, param_type)| Some(param_type.clone())) + .collect(), + ) + .with_return_type(return_type) +} + +/// Returns one non-nullable scalar/object builtin interface type. +fn builtin_type(variant: EvalParameterTypeVariant) -> EvalParameterType { + EvalParameterType::new(vec![variant], false) +} + +/// Returns one non-nullable class/interface builtin interface type. +fn builtin_class_type(name: &str) -> EvalParameterType { + builtin_type(EvalParameterTypeVariant::Class(name.to_string())) +} + +/// Returns one nullable class/interface builtin interface type. +fn nullable_builtin_class_type(name: &str) -> EvalParameterType { + EvalParameterType::new(vec![EvalParameterTypeVariant::Class(name.to_string())], true) +} diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs new file mode 100644 index 0000000000..a2733e94ea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -0,0 +1,153 @@ +//! Purpose: +//! Public metadata view for eval-interpreter builtin support. +//! Gives parity tests a stable API for builtin existence and named-argument +//! parameter lists without duplicating the interpreter registry. +//! +//! Called from: +//! - `elephc-magician::builtin_metadata` re-export. +//! +//! Key details: +//! - Lookup normalizes names with PHP-style case-insensitivity. +//! - Signature shape is the same registry data used by eval named-argument binding. + +use super::builtins::{ + eval_builtin_param_names, eval_builtin_signature_shape, eval_date_procedural_alias_names, + eval_declared_builtin_exists, eval_declared_builtin_spec, eval_php_visible_builtin_exists, + eval_php_visible_builtin_function_names, EvalBuiltinDefaultValue, +}; + +/// A compact, comparison-friendly view of an eval builtin call signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinSignatureMetadata { + /// Parameter names in PHP call order. + pub params: Vec, + /// Number of leading parameters that must be supplied positionally or by name. + pub required_param_count: usize, + /// Number of parameters that carry explicit default values. + pub default_param_count: usize, + /// Name of the variadic parameter, when the builtin accepts one. + pub variadic: Option, + /// Parameter names that must be passed by reference. + pub by_ref_params: Vec, +} + +/// Returns whether the eval interpreter exposes a PHP-visible builtin name. +pub fn php_visible_builtin_exists(name: &str) -> bool { + let canonical = php_symbol_key(name); + eval_php_visible_builtin_exists(&canonical) +} + +/// Returns the eval interpreter's PHP-visible builtin names. +pub fn php_visible_builtin_names() -> &'static [&'static str] { + eval_php_visible_builtin_function_names() +} + +/// Returns whether the eval builtin is backed by the declarative registry. +pub fn php_visible_builtin_is_registry_declared(name: &str) -> bool { + let canonical = php_symbol_key(name); + eval_declared_builtin_exists(&canonical) +} + +/// Returns comparison metadata for one eval builtin signature, when named calls are tracked. +pub fn builtin_signature_metadata(name: &str) -> Option { + let canonical = php_symbol_key(name); + let params = eval_builtin_param_names(&canonical)? + .iter() + .map(|param| (*param).to_string()) + .collect::>(); + let shape = eval_builtin_signature_shape(&canonical)?; + Some(BuiltinSignatureMetadata { + params, + required_param_count: shape.required_param_count, + default_param_count: shape.default_param_count, + variadic: shape.variadic.map(str::to_string), + by_ref_params: shape + .by_ref_params + .iter() + .map(|param| (*param).to_string()) + .collect(), + }) +} + +/// Parameter metadata for one eval builtin, documentation-oriented. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinDocsParam { + /// PHP-visible parameter name. + pub name: String, + /// Whether the parameter binds caller storage by reference. + pub by_ref: bool, + /// PHP-source spelling of the default value, when the parameter is optional. + pub default: Option, +} + +/// Documentation-oriented metadata for one registry-declared eval builtin. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinDocsMetadata { + /// Canonical registry name. + pub name: String, + /// Lowercase eval-area spelling (interpreter file layout family). + pub area: String, + /// Parameters in PHP call order. + pub params: Vec, + /// Variadic parameter name, when the builtin accepts one. + pub variadic: Option, + /// Number of leading parameters that must be supplied. + pub required_param_count: usize, + /// Whether an expression-level dispatch hook is registered. + pub has_direct_hook: bool, + /// Whether an evaluated-argument dispatch hook is registered. + pub has_values_hook: bool, + /// Workspace-relative home file that declared the builtin. + pub home_file: String, +} + +/// Returns documentation metadata for one registry-declared eval builtin. +pub fn builtin_docs_metadata(name: &str) -> Option { + let canonical = php_symbol_key(name); + let spec = eval_declared_builtin_spec(&canonical)?; + Some(BuiltinDocsMetadata { + name: spec.name.to_string(), + area: spec.area().name().to_string(), + params: spec + .params + .iter() + .map(|param| BuiltinDocsParam { + name: param.name.to_string(), + by_ref: param.by_ref, + default: param.default.map(default_value_php_repr), + }) + .collect(), + variadic: spec.variadic.map(str::to_string), + required_param_count: spec.required_param_count(), + has_direct_hook: spec.direct.is_some(), + has_values_hook: spec.values.is_some(), + home_file: spec.home_file.to_string(), + }) +} + +/// Returns the procedural date/time alias names the eval dispatcher accepts +/// without declarative registry entries. +pub fn date_procedural_alias_names() -> &'static [&'static str] { + eval_date_procedural_alias_names() +} + +/// Formats an eval builtin default value with its PHP-source spelling. +fn default_value_php_repr(value: EvalBuiltinDefaultValue) -> String { + match value { + EvalBuiltinDefaultValue::Null => "null".to_string(), + EvalBuiltinDefaultValue::Bool(true) => "true".to_string(), + EvalBuiltinDefaultValue::Bool(false) => "false".to_string(), + EvalBuiltinDefaultValue::Int(value) => value.to_string(), + EvalBuiltinDefaultValue::Float(value) => format!("{:?}", value), + EvalBuiltinDefaultValue::String(value) => format!("\"{}\"", value.escape_default()), + EvalBuiltinDefaultValue::Bytes(value) => { + format!("\"{}\"", String::from_utf8_lossy(value).escape_default()) + } + EvalBuiltinDefaultValue::EmptyArray => "[]".to_string(), + } +} + +/// Normalizes a PHP symbol name for case-insensitive builtin lookup. +fn php_symbol_key(name: &str) -> String { + name.trim_start_matches('\\').to_ascii_lowercase() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs new file mode 100644 index 0000000000..88c41d405c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs @@ -0,0 +1,87 @@ +//! Purpose: +//! Declarative eval registry entry for `array_chunk`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_chunk", + area: Array, + params: [array, length], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_chunk` array builtin. +pub(in crate::interpreter) fn eval_array_chunk_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_chunk(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_chunk` array builtin. +pub(in crate::interpreter) fn eval_array_chunk_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_chunk_result(*array, *length, values) +} + +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +pub(in crate::interpreter) fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +pub(in crate::interpreter) fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs new file mode 100644 index 0000000000..5f6ebb2603 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Declarative eval registry entry for `array_column`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_column", + area: Array, + params: [array, column_key], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_column` array builtin. +pub(in crate::interpreter) fn eval_array_column_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_column(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_column` array builtin. +pub(in crate::interpreter) fn eval_array_column_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_column_result(*array, *column_key, values) +} + +/// Evaluates PHP `array_column()` over row-array and column-key expressions. +pub(in crate::interpreter) fn eval_builtin_array_column( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let column_key = eval_expr(column_key, context, scope, values)?; + eval_array_column_result(array, column_key, values) +} + +/// Builds `array_column()` by extracting present row columns into a reindexed array. +pub(in crate::interpreter) fn eval_array_column_result( + array: RuntimeCellHandle, + column_key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + let mut output_index = 0_i64; + for position in 0..len { + let row_key = values.array_iter_key(array, position)?; + let row = values.array_get(array, row_key)?; + if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + continue; + } + let exists = values.array_key_exists(column_key, row)?; + if !values.truthy(exists)? { + continue; + } + let column = values.array_get(row, column_key)?; + let target_key = values.int(output_index)?; + output_index = output_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, column)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs new file mode 100644 index 0000000000..843ec10562 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Declarative eval registry entry for `array_combine`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_combine", + area: Array, + params: [keys, values], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_combine` array builtin. +pub(in crate::interpreter) fn eval_array_combine_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_combine(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_combine` array builtin. +pub(in crate::interpreter) fn eval_array_combine_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_combine_result(*keys, *values_array, values) +} + +/// Evaluates PHP `array_combine()` over key and value array expressions. +pub(in crate::interpreter) fn eval_builtin_array_combine( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let values_array = eval_expr(values_array, context, scope, values)?; + eval_array_combine_result(keys, values_array, values) +} + +/// Builds the associative result for `array_combine()` from two eval arrays. +pub(in crate::interpreter) fn eval_array_combine_result( + keys: RuntimeCellHandle, + values_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + if len != values.array_len(values_array)? { + return Err(EvalStatus::RuntimeFatal); + } + + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + let target_key = values.cast_string(target_key)?; + let value_key = values.array_iter_key(values_array, position)?; + let value = values.array_get(values_array, value_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs new file mode 100644 index 0000000000..f70d6b0fb0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Declarative eval registry entry for `array_diff`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_diff", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_diff` array builtin. +pub(in crate::interpreter) fn eval_array_diff_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_value_set("array_diff", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_diff` array builtin. +pub(in crate::interpreter) fn eval_array_diff_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_value_set_result("array_diff", *left, *right, values) +} + +/// Evaluates PHP value-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_value_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_value_set_result(name, left, right, values) +} + +/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_value_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let mut right_values = Vec::with_capacity(right_len); + for position in 0..right_len { + let key = values.array_iter_key(right, position)?; + let value = values.array_get(right, key)?; + right_values.push(values.string_bytes(value)?); + } + + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let comparable = values.string_bytes(value)?; + let found = right_values + .iter() + .any(|right_value| right_value == &comparable); + let keep = match name { + "array_diff" => !found, + "array_intersect" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs new file mode 100644 index 0000000000..fabb72b64c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Declarative eval registry entry for `array_diff_key`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_diff_key", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_diff_key` array builtin. +pub(in crate::interpreter) fn eval_array_diff_key_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_key_set("array_diff_key", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_diff_key` array builtin. +pub(in crate::interpreter) fn eval_array_diff_key_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_key_set_result("array_diff_key", *left, *right, values) +} + +/// Evaluates PHP key-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_key_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_key_set_result(name, left, right, values) +} + +/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. +pub(in crate::interpreter) fn eval_array_key_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let exists = values.array_key_exists(key, right)?; + let found = values.truthy(exists)?; + let keep = match name { + "array_diff_key" => !found, + "array_intersect_key" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs new file mode 100644 index 0000000000..89e4d3a552 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Declarative eval registry entry for `array_fill`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_fill", + area: Array, + params: [start_index, count, value], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_fill` array builtin. +pub(in crate::interpreter) fn eval_array_fill_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_fill(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_fill` array builtin. +pub(in crate::interpreter) fn eval_array_fill_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_fill_result(*start, *count, *value, values) +} + +/// Evaluates PHP `array_fill()` over start, count, and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let count = eval_expr(count, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_result(start, count, value, values) +} + +/// Builds an `array_fill()` result with PHP's explicit integer key range. +pub(in crate::interpreter) fn eval_array_fill_result( + start: RuntimeCellHandle, + count: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let count = eval_int_value(count, values)?; + if count < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = if start == 0 { + values.array_new(count)? + } else { + values.assoc_new(count)? + }; + for offset in 0..count { + let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs new file mode 100644 index 0000000000..a5f3dd4e4b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `array_fill_keys`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_fill_keys", + area: Array, + params: [keys, value], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_fill_keys` array builtin. +pub(in crate::interpreter) fn eval_array_fill_keys_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_fill_keys(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_fill_keys` array builtin. +pub(in crate::interpreter) fn eval_array_fill_keys_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_fill_keys_result(*keys, *value, values) +} + +/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_keys_result(keys, value, values) +} + +/// Builds an `array_fill_keys()` result preserving the source key iteration order. +pub(in crate::interpreter) fn eval_array_fill_keys_result( + keys: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs new file mode 100644 index 0000000000..3c9aa52b63 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Declarative eval registry entry for `array_filter`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_filter", + area: Array, + params: [ + array, + callback = EvalBuiltinDefaultValue::Null, + mode = EvalBuiltinDefaultValue::Int(0), + ], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_filter` array builtin. +pub(in crate::interpreter) fn eval_array_filter_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_filter(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_filter` array builtin. +pub(in crate::interpreter) fn eval_array_filter_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values), + [array, callback] => eval_array_filter_result(*array, Some(*callback), None, context, values), + [array, callback, mode] => eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_filter()` for null and callable filtering modes. +pub(in crate::interpreter) fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result_from_scope(array, None, None, Some(scope), context, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result_from_scope( + array, + Some(callback), + None, + Some(scope), + context, + values, + ) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result_from_scope( + array, + Some(callback), + Some(mode), + Some(scope), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters eval array entries through PHP truthiness or a callable callback. +pub(in crate::interpreter) fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_filter_result_from_scope(array, callback, mode, None, context, values) +} + +/// Filters eval array entries with optional lexical scope for callback names. +fn eval_array_filter_result_from_scope( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match callback { + Some(callback) if !values.is_null(callback)? => { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + } + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let keep = if let Some(callback) = callback.as_ref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Reads and validates the optional `array_filter()` callback mode. +pub(in crate::interpreter) fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +pub(in crate::interpreter) fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs new file mode 100644 index 0000000000..096ff25cf0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Declarative eval registry entry for `array_flip`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-flip hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_flip", + area: Array, + params: [array], + direct: ArrayFlip, + values: ArrayFlip, +} +/// Dispatches direct eval calls for the `array_flip` array builtin. +pub(in crate::interpreter) fn eval_array_flip_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_flip(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_flip` array builtin. +pub(in crate::interpreter) fn eval_array_flip_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_flip_result(*array, values) +} + +/// Evaluates PHP `array_flip()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +pub(in crate::interpreter) fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs new file mode 100644 index 0000000000..0078e69312 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `array_intersect`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_intersect", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_intersect` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_diff::eval_builtin_array_value_set("array_intersect", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_intersect` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_diff::eval_array_value_set_result("array_intersect", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs new file mode 100644 index 0000000000..80d188076e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `array_intersect_key`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_intersect_key", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_intersect_key` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_key_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_diff_key::eval_builtin_array_key_set("array_intersect_key", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_intersect_key` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_key_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_diff_key::eval_array_key_set_result("array_intersect_key", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs new file mode 100644 index 0000000000..fef9a1dce3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Declarative eval registry entry for `array_key_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the runtime key-existence hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_key_exists", + area: Array, + params: [key, array], + direct: ArrayKeyExists, + values: ArrayKeyExists, +} +/// Dispatches direct eval calls for the `array_key_exists` array builtin. +pub(in crate::interpreter) fn eval_array_key_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_key_exists(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_key_exists` array builtin. +pub(in crate::interpreter) fn eval_array_key_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + values.array_key_exists(*key, *array) +} + +/// Evaluates PHP `array_key_exists()` over a key and array expression. +pub(in crate::interpreter) fn eval_builtin_array_key_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = eval_expr(key, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + values.array_key_exists(key, array) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs new file mode 100644 index 0000000000..acfe560383 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Eval registry entry and implementation for `array_keys`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Output is always a sequential indexed array containing input keys in +//! iteration order. + +use super::super::super::*; + +eval_builtin! { + name: "array_keys", + area: Array, + params: [array], + direct: ArrayKeys, + values: ArrayKeys, +} + +/// Evaluates PHP `array_keys()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_keys_result(array, values) +} + +/// Builds the sequential result array for `array_keys()`. +pub(in crate::interpreter) fn eval_array_keys_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let index = values.int(position as i64)?; + result = values.array_set(result, index, key)?; + } + Ok(result) +} +/// Dispatches direct eval calls for the `array_keys` array builtin. +pub(in crate::interpreter) fn eval_array_keys_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_keys(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_keys` array builtin. +pub(in crate::interpreter) fn eval_array_keys_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_keys_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs new file mode 100644 index 0000000000..ddcaa7c78a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs @@ -0,0 +1,173 @@ +//! Purpose: +//! Declarative eval registry entry for `array_map`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_map", + area: Array, + params: [callback, array], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_map` array builtin. +pub(in crate::interpreter) fn eval_array_map_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_map(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_map` array builtin. +pub(in crate::interpreter) fn eval_array_map_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_map_result(*callback, arrays, context, values) +} + +/// Evaluates PHP `array_map()` for one or more arrays and an optional callback. +pub(in crate::interpreter) fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +pub(in crate::interpreter) fn eval_array_map_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_map_result_from_scope(callback, arrays, None, context, values) +} + +/// Maps one or more eval arrays with optional lexical scope for callback names. +fn eval_array_map_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result_from_scope( + callback, + arrays, + lexical_scope, + context, + values, + ); + }; + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let len = values.array_len(*array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Maps multiple eval arrays with optional lexical scope for callback names. +fn eval_array_map_variadic_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +pub(in crate::interpreter) fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs new file mode 100644 index 0000000000..b865f2953f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs @@ -0,0 +1,95 @@ +//! Purpose: +//! Declarative eval registry entry for `array_merge`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_merge", + area: Array, + params: [], + variadic: arrays, + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_merge` array builtin. +pub(in crate::interpreter) fn eval_array_merge_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_merge(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_merge` array builtin. +pub(in crate::interpreter) fn eval_array_merge_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_merge_result(*left, *right, values) +} + +/// Evaluates PHP `array_merge()` over two array expressions. +pub(in crate::interpreter) fn eval_builtin_array_merge( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_merge_result(left, right, values) +} + +/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. +pub(in crate::interpreter) fn eval_array_merge_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let capacity = left_len + .checked_add(right_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut result = values.assoc_new(capacity)?; + let mut next_numeric_key = 0_i64; + result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; + eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) +} + +/// Appends one source array to an `array_merge()` result using PHP key handling. +pub(in crate::interpreter) fn eval_array_merge_append_operand( + mut result: RuntimeCellHandle, + source: RuntimeCellHandle, + next_numeric_key: &mut i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(source)?; + for position in 0..len { + let source_key = values.array_iter_key(source, position)?; + let source_value = values.array_get(source, source_key)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { + source_key + } else { + let target_key = values.int(*next_numeric_key)?; + *next_numeric_key = (*next_numeric_key) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + target_key + }; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs new file mode 100644 index 0000000000..0cca24d3fa --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs @@ -0,0 +1,112 @@ +//! Purpose: +//! Declarative eval registry entry for `array_pad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-pad hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_pad", + area: Array, + params: [array, length, value], + direct: ArrayPad, + values: ArrayPad, +} +/// Dispatches direct eval calls for the `array_pad` array builtin. +pub(in crate::interpreter) fn eval_array_pad_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_pad(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_pad` array builtin. +pub(in crate::interpreter) fn eval_array_pad_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_pad_result(*array, *length, *value, values) +} + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +pub(in crate::interpreter) fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +pub(in crate::interpreter) fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +pub(in crate::interpreter) fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs new file mode 100644 index 0000000000..8242e26368 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Declarative eval registry entry for `array_pop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "array_pop", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_pop` array mutator. +pub(in crate::interpreter) fn eval_array_pop_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_warn_array_by_value("array_pop", values)?; + eval_array_pop_shift_value_result("array_pop", *array, values) +} + +/// Emits the standard by-value warning for array mutator callable calls. +pub(in crate::interpreter) fn eval_warn_array_by_value( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + )) +} + +/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. +pub(in crate::interpreter) fn eval_array_pop_shift_value_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + if len == 0 { + return values.null(); + } + let position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Builds the return value plus replacement array for direct pop/shift write-back. +pub(in crate::interpreter) fn eval_array_pop_shift_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + if len == 0 { + let replacement = match tag { + EVAL_TAG_ARRAY => values.array_new(0)?, + EVAL_TAG_ASSOC => values.assoc_new(0)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + return Ok((values.null()?, replacement)); + } + + let removed_position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let removed_key = values.array_iter_key(array, removed_position)?; + let removed_value = values.array_get(array, removed_key)?; + let replacement = match tag { + EVAL_TAG_ARRAY => { + eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? + } + EVAL_TAG_ASSOC => { + eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok((removed_value, replacement)) +} + +/// Rebuilds an indexed array after removing one position and reindexing values. +pub(in crate::interpreter) fn eval_array_pop_shift_indexed_replacement( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(len.saturating_sub(1))?; + let mut target = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. +pub(in crate::interpreter) fn eval_array_pop_shift_assoc_replacement( + name: &str, + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "array_shift" + && eval_array_remaining_keys_are_int(array, removed_position, len, values)? + { + return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); + } + + let mut result = values.assoc_new(len.saturating_sub(1))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every remaining key is an integer after removing one element. +pub(in crate::interpreter) fn eval_array_remaining_keys_are_int( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if position == removed_position { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. +pub(in crate::interpreter) fn eval_array_pop_shift_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (array, target) = super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values)?; + + let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs new file mode 100644 index 0000000000..6a1c5299ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `array_product`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-aggregate hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_product", + area: Array, + params: [array], + direct: ArrayAggregate, + values: ArrayAggregate, +} +/// Dispatches direct eval calls for the `array_product` array builtin. +pub(in crate::interpreter) fn eval_array_product_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_sum::eval_builtin_array_aggregate("array_product", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_product` array builtin. +pub(in crate::interpreter) fn eval_array_product_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_sum::eval_array_aggregate_result("array_product", *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs new file mode 100644 index 0000000000..abcca5d0b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs @@ -0,0 +1,218 @@ +//! Purpose: +//! Declarative eval registry entry for `array_push`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "array_push", + area: Array, + params: [array: by_ref], + variadic: values, + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_push` array mutator. +pub(in crate::interpreter) fn eval_array_push_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_push", values)?; + eval_array_push_unshift_count_result(*array, inserted.len(), values) +} + +/// Returns the resulting element count for by-value push/unshift dynamic calls. +pub(in crate::interpreter) fn eval_array_push_unshift_count_result( + array: RuntimeCellHandle, + inserted_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let total = values + .array_len(array)? + .checked_add(inserted_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(total) +} + +/// Builds the replacement array for direct push/unshift write-back. +pub(in crate::interpreter) fn eval_array_push_unshift_replacement( + name: &str, + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(array)?) { + ("array_push", EVAL_TAG_ARRAY) => { + eval_array_push_indexed_replacement(array, inserted, values) + } + ("array_push", EVAL_TAG_ASSOC) => { + eval_array_push_assoc_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ARRAY) => { + eval_array_unshift_indexed_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ASSOC) => { + eval_array_unshift_assoc_replacement(array, inserted, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Rebuilds an indexed array after appending values. +pub(in crate::interpreter) fn eval_array_push_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, target_key, value)?; + } + for (offset, value) in inserted.iter().copied().enumerate() { + let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after appending values at PHP's next integer keys. +pub(in crate::interpreter) fn eval_array_push_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_key = 0_i64; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? == EVAL_TAG_INT { + next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + for value in inserted.iter().copied() { + let key = values.int(next_key)?; + next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an indexed array after prepending values and reindexing the original tail. +pub(in crate::interpreter) fn eval_array_unshift_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + let mut target = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after prepending values and reindexing integer keys. +pub(in crate::interpreter) fn eval_array_unshift_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_keys_are_int(array, len, values)? { + return eval_array_unshift_indexed_replacement(array, inserted, values); + } + + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_int_key = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in the array is integer-shaped. +pub(in crate::interpreter) fn eval_array_keys_are_int( + array: RuntimeCellHandle, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls and writes back the array. +pub(in crate::interpreter) fn eval_array_push_unshift_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 || !eval_call_args_are_plain_positional(args) { + return Err(EvalStatus::RuntimeFatal); + } + let (array, target) = super::mutation::eval_array_mutation_lvalue_arg(&args[0], context, scope, values)?; + let mut inserted = Vec::with_capacity(args.len() - 1); + for arg in &args[1..] { + inserted.push(eval_expr(arg.value(), context, scope, values)?); + } + + let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; + let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs new file mode 100644 index 0000000000..b2c27aecac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Declarative eval registry entry for `array_rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-rand hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_rand", + area: Array, + params: [array], + direct: ArrayRand, + values: ArrayRand, +} +/// Dispatches direct eval calls for the `array_rand` array builtin. +pub(in crate::interpreter) fn eval_array_rand_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_rand(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_rand` array builtin. +pub(in crate::interpreter) fn eval_array_rand_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_rand_result(*array, values) +} + +/// Evaluates PHP `array_rand()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_rand_result(array, values) +} + +/// Returns a valid random key from a non-empty eval array. +pub(in crate::interpreter) fn eval_array_rand_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if len == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let position = eval_random_position(len); + values.array_iter_key(array, position) +} + +/// Chooses a pseudo-random array position within `[0, len)`. +pub(in crate::interpreter) fn eval_random_position(len: usize) -> usize { + (eval_random_u128() % (len as u128)) as usize +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs new file mode 100644 index 0000000000..1c59489a95 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Declarative eval registry entry for `array_reduce`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_reduce", + area: Array, + params: [array, callback, initial = EvalBuiltinDefaultValue::Null], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `array_reduce` array builtin. +pub(in crate::interpreter) fn eval_array_reduce_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_reduce(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_reduce` array builtin. +pub(in crate::interpreter) fn eval_array_reduce_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values) + } + [array, callback, initial] => eval_array_reduce_result(*array, *callback, *initial, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_reduce()` with an optional initial carry value. +pub(in crate::interpreter) fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) +} + +/// Reduces one eval array by invoking a callable with carry and item cells. +pub(in crate::interpreter) fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) +} + +/// Reduces one eval array with optional lexical scope for callback names. +fn eval_array_reduce_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = + eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs new file mode 100644 index 0000000000..f90abdaff5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs @@ -0,0 +1,103 @@ +//! Purpose: +//! Declarative eval registry entry for `array_reverse`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-reverse hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_reverse", + area: Array, + params: [array, preserve_keys = EvalBuiltinDefaultValue::Bool(false)], + direct: ArrayReverse, + values: ArrayReverse, +} +/// Dispatches direct eval calls for the `array_reverse` array builtin. +pub(in crate::interpreter) fn eval_array_reverse_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_reverse(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_reverse` array builtin. +pub(in crate::interpreter) fn eval_array_reverse_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values), + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_reverse()` over an eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +pub(in crate::interpreter) fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs new file mode 100644 index 0000000000..d56a1eaf81 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Declarative eval registry entry for `array_search`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-search hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_search", + area: Array, + params: [needle, haystack, strict = EvalBuiltinDefaultValue::Bool(false)], + direct: ArraySearch, + values: ArraySearch, +} +/// Dispatches direct eval calls for the `array_search` array builtin. +pub(in crate::interpreter) fn eval_array_search_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_search("array_search", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_search` array builtin. +pub(in crate::interpreter) fn eval_array_search_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_search_result("array_search", *needle, *array, values) +} + +/// Evaluates PHP array search builtins over a needle and haystack expression. +pub(in crate::interpreter) fn eval_builtin_array_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let needle = eval_expr(needle, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_search_result(name, needle, array, values) +} + +/// Searches an eval array with PHP's default loose comparison semantics. +pub(in crate::interpreter) fn eval_array_search_result( + name: &str, + needle: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; + if values.truthy(equal)? { + return match name { + "in_array" => values.bool_value(true), + "array_search" => Ok(key), + _ => Err(EvalStatus::UnsupportedConstruct), + }; + } + } + match name { + "in_array" => values.bool_value(false), + "array_search" => values.bool_value(false), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs new file mode 100644 index 0000000000..2c319edaf4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `array_shift`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "array_shift", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_shift` array mutator. +pub(in crate::interpreter) fn eval_array_shift_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_shift", values)?; + super::array_pop::eval_array_pop_shift_value_result("array_shift", *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs new file mode 100644 index 0000000000..4f87ec7ea6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs @@ -0,0 +1,129 @@ +//! Purpose: +//! Declarative eval registry entry for `array_slice`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-slice hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_slice", + area: Array, + params: [array, offset, length = EvalBuiltinDefaultValue::Null], + direct: ArraySlice, + values: ArraySlice, +} +/// Dispatches direct eval calls for the `array_slice` array builtin. +pub(in crate::interpreter) fn eval_array_slice_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_slice(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_slice` array builtin. +pub(in crate::interpreter) fn eval_array_slice_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values), + [array, offset, length] => eval_array_slice_result(*array, *offset, Some(*length), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +pub(in crate::interpreter) fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +pub(in crate::interpreter) fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +pub(in crate::interpreter) fn eval_slice_start( + len: usize, + offset: i64, +) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +pub(in crate::interpreter) fn eval_slice_end( + len: usize, + start: usize, + length: i64, +) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs new file mode 100644 index 0000000000..59904e8439 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs @@ -0,0 +1,355 @@ +//! Purpose: +//! Declarative eval registry entry for `array_splice`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "array_splice", + area: Array, + params: [ + array: by_ref, + offset, + length = EvalBuiltinDefaultValue::Null, + replacement = EvalBuiltinDefaultValue::EmptyArray, + ], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_splice` array mutator. +pub(in crate::interpreter) fn eval_array_splice_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match evaluated_args { + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, + [array, offset, length] => eval_array_splice_value_result(*array, *offset, Some(*length), values)?, + [array, offset, length, _replacement] => eval_array_splice_value_result(*array, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning("array_splice(): Argument #1 ($array) must be passed by reference, value given")?; + Ok(result) +} + +/// Evaluates direct by-reference `array_splice()` calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_splice_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target, offset, length, replacement_arg) = + eval_array_splice_direct_args(args, context, scope, values)?; + + let (removed, replacement) = + eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(removed) +} + +/// Evaluates and binds direct `array_splice()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_array_splice_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = None; + let mut offset = None; + let mut length = None; + let mut replacement = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "offset", + 2 => "length", + 3 => "replacement", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array = Some(super::mutation::eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); + } + "offset" => { + if offset.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + offset = Some(eval_expr(arg.value(), context, scope, values)?); + } + "length" => { + if length.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + length = Some(eval_expr(arg.value(), context, scope, values)?); + } + "replacement" => { + if replacement.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + replacement = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; + let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, target, offset, length, replacement)) +} + +/// Returns the removed elements that `array_splice()` would produce without mutating the source. +pub(in crate::interpreter) fn eval_array_splice_value_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + eval_array_splice_removed(array, start, end, values) +} + +/// Builds both removed and replacement arrays for direct `array_splice()` write-back. +pub(in crate::interpreter) fn eval_array_splice_removed_and_replacement( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + let removed = eval_array_splice_removed(array, start, end, values)?; + let inserted = eval_array_splice_insert_values(replacement, values)?; + let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; + Ok((removed, replacement)) +} + +/// Converts splice offset and length cells into bounded source positions. +pub(in crate::interpreter) fn eval_array_splice_bounds( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(usize, usize), EvalStatus> { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = super::array_slice::eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + super::array_slice::eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + Ok((start, end)) +} + +/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. +pub(in crate::interpreter) fn eval_array_splice_removed( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = end.saturating_sub(start); + if eval_array_range_keys_are_int(array, start, end, values)? { + let mut result = values.array_new(len)?; + let mut target = 0_i64; + for position in start..end { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len)?; + let mut next_int_key = 0_i64; + for position in start..end { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Expands the optional `array_splice()` replacement value into inserted values. +pub(in crate::interpreter) fn eval_array_splice_insert_values( + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(replacement) = replacement else { + return Ok(Vec::new()); + }; + if !matches!( + values.type_tag(replacement)?, + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC + ) { + return Ok(vec![replacement]); + } + + let len = values.array_len(replacement)?; + let mut inserted = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(replacement, position)?; + inserted.push(values.array_get(replacement, key)?); + } + Ok(inserted) +} + +/// Builds the source replacement after removing the requested splice range. +pub(in crate::interpreter) fn eval_array_splice_replacement( + array: RuntimeCellHandle, + start: usize, + end: usize, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let new_len = len + .saturating_sub(end.saturating_sub(start)) + .checked_add(inserted.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { + let mut result = values.array_new(new_len)?; + let mut target = 0_i64; + for position in 0..start { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(new_len)?; + let mut next_int_key = 0_i64; + for position in 0..start { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in one source position range is integer-shaped. +pub(in crate::interpreter) fn eval_array_range_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in start..end { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns true when every key outside the removed splice range is integer-shaped. +pub(in crate::interpreter) fn eval_array_splice_remaining_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs new file mode 100644 index 0000000000..45f141dc1b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Declarative eval registry entry for `array_sum`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-aggregate hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_sum", + area: Array, + params: [array], + direct: ArrayAggregate, + values: ArrayAggregate, +} +/// Dispatches direct eval calls for the `array_sum` array builtin. +pub(in crate::interpreter) fn eval_array_sum_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_aggregate("array_sum", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_sum` array builtin. +pub(in crate::interpreter) fn eval_array_sum_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_aggregate_result("array_sum", *array, values) +} + +/// Evaluates PHP array aggregate builtins over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_aggregate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_aggregate_result(name, array, values) +} + +/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. +pub(in crate::interpreter) fn eval_array_aggregate_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = match name { + "array_sum" => values.int(0)?, + "array_product" => values.int(1)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = match name { + "array_sum" => values.add(result, value)?, + "array_product" => values.mul(result, value)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs new file mode 100644 index 0000000000..90d1f019f6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Declarative eval registry entry for `array_unique`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-unique hook. + +use super::super::super::*; + +eval_builtin! { + name: "array_unique", + area: Array, + params: [array], + direct: ArrayUnique, + values: ArrayUnique, +} +/// Dispatches direct eval calls for the `array_unique` array builtin. +pub(in crate::interpreter) fn eval_array_unique_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_unique(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_unique` array builtin. +pub(in crate::interpreter) fn eval_array_unique_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_unique_result(*array, values) +} + +/// Evaluates PHP `array_unique()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs new file mode 100644 index 0000000000..9b6d3136d0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Declarative eval registry entry for `array_unshift`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "array_unshift", + area: Array, + params: [array: by_ref], + variadic: values, + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_unshift` array mutator. +pub(in crate::interpreter) fn eval_array_unshift_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_unshift", values)?; + super::array_push::eval_array_push_unshift_count_result(*array, inserted.len(), values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs new file mode 100644 index 0000000000..34f40324ce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Eval registry entry and implementation for `array_values`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Output is always a sequential indexed array containing input values in +//! iteration order. + +use super::super::super::*; + +eval_builtin! { + name: "array_values", + area: Array, + params: [array], + direct: ArrayValues, + values: ArrayValues, +} + +/// Evaluates PHP `array_values()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_values( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_values_result(array, values) +} + +/// Builds the sequential result array for `array_values()`. +pub(in crate::interpreter) fn eval_array_values_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) +} +/// Dispatches direct eval calls for the `array_values` array builtin. +pub(in crate::interpreter) fn eval_array_values_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_values(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_values` array builtin. +pub(in crate::interpreter) fn eval_array_values_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_values_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs new file mode 100644 index 0000000000..ee36e0d4c8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs @@ -0,0 +1,170 @@ +//! Purpose: +//! Declarative eval registry entry for `array_walk`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "array_walk", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `array_walk` array mutator. +pub(in crate::interpreter) fn eval_array_walk_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + values.warning("array_walk(): Argument #1 ($array) must be passed by reference, value given")?; + eval_array_walk_result(*array, *callback, context, values) +} + +/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. +pub(in crate::interpreter) fn eval_builtin_array_walk_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, array_target, callback) = + eval_array_walk_direct_args(args, context, scope, values)?; + eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) +} + +/// Evaluates and binds direct `array_walk()` arguments in PHP source order. +fn eval_array_walk_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array_target = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array_target = Some(super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values)?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, array_target, callback)) +} + +/// Walks one writable eval array by invoking a callable with element ref targets. +pub(in crate::interpreter) fn eval_array_walk_ref_result( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) +} + +/// Walks one writable eval array with optional lexical scope for callback names. +fn eval_array_walk_ref_result_from_scope( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let current_array = eval_reference_target_value(&array_target, context, values)?; + let key = values.array_iter_key(current_array, position)?; + let value = values.array_get(current_array, key)?; + let ref_target = EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target.clone()), + index: key, + }; + let args = vec![ + EvaluatedCallArg { + name: None, + value, + ref_target: Some(ref_target), + }, + EvaluatedCallArg { + name: None, + value: key, + ref_target: None, + }, + ]; + let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; + } + values.bool_value(true) +} + +/// Walks one eval array by invoking a callable with value and key cells. +pub(in crate::interpreter) fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_result_from_scope(array, callback, None, context, values) +} + +/// Walks one eval array with optional lexical scope for callback names. +fn eval_array_walk_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs new file mode 100644 index 0000000000..94e818a2df --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `arsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "arsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `arsort` array mutator. +pub(in crate::interpreter) fn eval_arsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("arsort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs new file mode 100644 index 0000000000..a969923046 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `asort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "asort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `asort` array mutator. +pub(in crate::interpreter) fn eval_asort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("asort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/count.rs b/crates/elephc-magician/src/interpreter/builtins/array/count.rs new file mode 100644 index 0000000000..85461169d4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/count.rs @@ -0,0 +1,130 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `count`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Recursive counting tracks visited arrays to avoid cycles. +//! - Top-level objects dispatch through `Countable::count()` when applicable. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "count", + area: Array, + params: [value, mode = EvalBuiltinDefaultValue::Int(0)], + direct: Count, + values: Count, +} +/// Dispatches direct eval calls for the `count` array builtin. +pub(in crate::interpreter) fn eval_count_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_count(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `count` array builtin. +pub(in crate::interpreter) fn eval_count_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_count_result(*value, None, context, values), + [value, mode] => eval_count_result(*value, Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates the builtin `count(...)` for arrays and `Countable` objects. +pub(in crate::interpreter) fn eval_builtin_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_count_result(value, None, context, values) + } + [value, mode] => { + let value = eval_expr(value, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_result(value, Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts an eval array or dispatches top-level `Countable` objects. +pub(in crate::interpreter) fn eval_count_result( + value: RuntimeCellHandle, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => EVAL_COUNT_NORMAL, + }; + if !matches!(mode, EVAL_COUNT_NORMAL | EVAL_COUNT_RECURSIVE) { + return Err(EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT + && eval_countable_object_matches(value, context, values)? + { + return eval_method_call_result(value, "count", Vec::new(), context, values); + } + let len = match mode { + EVAL_COUNT_NORMAL => values.array_len(value)?, + EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, + _ => unreachable!("count mode was validated before dispatch"), + }; + let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + +/// Returns whether an object value satisfies PHP's `Countable` interface. +fn eval_countable_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "Countable", false, context, values)? + .map_or_else(|| values.object_is_a(value, "Countable", false), Ok) +} + +/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. +pub(in crate::interpreter) fn eval_count_recursive_len( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + arrays_seen: &mut Vec, +) -> Result { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Ok(0); + } + arrays_seen.push(address); + + let len = values.array_len(value)?; + let mut total = len; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + if values.is_array_like(element)? { + total = total + .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) + .ok_or(EvalStatus::RuntimeFatal)?; + } + } + + arrays_seen.pop(); + Ok(total) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs new file mode 100644 index 0000000000..0ecfdd1af4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Area-level direct dispatch for array builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. +//! +//! Key details: +//! - Dispatch stays thin and routes every builtin through its leaf adapter. + +use super::super::super::*; + +/// Routes direct expression-level array builtin calls through per-builtin leaf adapters. +pub(in crate::interpreter) fn eval_builtin_array_declared_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_sum" => super::array_sum::eval_array_sum_declared_call(args, context, scope, values), + "array_product" => super::array_product::eval_array_product_declared_call(args, context, scope, values), + "array_chunk" => super::array_chunk::eval_array_chunk_declared_call(args, context, scope, values), + "array_column" => super::array_column::eval_array_column_declared_call(args, context, scope, values), + "array_combine" => super::array_combine::eval_array_combine_declared_call(args, context, scope, values), + "array_diff" => super::array_diff::eval_array_diff_declared_call(args, context, scope, values), + "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_call(args, context, scope, values), + "array_fill" => super::array_fill::eval_array_fill_declared_call(args, context, scope, values), + "array_fill_keys" => super::array_fill_keys::eval_array_fill_keys_declared_call(args, context, scope, values), + "array_filter" => super::array_filter::eval_array_filter_declared_call(args, context, scope, values), + "array_intersect" => super::array_intersect::eval_array_intersect_declared_call(args, context, scope, values), + "array_intersect_key" => super::array_intersect_key::eval_array_intersect_key_declared_call(args, context, scope, values), + "array_map" => super::array_map::eval_array_map_declared_call(args, context, scope, values), + "array_merge" => super::array_merge::eval_array_merge_declared_call(args, context, scope, values), + "array_reduce" => super::array_reduce::eval_array_reduce_declared_call(args, context, scope, values), + "iterator_apply" => super::iterator_apply::eval_iterator_apply_declared_call(args, context, scope, values), + "iterator_count" => super::iterator_count::eval_iterator_count_declared_call(args, context, scope, values), + "iterator_to_array" => super::iterator_to_array::eval_iterator_to_array_declared_call(args, context, scope, values), + "array_flip" => super::array_flip::eval_array_flip_declared_call(args, context, scope, values), + "array_key_exists" => super::array_key_exists::eval_array_key_exists_declared_call(args, context, scope, values), + "array_pad" => super::array_pad::eval_array_pad_declared_call(args, context, scope, values), + "array_keys" => super::array_keys::eval_array_keys_declared_call(args, context, scope, values), + "array_rand" => super::array_rand::eval_array_rand_declared_call(args, context, scope, values), + "array_reverse" => super::array_reverse::eval_array_reverse_declared_call(args, context, scope, values), + "array_search" => super::array_search::eval_array_search_declared_call(args, context, scope, values), + "in_array" => super::in_array::eval_in_array_declared_call(args, context, scope, values), + "array_slice" => super::array_slice::eval_array_slice_declared_call(args, context, scope, values), + "array_unique" => super::array_unique::eval_array_unique_declared_call(args, context, scope, values), + "array_values" => super::array_values::eval_array_values_declared_call(args, context, scope, values), + "count" => super::count::eval_count_declared_call(args, context, scope, values), + "range" => super::range::eval_range_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs new file mode 100644 index 0000000000..d773a16621 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `in_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-search hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "in_array", + area: Array, + params: [needle, haystack, strict = EvalBuiltinDefaultValue::Bool(false)], + direct: ArraySearch, + values: ArraySearch, +} +/// Dispatches direct eval calls for the `in_array` array builtin. +pub(in crate::interpreter) fn eval_in_array_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_search::eval_builtin_array_search("in_array", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `in_array` array builtin. +pub(in crate::interpreter) fn eval_in_array_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_search::eval_array_search_result("in_array", *needle, *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs new file mode 100644 index 0000000000..08a41d0b43 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs @@ -0,0 +1,153 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_apply`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "iterator_apply", + area: Array, + params: [iterator, callback, args = EvalBuiltinDefaultValue::Null], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `iterator_apply` array builtin. +pub(in crate::interpreter) fn eval_iterator_apply_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_apply(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_apply` array builtin. +pub(in crate::interpreter) fn eval_iterator_apply_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, context, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, context, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, context, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +pub(in crate::interpreter) fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, context, values) +} + +/// Applies a callback to each valid position of an eval-supported Traversable object. +pub(in crate::interpreter) fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs new file mode 100644 index 0000000000..f04411aff7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_count`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::super::*; + +eval_builtin! { + name: "iterator_count", + area: Array, + params: [iterator], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `iterator_count` array builtin. +pub(in crate::interpreter) fn eval_iterator_count_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_count(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_count` array builtin. +pub(in crate::interpreter) fn eval_iterator_count_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_iterator_count_result(*iterator, values) +} + +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs new file mode 100644 index 0000000000..5a6fceba42 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs @@ -0,0 +1,97 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_to_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +use super::super::super::*; + +eval_builtin! { + name: "iterator_to_array", + area: Array, + params: [iterator, preserve_keys = EvalBuiltinDefaultValue::Bool(true)], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `iterator_to_array` array builtin. +pub(in crate::interpreter) fn eval_iterator_to_array_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_to_array(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_to_array` array builtin. +pub(in crate::interpreter) fn eval_iterator_to_array_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values), + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +pub(in crate::interpreter) fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + super::array_values::eval_array_values_result(iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +pub(in crate::interpreter) fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs new file mode 100644 index 0000000000..123df91425 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `krsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "krsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `krsort` array mutator. +pub(in crate::interpreter) fn eval_krsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("krsort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs new file mode 100644 index 0000000000..ca403d432a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `ksort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "ksort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `ksort` array mutator. +pub(in crate::interpreter) fn eval_ksort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("ksort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs new file mode 100644 index 0000000000..ed9b8dc282 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Per-builtin declarations and eval adapters for array and collection functions. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own the concrete +//! direct or evaluated-argument adapter used by registry hooks. + +mod array_chunk; +mod array_column; +mod array_combine; +mod array_diff; +mod array_diff_key; +mod array_fill; +mod array_fill_keys; +mod array_filter; +mod array_flip; +mod array_intersect; +mod array_intersect_key; +mod array_key_exists; +mod array_keys; +mod array_map; +mod array_merge; +mod array_pad; +mod array_pop; +mod array_product; +mod array_push; +mod array_rand; +mod array_reduce; +mod array_reverse; +mod array_search; +mod array_shift; +mod array_slice; +mod array_splice; +mod array_sum; +mod array_unique; +mod array_unshift; +mod array_values; +mod array_walk; +mod arsort; +mod asort; +mod count; +mod direct_dispatch; +mod in_array; +mod iterator_apply; +mod iterator_count; +mod iterator_to_array; +mod krsort; +mod ksort; +mod mutating_dispatch; +mod mutation; +mod natcasesort; +mod natsort; +mod range; +mod rsort; +mod shuffle; +mod sort; +mod uasort; +mod uksort; +mod usort; +mod values_dispatch; + +pub(in crate::interpreter) use array_pop::eval_array_pop_shift_replacement; +pub(in crate::interpreter) use array_push::{ + eval_array_push_unshift_count_result, eval_array_push_unshift_replacement, +}; +pub(in crate::interpreter) use array_splice::eval_array_splice_removed_and_replacement; +pub(in crate::interpreter) use array_walk::eval_array_walk_ref_result; +pub(in crate::interpreter) use direct_dispatch::eval_builtin_array_declared_call; +pub(in crate::interpreter) use mutating_dispatch::eval_builtin_array_mutating_declared_call; +pub(in crate::interpreter) use sort::eval_array_sort_replacement; +pub(in crate::interpreter) use usort::eval_user_sort_replacement; +pub(in crate::interpreter) use values_dispatch::eval_array_declared_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs new file mode 100644 index 0000000000..769c6dc513 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Area-level direct dispatch for source-sensitive array mutator builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::calls::eval_call()`. +//! +//! Key details: +//! - Dispatch stays orchestration-only; actual PHP-visible behavior lives in the +//! builtin owner files or the closest shared owner builtin. + +use super::super::super::*; + +/// Routes direct source-sensitive array mutator calls through builtin owner files. +pub(in crate::interpreter) fn eval_builtin_array_mutating_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "settype" => eval_builtin_settype_call(args, context, scope, values), + "array_pop" | "array_shift" => { + super::array_pop::eval_array_pop_shift_declared_call(name, args, context, scope, values) + } + "array_push" | "array_unshift" => { + super::array_push::eval_array_push_unshift_declared_call(name, args, context, scope, values) + } + "array_splice" => super::array_splice::eval_builtin_array_splice_call(args, context, scope, values), + "array_walk" => super::array_walk::eval_builtin_array_walk_call(args, context, scope, values), + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + super::sort::eval_array_sort_declared_call(name, args, context, scope, values) + } + "uasort" | "uksort" | "usort" => { + super::usort::eval_user_sort_declared_call(name, args, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs new file mode 100644 index 0000000000..33f10d51ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Shared lvalue binding for source-sensitive array mutator builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::array` mutating builtin owners. +//! +//! Key details: +//! - The helper keeps the by-reference storage target together with the current +//! array cell so callers can write back replacements after PHP-visible work. + +use super::super::super::*; + +/// Captures the first by-reference array mutator argument as a writable lvalue. +pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( + arg: &EvalCallArg, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let (array, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + Ok((array, target)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs new file mode 100644 index 0000000000..8268460ead --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `natcasesort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "natcasesort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `natcasesort` array mutator. +pub(in crate::interpreter) fn eval_natcasesort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("natcasesort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs new file mode 100644 index 0000000000..f50e1ecd02 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `natsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "natsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `natsort` array mutator. +pub(in crate::interpreter) fn eval_natsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("natsort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/range.rs b/crates/elephc-magician/src/interpreter/builtins/array/range.rs new file mode 100644 index 0000000000..2d9815f184 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/range.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Declarative eval registry entry for `range`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the integer range hook. + +use super::super::super::*; + +eval_builtin! { + name: "range", + area: Array, + params: [start, end], + direct: Range, + values: Range, +} +/// Dispatches direct eval calls for the `range` array builtin. +pub(in crate::interpreter) fn eval_range_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_range(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `range` array builtin. +pub(in crate::interpreter) fn eval_range_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_range_result(*start, *end, values) +} + +/// Evaluates PHP `range()` over integer-compatible start and end expressions. +pub(in crate::interpreter) fn eval_builtin_range( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let end = eval_expr(end, context, scope, values)?; + eval_range_result(start, end, values) +} + +/// Builds an inclusive ascending or descending integer `range()` result. +pub(in crate::interpreter) fn eval_range_result( + start: RuntimeCellHandle, + end: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let end = eval_int_value(end, values)?; + let distance = if start <= end { + end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? + } else { + start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? + }; + let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let step = if start <= end { 1_i64 } else { -1_i64 }; + let mut current = start; + let mut result = values.array_new(count)?; + + for index in 0..count { + let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + let value = values.int(current)?; + result = values.array_set(result, key, value)?; + if index + 1 < count { + current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs new file mode 100644 index 0000000000..167b567f68 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `rsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "rsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `rsort` array mutator. +pub(in crate::interpreter) fn eval_rsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("rsort", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs new file mode 100644 index 0000000000..57b1ab7a49 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `shuffle`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "shuffle", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `shuffle` array mutator. +pub(in crate::interpreter) fn eval_shuffle_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("shuffle", values)?; + super::sort::eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs new file mode 100644 index 0000000000..a3d0b512e2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs @@ -0,0 +1,390 @@ +//! Purpose: +//! Declarative eval registry entry for `sort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "sort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `sort` array mutator. +pub(in crate::interpreter) fn eval_sort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("sort", values)?; + eval_array_sort_value_result(*array, values) +} + +/// Returns the dynamic callable result for by-value array ordering calls. +pub(in crate::interpreter) fn eval_array_sort_value_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) +} + +/// Sort key shape supported by eval's homogeneous array ordering implementation. +#[derive(Clone)] +pub(in crate::interpreter) enum EvalArraySortKey { + Numeric(f64), + Natural(Vec), + String(Vec), +} + +/// One source array entry plus its precomputed ordering key. +pub(in crate::interpreter) struct EvalArraySortEntry { + sort_key: EvalArraySortKey, + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut entries = match name { + "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, + "natsort" => eval_array_natural_sort_entries(array, false, values)?, + "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + "shuffle" => return eval_array_shuffle_replacement(array, values), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + entries.sort_by(|left, right| { + let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); + if matches!(name, "arsort" | "krsort" | "rsort") { + order.reverse() + } else { + order + } + }); + + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" + ) { + return eval_array_preserve_key_sort_result(entries, values); + } + eval_array_reindex_sort_result(entries, values) +} + +/// Builds a shuffled, reindexed replacement array for `shuffle()`. +pub(in crate::interpreter) fn eval_array_shuffle_replacement( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + entries.push(values.array_get(array, source_key)?); + } + + for index in (1..entries.len()).rev() { + let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; + entries.swap(index, swap_with); + } + + let mut result = values.array_new(entries.len())?; + for (index, value) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed result for `sort()` / `rsort()` after value ordering. +pub(in crate::interpreter) fn eval_array_reindex_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds a key-preserving associative result after value or key ordering. +pub(in crate::interpreter) fn eval_array_preserve_key_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Collects values and comparable value-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_value_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(value, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and natural-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_natural_sort_entries( + array: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and comparable key-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_key_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(source_key, values)?; + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Converts one scalar eval value into a natural-sort key. +pub(in crate::interpreter) fn eval_array_natural_sort_key( + value: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let mut bytes = values.string_bytes(value)?; + if case_insensitive { + bytes.make_ascii_lowercase(); + } + Ok(EvalArraySortKey::Natural(bytes)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts one scalar eval value into a homogeneous sort key. +pub(in crate::interpreter) fn eval_array_sort_key( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + match eval_array_numeric_string_sort_key(&bytes) { + Some(value) => Ok(EvalArraySortKey::Numeric(value)), + None => Ok(EvalArraySortKey::String(bytes)), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one PHP numeric string into the numeric sort domain when possible. +pub(in crate::interpreter) fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { + if !eval_is_numeric_string(bytes) { + return None; + } + std::str::from_utf8(bytes).ok()?.parse::().ok() +} + +/// Compares two precomputed eval sort keys. +pub(in crate::interpreter) fn eval_array_sort_key_cmp( + left: &EvalArraySortKey, + right: &EvalArraySortKey, +) -> std::cmp::Ordering { + match (left, right) { + (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + } + (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { + eval_natural_bytes_cmp(left, right) + } + (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), + _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), + } +} + +/// Returns a deterministic rank for mixed key-sort domains. +pub(in crate::interpreter) fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { + match key { + EvalArraySortKey::Numeric(_) => 0, + EvalArraySortKey::Natural(_) => 1, + EvalArraySortKey::String(_) => 2, + } +} + +/// Compares byte strings with a small PHP-style natural ordering. +pub(in crate::interpreter) fn eval_natural_bytes_cmp( + left: &[u8], + right: &[u8], +) -> std::cmp::Ordering { + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { + let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + if order != std::cmp::Ordering::Equal { + return order; + } + continue; + } + + let order = left[left_index].cmp(&right[right_index]); + if order != std::cmp::Ordering::Equal { + return order; + } + left_index += 1; + right_index += 1; + } + left.len().cmp(&right.len()) +} + +/// Compares two natural-sort digit runs and advances both byte indexes past them. +pub(in crate::interpreter) fn eval_natural_digit_run_cmp( + left: &[u8], + left_index: &mut usize, + right: &[u8], + right_index: &mut usize, +) -> std::cmp::Ordering { + let left_start = *left_index; + let right_start = *right_index; + while *left_index < left.len() && left[*left_index].is_ascii_digit() { + *left_index += 1; + } + while *right_index < right.len() && right[*right_index].is_ascii_digit() { + *right_index += 1; + } + + let left_digits = &left[left_start..*left_index]; + let right_digits = &right[right_start..*right_index]; + let left_trimmed = eval_trim_leading_zeroes(left_digits); + let right_trimmed = eval_trim_leading_zeroes(right_digits); + left_trimmed + .len() + .cmp(&right_trimmed.len()) + .then_with(|| left_trimmed.cmp(right_trimmed)) + .then_with(|| left_digits.len().cmp(&right_digits.len())) +} + +/// Drops leading zero bytes while keeping one zero for an all-zero digit run. +pub(in crate::interpreter) fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { + let trimmed = digits + .iter() + .position(|digit| *digit != b'0') + .map_or(&digits[digits.len().saturating_sub(1)..], |index| { + &digits[index..] + }); + if trimmed.is_empty() { + digits + } else { + trimmed + } +} + +/// Evaluates direct by-reference array ordering calls and writes back the array. +pub(in crate::interpreter) fn eval_array_sort_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target) = eval_array_sort_direct_arg(args, context, scope, values)?; + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} + +/// Extracts the writable array lvalue accepted by eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_direct_arg( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs new file mode 100644 index 0000000000..c197e70c9a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `uasort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "uasort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `uasort` array mutator. +pub(in crate::interpreter) fn eval_uasort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("uasort", values)?; + super::usort::eval_user_sort_value_result("uasort", *array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs new file mode 100644 index 0000000000..b66dff2a97 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Declarative eval registry entry for `uksort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "uksort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `uksort` array mutator. +pub(in crate::interpreter) fn eval_uksort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("uksort", values)?; + super::usort::eval_user_sort_value_result("uksort", *array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs new file mode 100644 index 0000000000..945e9b37ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs @@ -0,0 +1,244 @@ +//! Purpose: +//! Declarative eval registry entry for `usort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::super::*; + +eval_builtin! { + name: "usort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `usort` array mutator. +pub(in crate::interpreter) fn eval_usort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("usort", values)?; + eval_user_sort_value_result("usort", *array, *callback, context, values) +} + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +pub(in crate::interpreter) fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +pub(in crate::interpreter) struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +pub(in crate::interpreter) fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_user_sort_replacement_from_scope(name, array, callback, None, context, values) +} + +/// Builds the sorted replacement array with optional lexical scope for callback names. +pub(in crate::interpreter) fn eval_user_sort_replacement_from_scope( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +pub(in crate::interpreter) fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +pub(in crate::interpreter) fn eval_user_sort_entries_in_place( + name: &str, + callback: &EvaluatedCallable, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +pub(in crate::interpreter) fn eval_user_sort_compare( + name: &str, + callback: &EvaluatedCallable, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +pub(in crate::interpreter) fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +pub(in crate::interpreter) fn eval_user_sort_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + + let replacement = eval_user_sort_replacement_from_scope( + name, + array, + callback, + Some(scope), + context, + values, + )?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +pub(in crate::interpreter) fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array = Some(super::mutation::eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, target, callback)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs new file mode 100644 index 0000000000..3b7ab6a749 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Area-level evaluated-argument dispatch for array builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. +//! +//! Key details: +//! - Dispatch stays thin and routes every builtin through its leaf adapter. + +use super::super::super::*; + +/// Routes evaluated-argument array builtin calls through per-builtin leaf adapters. +pub(in crate::interpreter) fn eval_array_declared_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_sum" => super::array_sum::eval_array_sum_declared_values_result(evaluated_args, context, values), + "array_product" => super::array_product::eval_array_product_declared_values_result(evaluated_args, context, values), + "array_chunk" => super::array_chunk::eval_array_chunk_declared_values_result(evaluated_args, context, values), + "array_column" => super::array_column::eval_array_column_declared_values_result(evaluated_args, context, values), + "array_combine" => super::array_combine::eval_array_combine_declared_values_result(evaluated_args, context, values), + "array_diff" => super::array_diff::eval_array_diff_declared_values_result(evaluated_args, context, values), + "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_values_result(evaluated_args, context, values), + "array_fill" => super::array_fill::eval_array_fill_declared_values_result(evaluated_args, context, values), + "array_fill_keys" => super::array_fill_keys::eval_array_fill_keys_declared_values_result(evaluated_args, context, values), + "array_filter" => super::array_filter::eval_array_filter_declared_values_result(evaluated_args, context, values), + "array_intersect" => super::array_intersect::eval_array_intersect_declared_values_result(evaluated_args, context, values), + "array_intersect_key" => super::array_intersect_key::eval_array_intersect_key_declared_values_result(evaluated_args, context, values), + "array_map" => super::array_map::eval_array_map_declared_values_result(evaluated_args, context, values), + "array_merge" => super::array_merge::eval_array_merge_declared_values_result(evaluated_args, context, values), + "array_reduce" => super::array_reduce::eval_array_reduce_declared_values_result(evaluated_args, context, values), + "iterator_apply" => super::iterator_apply::eval_iterator_apply_declared_values_result(evaluated_args, context, values), + "iterator_count" => super::iterator_count::eval_iterator_count_declared_values_result(evaluated_args, context, values), + "iterator_to_array" => super::iterator_to_array::eval_iterator_to_array_declared_values_result(evaluated_args, context, values), + "array_flip" => super::array_flip::eval_array_flip_declared_values_result(evaluated_args, context, values), + "array_key_exists" => super::array_key_exists::eval_array_key_exists_declared_values_result(evaluated_args, context, values), + "array_pad" => super::array_pad::eval_array_pad_declared_values_result(evaluated_args, context, values), + "array_keys" => super::array_keys::eval_array_keys_declared_values_result(evaluated_args, context, values), + "array_rand" => super::array_rand::eval_array_rand_declared_values_result(evaluated_args, context, values), + "array_reverse" => super::array_reverse::eval_array_reverse_declared_values_result(evaluated_args, context, values), + "array_search" => super::array_search::eval_array_search_declared_values_result(evaluated_args, context, values), + "in_array" => super::in_array::eval_in_array_declared_values_result(evaluated_args, context, values), + "array_slice" => super::array_slice::eval_array_slice_declared_values_result(evaluated_args, context, values), + "array_unique" => super::array_unique::eval_array_unique_declared_values_result(evaluated_args, context, values), + "array_values" => super::array_values::eval_array_values_declared_values_result(evaluated_args, context, values), + "count" => super::count::eval_count_declared_values_result(evaluated_args, context, values), + "range" => super::range::eval_range_declared_values_result(evaluated_args, context, values), + "array_walk" => super::array_walk::eval_array_walk_declared_values_result(evaluated_args, context, values), + "array_pop" => super::array_pop::eval_array_pop_declared_values_result(evaluated_args, context, values), + "array_shift" => super::array_shift::eval_array_shift_declared_values_result(evaluated_args, context, values), + "array_push" => super::array_push::eval_array_push_declared_values_result(evaluated_args, context, values), + "array_unshift" => super::array_unshift::eval_array_unshift_declared_values_result(evaluated_args, context, values), + "array_splice" => super::array_splice::eval_array_splice_declared_values_result(evaluated_args, context, values), + "arsort" => super::arsort::eval_arsort_declared_values_result(evaluated_args, context, values), + "asort" => super::asort::eval_asort_declared_values_result(evaluated_args, context, values), + "krsort" => super::krsort::eval_krsort_declared_values_result(evaluated_args, context, values), + "ksort" => super::ksort::eval_ksort_declared_values_result(evaluated_args, context, values), + "natcasesort" => super::natcasesort::eval_natcasesort_declared_values_result(evaluated_args, context, values), + "natsort" => super::natsort::eval_natsort_declared_values_result(evaluated_args, context, values), + "rsort" => super::rsort::eval_rsort_declared_values_result(evaluated_args, context, values), + "shuffle" => super::shuffle::eval_shuffle_declared_values_result(evaluated_args, context, values), + "sort" => super::sort::eval_sort_declared_values_result(evaluated_args, context, values), + "uasort" => super::uasort::eval_uasort_declared_values_result(evaluated_args, context, values), + "uksort" => super::uksort::eval_uksort_declared_values_result(evaluated_args, context, values), + "usort" => super::usort::eval_usort_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs new file mode 100644 index 0000000000..9405c84998 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -0,0 +1,210 @@ +//! Purpose: +//! Hosts shared class metadata helpers for eval symbol builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Focused symbol builtin files own PHP-visible declarations and eval behavior. +//! - These helpers centralize class-name normalization, visibility metadata, +//! and eval/runtime class-like existence checks. + +use super::super::*; +use std::collections::HashSet; + +const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; +const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; +const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; + +/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. +pub(in crate::interpreter) fn eval_class_metadata_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, bool), EvalStatus> { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => Ok(( + eval_object_class_metadata_name(target, context, values)?, + true, + )), + EVAL_TAG_STRING => Ok(( + eval_resolved_class_metadata_name(target, context, values)?, + false, + )), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves an object cell to its eval or runtime class name. +pub(in crate::interpreter) fn eval_object_class_metadata_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + let class_name = values.object_class_name(object)?; + let class_name_bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Reads a class-name cell and applies eval alias resolution. +pub(in crate::interpreter) fn eval_resolved_class_metadata_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_class_metadata_name(name, values)?; + Ok(context.resolve_class_name(&name).unwrap_or(name)) +} + +/// Returns whether one eval or generated/AOT class name is the same as or extends another. +pub(in crate::interpreter) fn eval_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + eval_same_class_metadata_name(class_name, target) + || context.class_is_a(class_name, target, false) + || eval_native_class_metadata_is_a(class_name, target, context) +} + +/// Returns whether generated/AOT parent metadata proves one class extends another. +fn eval_native_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + let target = target.trim_start_matches('\\'); + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if eval_same_class_metadata_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} + +/// Returns whether two PHP class names refer to the same normalized metadata name. +pub(in crate::interpreter) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns access metadata for one generated/AOT method name, if reflection exposes it. +pub(in crate::interpreter) fn eval_runtime_method_access_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + ))) +} + +/// Returns access metadata for one generated/AOT property name, if reflection exposes it. +pub(in crate::interpreter) fn eval_runtime_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(class_name, property_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, + ))) +} + +/// Converts generated/AOT reflection member flags into eval visibility metadata. +fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Builds an indexed PHP array from owned Rust strings. +pub(in crate::interpreter) fn eval_indexed_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Copies a runtime string array into Rust-owned strings for class metadata helpers. +pub(in crate::interpreter) fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} + +/// Returns whether one normalized class-like name exists in eval or runtime metadata. +pub(in crate::interpreter) fn eval_class_relation_name_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + { + return Ok(true); + } + values.enum_exists(name) +} + +/// Reads and normalizes one class metadata string argument. +pub(in crate::interpreter) fn eval_class_metadata_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs new file mode 100644 index 0000000000..67f08874c1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and wrapper implementation for `call_user_func`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Callable normalization and invocation stay in `registry::callable` because +//! those helpers are shared by ordinary dynamic calls, arrays, reflection, and +//! `call_user_func_array`. + +use super::super::super::*; +use super::super::registry::eval_call_user_func_with_values_from_scope; + +eval_builtin! { + name: "call_user_func", + area: Core, + params: [callback], + variadic: args, + direct: Core, + values: Core, +} + +/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let release_callback = eval_call_user_func_callback_expr_is_temporary(&args[0]); + let mut evaluated_args = Vec::with_capacity(args.len()); + for (index, arg) in args.iter().enumerate() { + let value = match eval_expr(arg, context, scope, values) { + Ok(value) => value, + Err(status) => { + if index > 0 && release_callback { + values.release(evaluated_args[0])?; + } + return Err(status); + } + }; + evaluated_args.push(value); + } + let callback = evaluated_args[0]; + let result = + eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values); + if release_callback { + values.release(callback)?; + } + result +} + +/// Dispatches `call_user_func` after its callback and arguments are already evaluated. +pub(in crate::interpreter) fn eval_call_user_func_with_values( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_with_values_from_scope(evaluated_args, None, context, values) +} + +/// Returns whether a `call_user_func*` callback expression allocates a temporary cell. +pub(in crate::interpreter) fn eval_call_user_func_callback_expr_is_temporary( + callback: &EvalExpr, +) -> bool { + matches!(callback, EvalExpr::Const(_)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs new file mode 100644 index 0000000000..1b40006a82 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Eval registry entry and wrapper implementation for `call_user_func_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Callable normalization and invocation stay in `registry::callable` because +//! the callable engine is shared beyond this builtin. + +use super::call_user_func::eval_call_user_func_callback_expr_is_temporary; +use super::super::super::*; +use super::super::registry::eval_call_user_func_array_with_values_from_scope; + +eval_builtin! { + name: "call_user_func_array", + area: Core, + params: [callback, args], + direct: Core, + values: Core, +} + +/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, arg_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let release_callback = eval_call_user_func_callback_expr_is_temporary(callback); + let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); + let callback = eval_expr(callback, context, scope, values)?; + let arg_array = match eval_expr(arg_array, context, scope, values) { + Ok(arg_array) => arg_array, + Err(status) => { + if release_callback { + values.release(callback)?; + } + return Err(status); + } + }; + let result = eval_call_user_func_array_with_values_from_scope( + callback, + arg_array, + Some(scope), + context, + values, + ); + if release_arg_array { + values.release(arg_array)?; + } + if release_callback { + values.release(callback)?; + } + result +} + +/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. +pub(in crate::interpreter) fn eval_call_user_func_array_with_values( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_array_with_values_from_scope(callback, arg_array, None, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/define.rs b/crates/elephc-magician/src/interpreter/builtins/core/define.rs new file mode 100644 index 0000000000..887a876dea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/define.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Eval registry entry and implementation for `define`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Dynamic constants are stored in the eval context after predefined-constant +//! checks and keep PHP's case-sensitive dynamic constant names. + +use super::super::super::*; + +eval_builtin! { + name: "define", + area: Core, + params: [constant_name, value], + direct: Core, + values: Core, +} + +/// Evaluates `define(name, value)` for eval dynamic constant-name registration. +pub(in crate::interpreter) fn eval_builtin_define( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `define(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_define_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let defined = eval_define_name(*name, *value, context, values)?; + values.bool_value(defined) +} + +/// Normalizes and registers one eval dynamic constant name. +fn eval_define_name( + name: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + if name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { + values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; + return Ok(false); + } + let value = values.retain(value)?; + if context.define_constant(&name, value) { + Ok(true) + } else { + values.release(value)?; + Ok(false) + } +} + +/// Reads a PHP constant name from a runtime cell without changing case. +pub(in crate::interpreter) fn eval_constant_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/defined.rs b/crates/elephc-magician/src/interpreter/builtins/core/defined.rs new file mode 100644 index 0000000000..70d8b71944 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/defined.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Eval registry entry and implementation for `defined`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Dynamic names use `define`'s constant-name normalizer so the two builtins +//! stay in lockstep. + +use super::define::eval_constant_name; +use super::super::super::*; + +eval_builtin! { + name: "defined", + area: Core, + params: [constant_name], + direct: Core, + values: Core, +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +pub(in crate::interpreter) fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and probes one eval dynamic constant name. +fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/die.rs b/crates/elephc-magician/src/interpreter/builtins/core/die.rs new file mode 100644 index 0000000000..cb5bdf60e5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/die.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Eval registry entry and alias implementation for `die`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - `die` shares `exit`'s process-status coercion and process termination. + +use super::exit::{eval_exit_status_value, eval_process_exit}; +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "die", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} + +/// Evaluates direct `die` calls from unevaluated EvalIR arguments. +pub(in crate::interpreter) fn eval_builtin_die( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match args { + [] => 0, + [status] => { + let status = eval_expr(status, context, scope, values)?; + eval_int_value(status, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Evaluates by-value `die` calls from already materialized arguments. +pub(in crate::interpreter) fn eval_die_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let status = eval_exit_status_value(evaluated_args, values)?; + eval_process_exit(status) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/exit.rs b/crates/elephc-magician/src/interpreter/builtins/core/exit.rs new file mode 100644 index 0000000000..5efb24e7f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/exit.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and implementation for `exit`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - The helper terminates the host process to match elephc's compiled behavior. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "exit", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} + +/// Evaluates direct `exit` calls from unevaluated EvalIR arguments. +pub(in crate::interpreter) fn eval_builtin_exit( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match args { + [] => 0, + [status] => { + let status = eval_expr(status, context, scope, values)?; + eval_int_value(status, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Evaluates by-value `exit` calls from already materialized arguments. +pub(in crate::interpreter) fn eval_exit_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let status = eval_exit_status_value(evaluated_args, values)?; + eval_process_exit(status) +} + +/// Reads the optional PHP integer process status for `exit` and `die`. +pub(in crate::interpreter) fn eval_exit_status_value( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => Ok(0), + [status] => eval_int_value(*status, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Terminates the current process with the PHP integer status clamped to `i32`. +pub(in crate::interpreter) fn eval_process_exit( + status: i64, +) -> Result { + let status = i32::try_from(status).unwrap_or_else(|_| { + if status.is_negative() { + i32::MIN + } else { + i32::MAX + } + }); + std::process::exit(status) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs new file mode 100644 index 0000000000..6d6f431cae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs @@ -0,0 +1,79 @@ +//! Purpose: +//! Orchestrates eval metadata and implementations for core callable, constant, +//! process-control, and debug-output builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by registry hooks. +//! +//! Key details: +//! - Leaf builtin files own their declarations and builtin-specific wrappers. +//! - The callable dispatch engine remains shared because it is used by more than +//! `call_user_func*`. + +use super::super::*; + +mod call_user_func; +mod call_user_func_array; +mod define; +mod defined; +mod die; +mod exit; +mod print_r; +mod var_dump; + +pub(in crate::interpreter) use call_user_func::*; +pub(in crate::interpreter) use call_user_func_array::*; +pub(in crate::interpreter) use define::*; +pub(in crate::interpreter) use defined::*; +pub(in crate::interpreter) use die::*; +pub(in crate::interpreter) use exit::*; +pub(in crate::interpreter) use print_r::*; +pub(in crate::interpreter) use var_dump::*; + +/// Dispatches direct expression-level calls for core builtins. +pub(in crate::interpreter) fn eval_builtin_core_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), + "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "define" => eval_builtin_define(args, context, scope, values), + "defined" => eval_builtin_defined(args, context, scope, values), + "die" => eval_builtin_die(args, context, scope, values), + "exit" => eval_builtin_exit(args, context, scope, values), + "print_r" => eval_builtin_print_r(args, context, scope, values), + "var_dump" => eval_builtin_var_dump(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for core builtins. +pub(in crate::interpreter) fn eval_core_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => { + eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + } + "define" => eval_define_result(evaluated_args, context, values), + "defined" => eval_defined_result(evaluated_args, context, values), + "die" => eval_die_values_result(evaluated_args, values), + "exit" => eval_exit_values_result(evaluated_args, values), + "print_r" => eval_print_r_result(evaluated_args, context, values), + "var_dump" => eval_var_dump_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs b/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs new file mode 100644 index 0000000000..fda4b6fe0c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs @@ -0,0 +1,421 @@ +//! Purpose: +//! Eval registry entry and implementation for `print_r` plus shared debug-output helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::core` direct and by-value dispatch. +//! - `crate::interpreter::builtins::core::var_dump` for shared object metadata rendering. +//! +//! Key details: +//! - `print_r($value, true)` returns captured output instead of echoing it. +//! - Object metadata helpers stay here so related debug builtins can reuse one +//! PHP-visible object traversal model without a generic implementation bucket. + +use std::collections::HashSet; + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "print_r", + area: Core, + params: [value, r#return = EvalBuiltinDefaultValue::Bool(false)], + direct: Core, + values: Core, +} + + +/// Property visibility rendered by `var_dump()` and `print_r()` object output. +#[derive(Clone)] +pub(in crate::interpreter) struct EvalDebugPropertyVisibility { + pub(in crate::interpreter) kind: EvalDebugPropertyVisibilityKind, +} + +/// Concrete PHP visibility shape for one object property key. +#[derive(Clone)] +pub(in crate::interpreter) enum EvalDebugPropertyVisibilityKind { + Public, + Protected, + Private(String), +} + +/// Object property entry collected before rendering object headers. +#[derive(Clone)] +pub(in crate::interpreter) struct EvalDebugObjectProperty { + pub(in crate::interpreter) name: String, + pub(in crate::interpreter) visibility: EvalDebugPropertyVisibility, + pub(in crate::interpreter) value: RuntimeCellHandle, + pub(in crate::interpreter) is_reference: bool, +} + +/// Evaluates PHP `print_r()` over one value and an optional return flag. + +pub(in crate::interpreter) fn eval_builtin_print_r( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let value = eval_expr(&args[0], context, scope, values)?; + let return_output = match args.get(1) { + Some(arg) => { + let flag = eval_expr(arg, context, scope, values)?; + values.truthy(flag)? + } + None => false, + }; + eval_print_r_value_result(value, return_output, context, values) +} + +/// Evaluates already materialized `print_r()` arguments. +pub(in crate::interpreter) fn eval_print_r_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let return_output = match args.get(1) { + Some(flag) => values.truthy(*flag)?, + None => false, + }; + eval_print_r_value_result(args[0], return_output, context, values) +} + +/// Renders, echoes, or returns one `print_r()` output string. +fn eval_print_r_value_result( + value: RuntimeCellHandle, + return_output: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + let mut objects_seen = Vec::new(); + eval_print_r_append_value( + value, + context, + values, + 0, + &mut arrays_seen, + &mut objects_seen, + &mut output, + )?; + let output = values.string_bytes_value(&output)?; + if return_output { + Ok(output) + } else { + values.echo(output)?; + values.bool_value(true) + } +} + +/// Appends one value to a `print_r()` byte buffer. +fn eval_print_r_append_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + eval_print_r_append_array(value, context, values, depth, arrays_seen, objects_seen, output) + } + EVAL_TAG_OBJECT => { + eval_print_r_append_object(value, context, values, depth, arrays_seen, objects_seen, output) + } + _ => { + output.extend_from_slice(&values.string_bytes(value)?); + Ok(()) + } + } +} + +/// Appends one array in PHP `print_r()` style. +fn eval_print_r_append_array( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + output.extend_from_slice(b"*RECURSION*"); + return Ok(()); + } + arrays_seen.push(address); + output.extend_from_slice(b"Array\n"); + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b"(\n"); + let len = values.array_len(value)?; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_print_r_append_indent(depth + 1, output); + output.extend_from_slice(b"["); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"] => "); + eval_print_r_append_value( + element, + context, + values, + depth + 1, + arrays_seen, + objects_seen, + output, + )?; + output.extend_from_slice(b"\n"); + } + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b")\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one object in PHP `print_r()` style. +fn eval_print_r_append_object( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let identity = eval_debug_object_identity(value, values); + let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; + if objects_seen.contains(&object_key) { + output.extend_from_slice(b"*RECURSION*"); + return Ok(()); + } + objects_seen.push(object_key); + let class_name = eval_debug_object_class_name(value, identity, context, values)?; + let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b" Object\n"); + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b"(\n"); + for property in &properties { + eval_print_r_append_indent(depth + 1, output); + eval_print_r_append_object_key(property, output); + output.extend_from_slice(b" => "); + eval_print_r_append_value( + property.value, + context, + values, + depth + 1, + arrays_seen, + objects_seen, + output, + )?; + output.extend_from_slice(b"\n"); + } + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b")\n"); + objects_seen.pop(); + Ok(()) +} + +/// Appends one object property key for `print_r()`. +fn eval_print_r_append_object_key(property: &EvalDebugObjectProperty, output: &mut Vec) { + output.extend_from_slice(b"["); + output.extend_from_slice(property.name.as_bytes()); + match &property.visibility.kind { + EvalDebugPropertyVisibilityKind::Public => {} + EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), + EvalDebugPropertyVisibilityKind::Private(class_name) => { + output.extend_from_slice(b":"); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b":private"); + } + } + output.extend_from_slice(b"]"); +} + +/// Appends the four-space indentation used by PHP `print_r()`. +fn eval_print_r_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + +/// Returns an object identity without turning non-object-like values into fatals. +pub(in crate::interpreter) fn eval_debug_object_identity( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Option { + values.object_identity(value).ok() +} + +/// Resolves the PHP-visible class name for one object value. +pub(in crate::interpreter) fn eval_debug_object_class_name( + value: RuntimeCellHandle, + identity: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(identity) = identity { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().trim_start_matches('\\').to_string()); + } + } + let class_name = values.object_class_name(value)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + String::from_utf8(trim_leading_namespace_separator(&bytes).to_vec()) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Collects object properties visible to debug-output rendering. +pub(in crate::interpreter) fn eval_debug_object_properties( + object: RuntimeCellHandle, + identity: Option, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(identity) = identity { + if context.dynamic_object_class(identity).is_some() { + return eval_debug_dynamic_object_properties(object, identity, class_name, context, values); + } + } + eval_debug_public_object_properties(object, values) +} + +/// Collects eval-declared object properties plus public dynamic properties. +fn eval_debug_dynamic_object_properties( + object: RuntimeCellHandle, + identity: u64, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut properties = Vec::new(); + let mut storage_keys = HashSet::new(); + let mut emitted_public_names = HashSet::new(); + + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() { + continue; + } + let storage_name = eval_instance_property_storage_name(class.name(), property); + storage_keys.insert(storage_name.clone()); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_name) + { + continue; + } + let alias = context.dynamic_property_alias(identity, &storage_name).cloned(); + let value = match &alias { + Some(target) => eval_reference_target_value(target, context, values)?, + None => values.property_get(object, &storage_name)?, + }; + if property.visibility() == EvalVisibility::Public { + emitted_public_names.insert(property.name().to_string()); + } + properties.push(EvalDebugObjectProperty { + name: property.name().to_string(), + visibility: eval_debug_property_visibility(class.name(), property.visibility()), + value, + is_reference: alias.is_some(), + }); + } + } + + eval_debug_append_dynamic_public_properties( + object, + &storage_keys, + &emitted_public_names, + &mut properties, + values, + )?; + Ok(properties) +} + +/// Appends dynamic public properties stored directly on an eval object. +fn eval_debug_append_dynamic_public_properties( + object: RuntimeCellHandle, + storage_keys: &HashSet, + emitted_public_names: &HashSet, + properties: &mut Vec, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key)?; + values.release(key)?; + let key_name = String::from_utf8(key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || emitted_public_names.contains(&key_name) + { + continue; + } + let value = values.property_get(object, &key_name)?; + properties.push(EvalDebugObjectProperty { + name: key_name, + visibility: EvalDebugPropertyVisibility { + kind: EvalDebugPropertyVisibilityKind::Public, + }, + value, + is_reference: false, + }); + } + Ok(()) +} + +/// Collects public bridge-visible properties for non-eval runtime objects. +fn eval_debug_public_object_properties( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let property_count = values.object_property_len(object)?; + let mut properties = Vec::with_capacity(property_count); + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key)?; + values.release(key)?; + let key_name = String::from_utf8(key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = values.property_get(object, &key_name)?; + properties.push(EvalDebugObjectProperty { + name: key_name, + visibility: EvalDebugPropertyVisibility { + kind: EvalDebugPropertyVisibilityKind::Public, + }, + value, + is_reference: false, + }); + } + Ok(properties) +} + +/// Converts eval visibility metadata into debug-output key metadata. +fn eval_debug_property_visibility( + declaring_class: &str, + visibility: EvalVisibility, +) -> EvalDebugPropertyVisibility { + let kind = match visibility { + EvalVisibility::Public => EvalDebugPropertyVisibilityKind::Public, + EvalVisibility::Protected => EvalDebugPropertyVisibilityKind::Protected, + EvalVisibility::Private => { + EvalDebugPropertyVisibilityKind::Private(declaring_class.trim_start_matches('\\').to_string()) + } + }; + EvalDebugPropertyVisibility { kind } +} + +/// Removes a leading PHP namespace separator from a runtime class-name byte slice. +fn trim_leading_namespace_separator(bytes: &[u8]) -> &[u8] { + bytes.strip_prefix(b"\\").unwrap_or(bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs b/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs new file mode 100644 index 0000000000..05cbfeeee9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs @@ -0,0 +1,328 @@ +//! Purpose: +//! Eval registry entry and implementation for `var_dump`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core` direct and by-value dispatch. +//! +//! Key details: +//! - The formatter preserves PHP scalar, array, object, reference, and recursion output shape. +//! - Shared debug object metadata traversal is owned by `print_r` and reused here. + +use super::print_r::{ + eval_debug_object_class_name, eval_debug_object_identity, eval_debug_object_properties, + EvalDebugObjectProperty, EvalDebugPropertyVisibilityKind, +}; +use super::super::super::*; + +eval_builtin! { + name: "var_dump", + area: Core, + params: [value], + variadic: values, + direct: Core, + values: Core, +} + +/// Evaluates PHP `var_dump()` over one or more eval expressions and returns null. +pub(in crate::interpreter) fn eval_builtin_var_dump( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_var_dump_result(&evaluated_args, context, values) +} + +/// Emits already materialized values using PHP-style `var_dump()` debug formatting. +pub(in crate::interpreter) fn eval_var_dump_result( + values_to_dump: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values_to_dump.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + let mut objects_seen = Vec::new(); + for value in values_to_dump { + eval_var_dump_append_value( + *value, + context, + values, + 0, + false, + &mut arrays_seen, + &mut objects_seen, + &mut output, + )?; + } + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.null() +} + +/// Appends one value and its nested entries to a `var_dump()` byte buffer. +fn eval_var_dump_append_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => { + eval_var_dump_append_scalar(b"int", value, values, depth, is_reference, output) + } + EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, is_reference, output), + EVAL_TAG_FLOAT => { + eval_var_dump_append_scalar(b"float", value, values, depth, is_reference, output) + } + EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, is_reference, output), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => eval_var_dump_append_array( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), + EVAL_TAG_OBJECT => eval_var_dump_append_object( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), + EVAL_TAG_NULL => { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"NULL\n"); + Ok(()) + } + EVAL_TAG_RESOURCE => { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"resource(0) of type (stream)\n"); + Ok(()) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends one integer-like or float-like `var_dump()` scalar line. +fn eval_var_dump_append_scalar( + label: &[u8], + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(label); + output.extend_from_slice(b"("); + output.extend_from_slice(&values.string_bytes(value)?); + output.extend_from_slice(b")\n"); + Ok(()) +} + +/// Appends one string `var_dump()` line while preserving raw PHP string bytes. +fn eval_var_dump_append_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let bytes = values.string_bytes(value)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"string("); + output.extend_from_slice(bytes.len().to_string().as_bytes()); + output.extend_from_slice(b") \""); + output.extend_from_slice(&bytes); + output.extend_from_slice(b"\"\n"); + Ok(()) +} + +/// Appends one boolean `var_dump()` line from PHP truthiness. +fn eval_var_dump_append_bool( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_prefix(depth, is_reference, output); + if values.truthy(value)? { + output.extend_from_slice(b"bool(true)\n"); + } else { + output.extend_from_slice(b"bool(false)\n"); + } + Ok(()) +} + +/// Appends one array shell and recursively emits foreach-visible entries. +fn eval_var_dump_append_array( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + arrays_seen.push(address); + let len = values.array_len(value)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"array("); + output.extend_from_slice(len.to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_var_dump_append_array_key(key, values, depth + 1, output)?; + eval_var_dump_append_value( + element, + context, + values, + depth + 1, + false, + arrays_seen, + objects_seen, + output, + )?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one object shell and its collected properties. +fn eval_var_dump_append_object( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let identity = eval_debug_object_identity(value, values); + let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; + if objects_seen.contains(&object_key) { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + objects_seen.push(object_key); + let class_name = eval_debug_object_class_name(value, identity, context, values)?; + let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"object("); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b")#"); + output.extend_from_slice(object_key.to_string().as_bytes()); + output.extend_from_slice(b" ("); + output.extend_from_slice(properties.len().to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for property in &properties { + eval_var_dump_append_object_key(property, depth + 1, output); + eval_var_dump_append_value( + property.value, + context, + values, + depth + 1, + property.is_reference, + arrays_seen, + objects_seen, + output, + )?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + objects_seen.pop(); + Ok(()) +} + +/// Appends one array key line for an indexed or associative `var_dump()` entry. +fn eval_var_dump_append_array_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"["); + match values.type_tag(key)? { + EVAL_TAG_STRING => { + output.extend_from_slice(b"\""); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"\""); + } + _ => output.extend_from_slice(&values.string_bytes(key)?), + } + output.extend_from_slice(b"]=>\n"); + Ok(()) +} + +/// Appends one object property key line for `var_dump()`. +fn eval_var_dump_append_object_key( + property: &EvalDebugObjectProperty, + depth: usize, + output: &mut Vec, +) { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"[\""); + output.extend_from_slice(property.name.as_bytes()); + output.extend_from_slice(b"\""); + match &property.visibility.kind { + EvalDebugPropertyVisibilityKind::Public => {} + EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), + EvalDebugPropertyVisibilityKind::Private(class_name) => { + output.extend_from_slice(b":\""); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b"\":private"); + } + } + output.extend_from_slice(b"]=>\n"); +} + +/// Appends one `var_dump()` line prefix, including a reference marker when needed. +fn eval_var_dump_append_prefix(depth: usize, is_reference: bool, output: &mut Vec) { + eval_var_dump_append_indent(depth, output); + if is_reference { + output.extend_from_slice(b"&"); + } +} + +/// Appends the two-space indentation used by PHP `var_dump()` arrays and objects. +fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs new file mode 100644 index 0000000000..5613408937 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Declarative eval registry entry for `basename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the path helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "basename", + area: Filesystem, + params: [path, suffix = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `basename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_basename_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_basename(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `basename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_basename_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_basename_result(*path, None, values), + [path, suffix] => eval_basename_result(*path, Some(*suffix), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_basename( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_basename_result(path, None, values) + } + [path, suffix] => { + let path = eval_expr(path, context, scope, values)?; + let suffix = eval_expr(suffix, context, scope, values)?; + eval_basename_result(path, Some(suffix), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `basename()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_basename_result( + path: RuntimeCellHandle, + suffix: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let suffix = suffix + .map(|suffix| values.string_bytes(suffix)) + .transpose()?; + let result = eval_basename_bytes(&path, suffix.as_deref()); + values.string_bytes_value(&result) +} + +/// Extracts a PHP basename from one path byte string. +pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return Vec::new(); + } + let mut start = end; + while start > 0 && path[start - 1] != b'/' { + start -= 1; + } + let mut result = path[start..end].to_vec(); + if let Some(suffix) = suffix { + if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { + result.truncate(result.len() - suffix.len()); + } + } + result +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs new file mode 100644 index 0000000000..49938bd93d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Declarative eval registry entry for `chdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "chdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `chdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_unary_path_bool("chdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_unary_path_bool_result("chdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, context, values) +} + +/// Executes a one-path filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + if let Some(result) = eval_user_wrapper_single_path_op_result(name, &path, context, values)? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs new file mode 100644 index 0000000000..b01acb2932 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `chgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "chgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `chgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chgrp_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::chown::eval_builtin_chown_like("chgrp", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chgrp_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("chgrp", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs new file mode 100644 index 0000000000..0f580a1f8f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Declarative eval registry entry for `chmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the chmod helper. + +eval_builtin! { + name: "chmod", + area: Filesystem, + params: [filename, permissions], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `chmod` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chmod_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_chmod(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chmod` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chmod_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, context, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let metadata_value = values.int(i64::from(mode))?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_ACCESS, + metadata_value, + context, + values, + )? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs new file mode 100644 index 0000000000..ad33ac4f01 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Declarative eval registry entry for `chown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "chown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use std::ffi::CString; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `chown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_chown_like("chown", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, principal] => eval_chown_like_result("chown", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP ownership/group path mutation builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chown_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, principal] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let principal = eval_expr(principal, context, scope, values)?; + eval_chown_like_result(name, filename, principal, context, values) +} + +/// Changes one local path owner or group and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chown_like_result( + name: &str, + filename: RuntimeCellHandle, + principal: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if matches!(name, "chown" | "chgrp") { + let (option, metadata_value) = + eval_chown_metadata_arg(name, principal, values)?; + if let Some(result) = + eval_user_wrapper_stream_metadata_result(&path, option, metadata_value, context, values)? + { + return Ok(result); + } + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let Some(path) = eval_c_string(&path) else { + return values.bool_value(false); + }; + let Some((uid, gid)) = eval_chown_principal_ids(name, principal, values)? else { + return values.bool_value(false); + }; + let status = unsafe { + match name { + "chown" | "chgrp" => libc::chown(path.as_ptr(), uid, gid), + "lchown" | "lchgrp" => libc::lchown(path.as_ptr(), uid, gid), + _ => return Err(EvalStatus::RuntimeFatal), + } + }; + values.bool_value(status == 0) +} + +/// Builds the wrapper metadata option and value for `chown()` or `chgrp()`. +fn eval_chown_metadata_arg( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(i64, RuntimeCellHandle), EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_OWNER, values.int(principal)?)) + } + ("chgrp", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_GROUP, values.int(principal)?)) + } + ("chown", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_OWNER_NAME, principal)), + ("chgrp", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_GROUP_NAME, principal)), + ("chown" | "chgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves one PHP owner/group argument into libc uid/gid slots. +fn eval_chown_principal_ids( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown" | "lchown", EVAL_TAG_INT) => { + Ok(Some(( + eval_int_value(principal, values)? as libc::uid_t, + !0 as libc::gid_t, + ))) + } + ("chgrp" | "lchgrp", EVAL_TAG_INT) => { + Ok(Some(( + !0 as libc::uid_t, + eval_int_value(principal, values)? as libc::gid_t, + ))) + } + ("chown" | "lchown", EVAL_TAG_STRING) => { + Ok(eval_owner_name_id(principal, values)?.map(|uid| (uid, !0 as libc::gid_t))) + } + ("chgrp" | "lchgrp", EVAL_TAG_STRING) => { + Ok(eval_group_name_id(principal, values)?.map(|gid| (!0 as libc::uid_t, gid))) + } + ("chown" | "chgrp" | "lchown" | "lchgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a PHP user-name cell to a libc uid. +fn eval_owner_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let passwd = unsafe { libc::getpwnam(name.as_ptr()) }; + if passwd.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*passwd).pw_uid })) + } +} + +/// Resolves a PHP group-name cell to a libc gid. +fn eval_group_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let group = unsafe { libc::getgrnam(name.as_ptr()) }; + if group.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*group).gr_gid })) + } +} + +/// Converts a Rust path string into a C string, rejecting embedded NUL bytes. +fn eval_c_string(value: &str) -> Option { + CString::new(value).ok() +} + +/// Converts raw PHP bytes into a C string, rejecting embedded NUL bytes. +fn eval_c_bytes(value: &[u8]) -> Option { + CString::new(value).ok() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs new file mode 100644 index 0000000000..a8194d0bac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Declarative eval registry entry for `clearstatcache`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through eval's ordered no-op stat-cache helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "clearstatcache", + area: Filesystem, + params: [ + clear_realpath_cache = EvalBuiltinDefaultValue::Bool(false), + filename = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `clearstatcache` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_clearstatcache_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_clearstatcache(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `clearstatcache` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_clearstatcache_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null() +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +pub(in crate::interpreter) fn eval_builtin_clearstatcache( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs new file mode 100644 index 0000000000..fceb6f6cc4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs @@ -0,0 +1,104 @@ +//! Purpose: +//! Declarative eval registry entry for `closedir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource close helper. + +eval_builtin! { + name: "closedir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `closedir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_closedir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_unary_directory("closedir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `closedir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_closedir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [dir_handle] => eval_unary_directory_result("closedir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP directory handle builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unary_directory( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [dir_handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let dir_handle = eval_expr(dir_handle, context, scope, values)?; + eval_unary_directory_result(name, dir_handle, context, values) +} + +/// Evaluates a materialized directory handle builtin argument. +pub(in crate::interpreter) fn eval_unary_directory_result( + name: &str, + dir_handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_directory_resource_id(dir_handle, values)?; + match name { + "closedir" => { + if let Some(result) = eval_user_wrapper_closedir_result(id, context, values)? { + return Ok(result); + } + context.stream_resources_mut().close_directory(id); + values.null() + } + "readdir" => { + if let Some(result) = eval_user_wrapper_readdir_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read_directory(id) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } + } + "rewinddir" => { + if let Some(result) = eval_user_wrapper_rewinddir_result(id, context, values)? { + return Ok(result); + } + context.stream_resources_mut().rewind_directory(id); + values.null() + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based directory id. +fn eval_directory_resource_id( + dir_handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(dir_handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(dir_handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs new file mode 100644 index 0000000000..2e762e9257 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Declarative eval registry entry for `copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "copy", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `copy` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_copy_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_binary_path_bool("copy", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `copy` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_copy_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [from, to] => eval_binary_path_bool_result("copy", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, context, values) +} + +/// Executes a two-path filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + if name == "rename" { + if let Some(result) = eval_user_wrapper_rename_result(&from, &to, context, values)? { + return Ok(result); + } + } + let Some(from) = stream_wrappers::local_filesystem_path(&from) else { + return values.bool_value(false); + }; + let Some(to) = stream_wrappers::local_filesystem_path(&to) else { + return values.bool_value(false); + }; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs new file mode 100644 index 0000000000..ce4dbb5154 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Direct expression-level dispatch for filesystem builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. +//! +//! Key details: +//! - This dispatcher keeps filesystem call lowering area-scoped while per-builtin +//! metadata lives in individual declaration files. + +use super::super::super::*; + +/// Routes direct expression-level filesystem builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_builtin_filesystem_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), + "chdir" => super::chdir::eval_chdir_declared_call(args, context, scope, values), + "chgrp" => super::chgrp::eval_chgrp_declared_call(args, context, scope, values), + "chmod" => super::chmod::eval_chmod_declared_call(args, context, scope, values), + "chown" => super::chown::eval_chown_declared_call(args, context, scope, values), + "clearstatcache" => super::clearstatcache::eval_clearstatcache_declared_call(args, context, scope, values), + "closedir" => super::closedir::eval_closedir_declared_call(args, context, scope, values), + "copy" => super::copy::eval_copy_declared_call(args, context, scope, values), + "dirname" => super::dirname::eval_dirname_declared_call(args, context, scope, values), + "disk_free_space" => super::disk_free_space::eval_disk_free_space_declared_call(args, context, scope, values), + "disk_total_space" => super::disk_total_space::eval_disk_total_space_declared_call(args, context, scope, values), + "fclose" => super::fclose::eval_fclose_declared_call(args, context, scope, values), + "fdatasync" => super::fdatasync::eval_fdatasync_declared_call(args, context, scope, values), + "feof" => super::feof::eval_feof_declared_call(args, context, scope, values), + "fflush" => super::fflush::eval_fflush_declared_call(args, context, scope, values), + "fgetc" => super::fgetc::eval_fgetc_declared_call(args, context, scope, values), + "fgetcsv" => super::fgetcsv::eval_fgetcsv_declared_call(args, context, scope, values), + "fgets" => super::fgets::eval_fgets_declared_call(args, context, scope, values), + "file" => super::file::eval_file_declared_call(args, context, scope, values), + "file_exists" => super::file_exists::eval_file_exists_declared_call(args, context, scope, values), + "file_get_contents" => super::file_get_contents::eval_file_get_contents_declared_call(args, context, scope, values), + "file_put_contents" => super::file_put_contents::eval_file_put_contents_declared_call(args, context, scope, values), + "fileatime" => super::fileatime::eval_fileatime_declared_call(args, context, scope, values), + "filectime" => super::filectime::eval_filectime_declared_call(args, context, scope, values), + "filegroup" => super::filegroup::eval_filegroup_declared_call(args, context, scope, values), + "fileinode" => super::fileinode::eval_fileinode_declared_call(args, context, scope, values), + "filemtime" => super::filemtime::eval_filemtime_declared_call(args, context, scope, values), + "fileowner" => super::fileowner::eval_fileowner_declared_call(args, context, scope, values), + "fileperms" => super::fileperms::eval_fileperms_declared_call(args, context, scope, values), + "filesize" => super::filesize::eval_filesize_declared_call(args, context, scope, values), + "filetype" => super::filetype::eval_filetype_declared_call(args, context, scope, values), + "flock" => super::flock::eval_flock_declared_call(args, context, scope, values), + "fnmatch" => super::fnmatch::eval_fnmatch_declared_call(args, context, scope, values), + "fopen" => super::fopen::eval_fopen_declared_call(args, context, scope, values), + "fpassthru" => super::fpassthru::eval_fpassthru_declared_call(args, context, scope, values), + "fprintf" => super::fprintf::eval_fprintf_declared_call(args, context, scope, values), + "fputcsv" => super::fputcsv::eval_fputcsv_declared_call(args, context, scope, values), + "fread" => super::fread::eval_fread_declared_call(args, context, scope, values), + "fscanf" => super::fscanf::eval_fscanf_declared_call(args, context, scope, values), + "fseek" => super::fseek::eval_fseek_declared_call(args, context, scope, values), + "fsockopen" => super::fsockopen::eval_fsockopen_declared_call(args, context, scope, values), + "fstat" => super::fstat::eval_fstat_declared_call(args, context, scope, values), + "fsync" => super::fsync::eval_fsync_declared_call(args, context, scope, values), + "ftell" => super::ftell::eval_ftell_declared_call(args, context, scope, values), + "ftruncate" => super::ftruncate::eval_ftruncate_declared_call(args, context, scope, values), + "fwrite" => super::fwrite::eval_fwrite_declared_call(args, context, scope, values), + "getcwd" => super::getcwd::eval_getcwd_declared_call(args, context, scope, values), + "glob" => super::glob::eval_glob_declared_call(args, context, scope, values), + "is_dir" => super::is_dir::eval_is_dir_declared_call(args, context, scope, values), + "is_executable" => super::is_executable::eval_is_executable_declared_call(args, context, scope, values), + "is_file" => super::is_file::eval_is_file_declared_call(args, context, scope, values), + "is_link" => super::is_link::eval_is_link_declared_call(args, context, scope, values), + "is_readable" => super::is_readable::eval_is_readable_declared_call(args, context, scope, values), + "is_writable" => super::is_writable::eval_is_writable_declared_call(args, context, scope, values), + "is_writeable" => super::is_writeable::eval_is_writeable_declared_call(args, context, scope, values), + "lchgrp" => super::lchgrp::eval_lchgrp_declared_call(args, context, scope, values), + "lchown" => super::lchown::eval_lchown_declared_call(args, context, scope, values), + "link" => super::link::eval_link_declared_call(args, context, scope, values), + "linkinfo" => super::linkinfo::eval_linkinfo_declared_call(args, context, scope, values), + "lstat" => super::lstat::eval_lstat_declared_call(args, context, scope, values), + "mkdir" => super::mkdir::eval_mkdir_declared_call(args, context, scope, values), + "opendir" => super::opendir::eval_opendir_declared_call(args, context, scope, values), + "pathinfo" => super::pathinfo::eval_pathinfo_declared_call(args, context, scope, values), + "pclose" => super::pclose::eval_pclose_declared_call(args, context, scope, values), + "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_call(args, context, scope, values), + "popen" => super::popen::eval_popen_declared_call(args, context, scope, values), + "readdir" => super::readdir::eval_readdir_declared_call(args, context, scope, values), + "readfile" => super::readfile::eval_readfile_declared_call(args, context, scope, values), + "readline" => super::readline::eval_readline_declared_call(args, context, scope, values), + "readlink" => super::readlink::eval_readlink_declared_call(args, context, scope, values), + "realpath" => super::realpath::eval_realpath_declared_call(args, context, scope, values), + "realpath_cache_get" => super::realpath_cache_get::eval_realpath_cache_get_declared_call(args, context, scope, values), + "realpath_cache_size" => super::realpath_cache_size::eval_realpath_cache_size_declared_call(args, context, scope, values), + "rename" => super::rename::eval_rename_declared_call(args, context, scope, values), + "rewind" => super::rewind::eval_rewind_declared_call(args, context, scope, values), + "rewinddir" => super::rewinddir::eval_rewinddir_declared_call(args, context, scope, values), + "rmdir" => super::rmdir::eval_rmdir_declared_call(args, context, scope, values), + "scandir" => super::scandir::eval_scandir_declared_call(args, context, scope, values), + "stat" => super::stat::eval_stat_declared_call(args, context, scope, values), + "stream_bucket_append" => super::stream_bucket_append::eval_stream_bucket_append_declared_call(args, context, scope, values), + "stream_bucket_make_writeable" => super::stream_bucket_make_writeable::eval_stream_bucket_make_writeable_declared_call(args, context, scope, values), + "stream_bucket_new" => super::stream_bucket_new::eval_stream_bucket_new_declared_call(args, context, scope, values), + "stream_bucket_prepend" => super::stream_bucket_prepend::eval_stream_bucket_prepend_declared_call(args, context, scope, values), + "stream_context_create" => super::stream_context_create::eval_stream_context_create_declared_call(args, context, scope, values), + "stream_context_get_default" => super::stream_context_get_default::eval_stream_context_get_default_declared_call(args, context, scope, values), + "stream_context_get_options" => super::stream_context_get_options::eval_stream_context_get_options_declared_call(args, context, scope, values), + "stream_context_get_params" => super::stream_context_get_params::eval_stream_context_get_params_declared_call(args, context, scope, values), + "stream_context_set_default" => super::stream_context_set_default::eval_stream_context_set_default_declared_call(args, context, scope, values), + "stream_context_set_option" => super::stream_context_set_option::eval_stream_context_set_option_declared_call(args, context, scope, values), + "stream_context_set_params" => super::stream_context_set_params::eval_stream_context_set_params_declared_call(args, context, scope, values), + "stream_copy_to_stream" => super::stream_copy_to_stream::eval_stream_copy_to_stream_declared_call(args, context, scope, values), + "stream_filter_append" => super::stream_filter_append::eval_stream_filter_append_declared_call(args, context, scope, values), + "stream_filter_prepend" => super::stream_filter_prepend::eval_stream_filter_prepend_declared_call(args, context, scope, values), + "stream_filter_register" => super::stream_filter_register::eval_stream_filter_register_declared_call(args, context, scope, values), + "stream_filter_remove" => super::stream_filter_remove::eval_stream_filter_remove_declared_call(args, context, scope, values), + "stream_get_contents" => super::stream_get_contents::eval_stream_get_contents_declared_call(args, context, scope, values), + "stream_get_line" => super::stream_get_line::eval_stream_get_line_declared_call(args, context, scope, values), + "stream_get_meta_data" => super::stream_get_meta_data::eval_stream_get_meta_data_declared_call(args, context, scope, values), + "stream_isatty" => super::stream_isatty::eval_stream_isatty_declared_call(args, context, scope, values), + "stream_resolve_include_path" => super::stream_resolve_include_path::eval_stream_resolve_include_path_declared_call(args, context, scope, values), + "stream_select" => super::stream_select::eval_stream_select_declared_call(args, context, scope, values), + "stream_set_blocking" => super::stream_set_blocking::eval_stream_set_blocking_declared_call(args, context, scope, values), + "stream_set_chunk_size" => super::stream_set_chunk_size::eval_stream_set_chunk_size_declared_call(args, context, scope, values), + "stream_set_read_buffer" => super::stream_set_read_buffer::eval_stream_set_read_buffer_declared_call(args, context, scope, values), + "stream_set_timeout" => super::stream_set_timeout::eval_stream_set_timeout_declared_call(args, context, scope, values), + "stream_set_write_buffer" => super::stream_set_write_buffer::eval_stream_set_write_buffer_declared_call(args, context, scope, values), + "stream_socket_accept" => super::stream_socket_accept::eval_stream_socket_accept_declared_call(args, context, scope, values), + "stream_socket_client" => super::stream_socket_client::eval_stream_socket_client_declared_call(args, context, scope, values), + "stream_socket_enable_crypto" => super::stream_socket_enable_crypto::eval_stream_socket_enable_crypto_declared_call(args, context, scope, values), + "stream_socket_get_name" => super::stream_socket_get_name::eval_stream_socket_get_name_declared_call(args, context, scope, values), + "stream_socket_pair" => super::stream_socket_pair::eval_stream_socket_pair_declared_call(args, context, scope, values), + "stream_socket_recvfrom" => super::stream_socket_recvfrom::eval_stream_socket_recvfrom_declared_call(args, context, scope, values), + "stream_socket_sendto" => super::stream_socket_sendto::eval_stream_socket_sendto_declared_call(args, context, scope, values), + "stream_socket_server" => super::stream_socket_server::eval_stream_socket_server_declared_call(args, context, scope, values), + "stream_socket_shutdown" => super::stream_socket_shutdown::eval_stream_socket_shutdown_declared_call(args, context, scope, values), + "stream_wrapper_register" => super::stream_wrapper_register::eval_stream_wrapper_register_declared_call(args, context, scope, values), + "stream_wrapper_restore" => super::stream_wrapper_restore::eval_stream_wrapper_restore_declared_call(args, context, scope, values), + "stream_wrapper_unregister" => super::stream_wrapper_unregister::eval_stream_wrapper_unregister_declared_call(args, context, scope, values), + "symlink" => super::symlink::eval_symlink_declared_call(args, context, scope, values), + "sys_get_temp_dir" => super::sys_get_temp_dir::eval_sys_get_temp_dir_declared_call(args, context, scope, values), + "tempnam" => super::tempnam::eval_tempnam_declared_call(args, context, scope, values), + "tmpfile" => super::tmpfile::eval_tmpfile_declared_call(args, context, scope, values), + "touch" => super::touch::eval_touch_declared_call(args, context, scope, values), + "umask" => super::umask::eval_umask_declared_call(args, context, scope, values), + "unlink" => super::unlink::eval_unlink_declared_call(args, context, scope, values), + "vfprintf" => super::vfprintf::eval_vfprintf_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs new file mode 100644 index 0000000000..0a51247a95 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Declarative eval registry entry for `dirname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the path helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "dirname", + area: Filesystem, + params: [path, levels = EvalBuiltinDefaultValue::Int(1)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `dirname` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_dirname_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_dirname(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `dirname` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_dirname_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_dirname_result(*path, None, values), + [path, levels] => eval_dirname_result(*path, Some(*levels), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_dirname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_dirname_result(path, None, values) + } + [path, levels] => { + let path = eval_expr(path, context, scope, values)?; + let levels = eval_expr(levels, context, scope, values)?; + eval_dirname_result(path, Some(levels), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `dirname()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_dirname_result( + path: RuntimeCellHandle, + levels: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let levels = match levels { + Some(levels) => eval_int_value(levels, values)?, + None => 1, + }; + if levels < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let mut current = path; + for _ in 0..levels { + current = eval_dirname_once(¤t); + } + values.string_bytes_value(¤t) +} + +/// Applies one PHP `dirname()` parent traversal to a path byte string. +pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { + if path.is_empty() { + return b".".to_vec(); + } + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return b"/".to_vec(); + } + let mut cursor = end; + while cursor > 0 { + cursor -= 1; + if path[cursor] == b'/' { + let mut parent_end = cursor; + while parent_end > 0 && path[parent_end - 1] == b'/' { + parent_end -= 1; + } + return if parent_end == 0 { + b"/".to_vec() + } else { + path[..parent_end].to_vec() + }; + } + } + b".".to_vec() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs new file mode 100644 index 0000000000..206167594f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_free_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the disk-space helper. + +eval_builtin! { + name: "disk_free_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `disk_free_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_free_space_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_disk_space("disk_free_space", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `disk_free_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_free_space_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => eval_disk_space_result("disk_free_space", *directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +pub(in crate::interpreter) fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +pub(in crate::interpreter) fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs new file mode 100644 index 0000000000..06ee8a1cbf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_total_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the disk-space helper. + +eval_builtin! { + name: "disk_total_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `disk_total_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_total_space_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::disk_free_space::eval_builtin_disk_space("disk_total_space", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `disk_total_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_total_space_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => super::disk_free_space::eval_disk_space_result("disk_total_space", *directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs new file mode 100644 index 0000000000..b093a914b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `fclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fclose", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fclose_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fclose(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fclose_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fclose_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fclose($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fclose( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fclose_result(stream, context, values) +} + +/// Closes one materialized stream resource and returns whether it succeeded. +pub(in crate::interpreter) fn eval_fclose_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fclose_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().close(id)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs new file mode 100644 index 0000000000..51bcc93b3c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `fdatasync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fdatasync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fdatasync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fdatasync_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::fsync::eval_builtin_stream_sync("fdatasync", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fdatasync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fdatasync_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => super::fsync::eval_stream_sync_result("fdatasync", *stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs new file mode 100644 index 0000000000..53323c632e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `feof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "feof", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `feof` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_feof_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_feof(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `feof` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_feof_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_feof_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `feof($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_feof( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_feof_result(stream, context, values) +} + +/// Reports whether a materialized stream resource is at EOF. +pub(in crate::interpreter) fn eval_feof_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_feof_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources().eof(id).unwrap_or(false)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs new file mode 100644 index 0000000000..a754b943d3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `fflush`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fflush", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fflush` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fflush_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fflush(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fflush` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fflush_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fflush_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fflush($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fflush( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fflush_result(stream, context, values) +} + +/// Flushes a materialized stream resource. +pub(in crate::interpreter) fn eval_fflush_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fflush_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().flush(id)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs new file mode 100644 index 0000000000..b7b0037f87 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetc`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fgetc", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fgetc` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetc_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fgetc(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgetc` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetc_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fgetc_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgetc($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fgetc( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fgetc_result(stream, context, values) +} + +/// Reads one byte from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetc_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fread_result(id, 1, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read(id, 1) { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs new file mode 100644 index 0000000000..2ccb5221b9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs @@ -0,0 +1,149 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the CSV stream read helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fgetcsv", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + separator = EvalBuiltinDefaultValue::String(",") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fgetcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetcsv_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fgetcsv(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgetcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetcsv_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fgetcsv_result(*stream, None, None, context, values), + [stream, length] => eval_fgetcsv_result(*stream, Some(*length), None, context, values), + [stream, length, separator] => eval_fgetcsv_result(*stream, Some(*length), Some(*separator), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. +pub(in crate::interpreter) fn eval_builtin_fgetcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + eval_fgetcsv_result(stream, length, separator, context, values) +} + +/// Reads and parses one CSV record from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetcsv_result( + stream: RuntimeCellHandle, + length: Option, + separator: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); + let separator = eval_optional_delimiter(separator, b',', values)?; + let Some(mut line) = context + .stream_resources_mut() + .read_line(id, length, None, true, true) + else { + return values.bool_value(false); + }; + if line.is_empty() { + return values.bool_value(false); + } + eval_trim_csv_line_end(&mut line); + let fields = eval_parse_csv_record(&line, separator, b'"'); + eval_csv_fields_array(&fields, values) +} +/// Removes CR/LF line terminators from a CSV record buffer. +fn eval_trim_csv_line_end(line: &mut Vec) { + if line.ends_with(b"\n") { + line.pop(); + } + if line.ends_with(b"\r") { + line.pop(); + } +} + +/// Parses one CSV record using PHP-style doubled-enclosure escaping. +fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { + let mut fields = Vec::new(); + let mut field = Vec::new(); + let mut quoted = false; + let mut index = 0; + while index < line.len() { + let byte = line[index]; + if quoted { + if byte == enclosure { + if line.get(index + 1).copied() == Some(enclosure) { + field.push(enclosure); + index += 2; + continue; + } + quoted = false; + } else { + field.push(byte); + } + } else if byte == enclosure && field.is_empty() { + quoted = true; + } else if byte == separator { + fields.push(std::mem::take(&mut field)); + } else { + field.push(byte); + } + index += 1; + } + fields.push(field); + fields +} + +/// Builds a PHP indexed array from parsed CSV field bytes. +fn eval_csv_fields_array( + fields: &[Vec], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(fields.len())?; + for (index, field) in fields.iter().enumerate() { + result = super::scandir::eval_array_set_indexed_bytes(result, index, field, values)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs new file mode 100644 index 0000000000..f77ba004cc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Declarative eval registry entry for `fgets`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fgets", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fgets` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgets_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fgets(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgets` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgets_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fgets_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgets($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fgets( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fgets_result(stream, context, values) +} + +/// Reads one newline-terminated string from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgets_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fgets_result(id, context, values)? { + return Ok(result); + } + match context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs new file mode 100644 index 0000000000..c9343252a6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs @@ -0,0 +1,103 @@ +//! Purpose: +//! Declarative eval registry entry for `file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-lines helper. + +eval_builtin! { + name: "file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_file(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_file_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, context, values) +} + +/// Reads one local file or supported wrapper and returns indexed line byte strings. +pub(in crate::interpreter) fn eval_file_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + if values.type_tag(result)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(result)?; + return eval_file_lines_array(&bytes, values); + } + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + let bytes = match super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs new file mode 100644 index 0000000000..3307c5d260 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Declarative eval registry entry for `file_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "file_exists", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `file_exists` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_file_probe("file_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_exists` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_file_probe_result("file_exists", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates one PHP filesystem predicate over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_probe( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_probe_result(name, filename, context, values) +} + +/// Computes one local filesystem predicate and returns a PHP boolean. +pub(in crate::interpreter) fn eval_file_probe_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_probe_from_stat(name, stat, values); + } + if stream_wrappers::is_phar_stream(&path) { + let exists = elephc_phar::extract_url_bytes(path.as_bytes()).is_some(); + let supported = matches!(name, "file_exists" | "is_file" | "is_readable"); + return values.bool_value(supported && exists); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let path = std::path::Path::new(&path); + let result = match name { + "file_exists" => path.exists(), + "is_dir" => path.is_dir(), + "is_executable" => eval_path_is_executable(path), + "is_file" => path.is_file(), + "is_link" => std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false), + "is_readable" => eval_path_is_readable(path), + "is_writable" | "is_writeable" => eval_path_is_writable(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs new file mode 100644 index 0000000000..1ffa82f2e3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs @@ -0,0 +1,94 @@ +//! Purpose: +//! Declarative eval registry entry for `file_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the one-shot file read helper. + +eval_builtin! { + name: "file_get_contents", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; +use crate::stream_wrappers; + +/// Dispatches direct eval calls for the `file_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_get_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_file_get_contents(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_get_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_file_get_contents_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, context, values) +} + +/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. +pub(in crate::interpreter) fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + return Ok(result); + } + match eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Reads bytes from supported direct path or stream-wrapper URLs. +pub(in crate::interpreter) fn eval_read_path_or_wrapper_bytes( + path: &str, +) -> Result, ()> { + if stream_wrappers::is_data_stream(path) { + return stream_wrappers::decode_data_uri(path).ok_or(()); + } + if stream_wrappers::is_phar_stream(path) { + return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); + } + if stream_wrappers::is_http_stream(path) { + return stream_wrappers::read_http_url(path).ok_or(()); + } + let Some(path) = stream_wrappers::local_filesystem_path(path) else { + return Err(()); + }; + std::fs::read(path).map_err(|_| ()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs new file mode 100644 index 0000000000..eda110509c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Declarative eval registry entry for `file_put_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the one-shot file write helper. + +eval_builtin! { + name: "file_put_contents", + area: Filesystem, + params: [filename, data], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; +use crate::stream_wrappers; + +/// Dispatches direct eval calls for the `file_put_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_put_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_file_put_contents(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_put_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_put_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, data] => eval_file_put_contents_result(*filename, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, context, values) +} + +/// Writes a PHP string to a local file or supported wrapper and returns a byte count. +pub(in crate::interpreter) fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + if stream_wrappers::is_phar_stream(&path) { + return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { + Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + }; + } + if let Some(result) = + eval_user_wrapper_file_put_contents_result(&path, &data, context, values)? + { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs new file mode 100644 index 0000000000..3dbd772bdd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `fileatime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileatime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileatime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileatime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("fileatime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileatime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileatime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileatime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs new file mode 100644 index 0000000000..df5ccc9460 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `filectime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filectime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filectime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filectime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("filectime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filectime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filectime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filectime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs new file mode 100644 index 0000000000..c366b78d4e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `filegroup`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filegroup", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filegroup` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filegroup_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("filegroup", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filegroup` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filegroup_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filegroup", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs new file mode 100644 index 0000000000..900f97458d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `fileinode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileinode", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileinode` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileinode_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("fileinode", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileinode` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileinode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileinode", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs new file mode 100644 index 0000000000..20269d4ff4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `filemtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filemtime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filemtime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filemtime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("filemtime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filemtime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filemtime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filemtime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs new file mode 100644 index 0000000000..252af59a3e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `fileowner`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileowner", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileowner` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileowner_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("fileowner", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileowner` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileowner_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileowner", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs new file mode 100644 index 0000000000..8952df9ff6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `fileperms`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileperms", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileperms` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileperms_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_file_stat_scalar("fileperms", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileperms` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileperms_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileperms", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs new file mode 100644 index 0000000000..f3d22d92ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs @@ -0,0 +1,79 @@ +//! Purpose: +//! Declarative eval registry entry for `filesize`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the filesize helper. + +eval_builtin! { + name: "filesize", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `filesize` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filesize_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_filesize(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filesize` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filesize_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_filesize_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `filesize($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filesize( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filesize_result(filename, context, values) +} + +/// Returns one local file or supported wrapper size in bytes, or zero on failure. +pub(in crate::interpreter) fn eval_filesize_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + let size = eval_user_wrapper_stat_int_field(stat, "size", values)?.unwrap_or(0); + return values.int(size); + } + if let Ok(bytes) = super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(0); + }; + let len = std::fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0); + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs new file mode 100644 index 0000000000..711016f787 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs @@ -0,0 +1,103 @@ +//! Purpose: +//! Declarative eval registry entry for `filetype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the filetype helper. + +eval_builtin! { + name: "filetype", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `filetype` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filetype_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_filetype(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filetype` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filetype_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_filetype_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `filetype($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filetype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filetype_result(filename, context, values) +} + +/// Returns the PHP filetype string for one path, or false when lstat fails. +pub(in crate::interpreter) fn eval_filetype_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return match eval_user_wrapper_stat_int_field(stat, "mode", values)? { + Some(mode) => values.string(eval_filetype_label_from_mode(mode)), + None => values.bool_value(false), + }; + } + if stream_wrappers::is_phar_stream(&path) { + return if elephc_phar::extract_url_bytes(path.as_bytes()).is_some() { + values.string("file") + } else { + values.bool_value(false) + }; + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let file_type = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata.file_type(), + Err(_) => return values.bool_value(false), + }; + let label = if file_type.is_file() { + "file" + } else if file_type.is_dir() { + "dir" + } else if file_type.is_symlink() { + "link" + } else if file_type.is_char_device() { + "char" + } else if file_type.is_block_device() { + "block" + } else if file_type.is_fifo() { + "fifo" + } else if file_type.is_socket() { + "socket" + } else { + "unknown" + }; + values.string(label) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs new file mode 100644 index 0000000000..586be58025 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs @@ -0,0 +1,169 @@ +//! Purpose: +//! Declarative eval registry entry for `flock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "flock", + area: Filesystem, + params: [stream, operation, would_block: by_ref = EvalBuiltinDefaultValue::Null], + by_ref: [would_block], + direct: none, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `flock` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_flock_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let operation = eval_expr(&args[1], context, scope, values)?; + if args.len() >= 3 { + eval_expr(&args[2], context, scope, values)?; + values.warning("flock(): Argument #3 ($would_block) must be passed by reference, value given")?; + } + let (success, _) = eval_flock_result(stream, operation, context, values)?; + values.bool_value(success) +} + +/// Dispatches evaluated-argument calls for the `flock` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_flock_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 3 { + values.warning("flock(): Argument #3 ($would_block) must be passed by reference, value given")?; + } + let (success, _) = eval_flock_result(evaluated_args[0], evaluated_args[1], context, values)?; + values.bool_value(success) +} + +/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. +pub(in crate::interpreter) fn eval_builtin_flock( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (stream, operation, would_block_target) = + eval_flock_direct_args(args, context, scope, values)?; + let (success, would_block) = eval_flock_result(stream, operation, context, values)?; + if let Some(target) = would_block_target { + let value = values.bool_value(would_block)?; + eval_write_direct_ref_target( + &target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(success) +} + +/// Applies a materialized PHP `flock()` operation to a local eval stream resource. +pub(in crate::interpreter) fn eval_flock_result( + stream: RuntimeCellHandle, + operation: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(bool, bool), EvalStatus> { + let id = eval_stream_resource_id(stream, values)?; + let operation = eval_int_value(operation, values)?; + if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { + return Ok((success, false)); + } + Ok(context + .stream_resources() + .flock(id, operation) + .unwrap_or((false, false))) +} + +/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. +fn eval_flock_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result< + ( + RuntimeCellHandle, + RuntimeCellHandle, + Option, + ), + EvalStatus, +> { + let mut stream = None; + let mut operation = None; + let mut would_block = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "stream", + 1 => "operation", + 2 => "would_block", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "stream" => { + if stream.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + stream = Some(eval_expr(arg.value(), context, scope, values)?); + } + "operation" => { + if operation.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + operation = Some(eval_expr(arg.value(), context, scope, values)?); + } + "would_block" => { + if would_block.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; + let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; + Ok((stream, operation, would_block)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs new file mode 100644 index 0000000000..36a8e5ccf4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs @@ -0,0 +1,384 @@ +//! Purpose: +//! Declarative eval registry entry for `fnmatch`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Implements the eval-supported shell glob grammar and flags locally. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fnmatch", + area: Filesystem, + params: [pattern, filename, flags = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `fnmatch($pattern, $filename, $flags = 0)`. +pub(in crate::interpreter) fn eval_fnmatch_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fnmatch(args, context, scope, values) +} + +/// Evaluates `fnmatch()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_fnmatch_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), + [pattern, filename, flags] => eval_fnmatch_result(*pattern, *filename, Some(*flags), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +use super::super::*; + +/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fnmatch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, filename] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + eval_fnmatch_result(pattern, filename, None, values) + } + [pattern, filename, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_fnmatch_result(pattern, filename, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Runs PHP-style shell glob matching for one pattern/name pair. +pub(in crate::interpreter) fn eval_fnmatch_result( + pattern: RuntimeCellHandle, + filename: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let filename = values.string_bytes(filename)?; + let flags = match flags { + Some(flags) => eval_int_value(flags, values)?, + None => 0, + }; + values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) +} + +/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. +pub(in crate::interpreter) fn eval_fnmatch_bytes( + pattern: &[u8], + filename: &[u8], + flags: i64, +) -> bool { + let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; + eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) +} + +/// Recursively matches a pattern suffix against a filename suffix with memoization. +pub(in crate::interpreter) fn eval_fnmatch_at( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if let Some(result) = memo[pattern_index][filename_index] { + return result; + } + let result = if pattern_index == pattern.len() { + filename_index == filename.len() + } else { + match pattern[pattern_index] { + b'*' => eval_fnmatch_star( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'?' => { + eval_fnmatch_single_wildcard(filename, flags, filename_index) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + b'[' => eval_fnmatch_class_or_literal( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { + let (literal, next_pattern_index) = + eval_fnmatch_escaped_literal(pattern, pattern_index); + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) + } + literal => { + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + } + }; + memo[pattern_index][filename_index] = Some(result); + result +} + +/// Handles `*`, including pathname and leading-period restrictions. +pub(in crate::interpreter) fn eval_fnmatch_star( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + let mut next_pattern_index = pattern_index + 1; + while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { + next_pattern_index += 1; + } + if eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index, + memo, + ) { + return true; + } + let mut cursor = filename_index; + while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { + cursor += 1; + if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { + return true; + } + } + false +} + +/// Returns whether `?` can consume the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_single_wildcard( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + filename_index < filename.len() + && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) +} + +/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. +pub(in crate::interpreter) fn eval_fnmatch_class_or_literal( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if filename_index >= filename.len() + || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) + { + return false; + } + let Some((matches, next_pattern_index)) = + eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) + else { + return eval_fnmatch_literal(filename, flags, filename_index, b'[') + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ); + }; + matches + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) +} + +/// Matches one bracket class body against the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_class_matches( + pattern: &[u8], + mut index: usize, + candidate: u8, + flags: i64, +) -> Option<(bool, usize)> { + let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); + if negated { + index += 1; + } + let mut matched = false; + let mut closed = false; + while index < pattern.len() { + if pattern[index] == b']' { + closed = true; + index += 1; + break; + } + let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { + index += 1; + let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if eval_fnmatch_byte_in_range(candidate, start, end, flags) { + matched = true; + } + } else if eval_fnmatch_byte_eq(candidate, start, flags) { + matched = true; + } + } + closed.then_some((if negated { !matched } else { matched }, index)) +} + +/// Reads one character from a bracket class, respecting escapes when enabled. +pub(in crate::interpreter) fn eval_fnmatch_class_char( + pattern: &[u8], + index: &mut usize, + flags: i64, +) -> Option { + if *index >= pattern.len() { + return None; + } + if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { + *index += 2; + return Some(pattern[*index - 1]); + } + let byte = pattern[*index]; + *index += 1; + Some(byte) +} + +/// Returns whether one candidate byte falls within a possibly case-folded range. +pub(in crate::interpreter) fn eval_fnmatch_byte_in_range( + candidate: u8, + start: u8, + end: u8, + flags: i64, +) -> bool { + let candidate = eval_fnmatch_fold(candidate, flags); + let start = eval_fnmatch_fold(start, flags); + let end = eval_fnmatch_fold(end, flags); + if start <= end { + candidate >= start && candidate <= end + } else { + candidate >= end && candidate <= start + } +} + +/// Reads an escaped literal token outside bracket classes. +pub(in crate::interpreter) fn eval_fnmatch_escaped_literal( + pattern: &[u8], + pattern_index: usize, +) -> (u8, usize) { + if pattern_index + 1 < pattern.len() { + (pattern[pattern_index + 1], pattern_index + 2) + } else { + (b'\\', pattern_index + 1) + } +} + +/// Returns whether one literal pattern byte matches the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_literal( + filename: &[u8], + flags: i64, + filename_index: usize, + literal: u8, +) -> bool { + filename_index < filename.len() + && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) +} + +/// Returns whether a wildcard token may consume the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_wildcard_can_consume( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + if filename_index >= filename.len() { + return false; + } + if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { + return false; + } + if flags & EVAL_FNM_PERIOD != 0 + && eval_fnmatch_is_leading_period(filename, flags, filename_index) + { + return false; + } + true +} + +/// Returns whether the current byte is a leading period for `FNM_PERIOD`. +pub(in crate::interpreter) fn eval_fnmatch_is_leading_period( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + filename[filename_index] == b'.' + && (filename_index == 0 + || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) +} + +/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. +pub(in crate::interpreter) fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { + eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) +} + +/// Applies eval fnmatch's ASCII case folding. +pub(in crate::interpreter) fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { + if flags & EVAL_FNM_CASEFOLD != 0 { + byte.to_ascii_lowercase() + } else { + byte + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs new file mode 100644 index 0000000000..fe8c987c07 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Declarative eval registry entry for `fopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream-opening helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fopen", + area: Filesystem, + params: [ + filename, + mode, + use_include_path = EvalBuiltinDefaultValue::Bool(false), + context = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fopen(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fopen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let filename = eval_expr(&args[0], context, scope, values)?; + let mode = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + eval_fopen_path_result(&filename, &mode, context, scope, values) +} + +/// Opens a local file stream and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_fopen_result( + filename: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + let mut scope = ElephcEvalScope::new(); + eval_fopen_path_result(&filename, &mode, context, &mut scope, values) +} + +/// Opens a stream by already-coerced path and mode strings. +fn eval_fopen_path_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? { + return Ok(result); + } + match context.stream_resources_mut().open_path(filename, mode) { + Some(id) => values.resource(id), + None => { + values.warning("Warning: fopen(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs new file mode 100644 index 0000000000..1eaebdf67f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Declarative eval registry entry for `fpassthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fpassthru", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fpassthru` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fpassthru_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fpassthru(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fpassthru` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fpassthru_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fpassthru_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fpassthru($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fpassthru( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fpassthru_result(stream, context, values) +} + +/// Streams all remaining bytes to eval output and returns the emitted byte count. +pub(in crate::interpreter) fn eval_fpassthru_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fpassthru_result(id, context, values)? { + return Ok(result); + } + let Some(bytes) = context.stream_resources_mut().get_contents(id, None, None) else { + return values.bool_value(false); + }; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs new file mode 100644 index 0000000000..e9d4de11ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Declarative eval registry entry for `fprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Variadic values are formatted by the existing printf-family helper. + +eval_builtin! { + name: "fprintf", + area: Filesystem, + params: [stream, format], + variadic: values, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fprintf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fprintf(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fprintf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((stream, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some((format, format_args)) = rest.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fprintf_result(*stream, *format, format_args, context, values) +} + +/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); + for arg in &args[2..] { + format_args.push(eval_expr(arg, context, scope, values)?); + } + eval_fprintf_result(stream, format, &format_args, context, values) +} + +/// Formats and writes `fprintf()` arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_fprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + format_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let format = values.string_bytes(format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs new file mode 100644 index 0000000000..bc85a2623e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs @@ -0,0 +1,135 @@ +//! Purpose: +//! Declarative eval registry entry for `fputcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the CSV stream write helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fputcsv", + area: Filesystem, + params: [ + stream, + fields, + separator = EvalBuiltinDefaultValue::String(","), + enclosure = EvalBuiltinDefaultValue::String("\"") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fputcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fputcsv_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fputcsv(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fputcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fputcsv_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, fields] => eval_fputcsv_result(*stream, *fields, None, None, context, values), + [stream, fields, separator] => eval_fputcsv_result(*stream, *fields, Some(*separator), None, context, values), + [stream, fields, separator, enclosure] => eval_fputcsv_result(*stream, *fields, Some(*separator), Some(*enclosure), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. +pub(in crate::interpreter) fn eval_builtin_fputcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let fields = eval_expr(&args[1], context, scope, values)?; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + let enclosure = match args.get(3) { + Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), + None => None, + }; + eval_fputcsv_result(stream, fields, separator, enclosure, context, values) +} + +/// Formats and writes one CSV record to a materialized stream resource. +pub(in crate::interpreter) fn eval_fputcsv_result( + stream: RuntimeCellHandle, + fields: RuntimeCellHandle, + separator: Option, + enclosure: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let separator = eval_optional_delimiter(separator, b',', values)?; + let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; + let output = eval_format_csv_record(fields, separator, enclosure, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} +/// Formats one PHP array-like value as a CSV record ending in LF. +fn eval_format_csv_record( + fields: RuntimeCellHandle, + separator: u8, + enclosure: u8, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(fields)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(fields)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.push(separator); + } + let key = values.array_iter_key(fields, position)?; + let value = values.array_get(fields, key)?; + let bytes = values.string_bytes(value)?; + eval_append_csv_field(&mut output, &bytes, separator, enclosure); + } + output.push(b'\n'); + Ok(output) +} + +/// Appends one CSV field, quoting and escaping only when required. +fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { + let needs_quotes = field + .iter() + .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); + if !needs_quotes { + output.extend_from_slice(field); + return; + } + output.push(enclosure); + for byte in field { + if *byte == enclosure { + output.push(enclosure); + } + output.push(*byte); + } + output.push(enclosure); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs new file mode 100644 index 0000000000..77e2400b9a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Declarative eval registry entry for `fread`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream read helper. + +eval_builtin! { + name: "fread", + area: Filesystem, + params: [stream, length], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fread` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fread_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fread(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fread` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fread_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, length] => eval_fread_result(*stream, *length, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fread($stream, $length)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fread( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a materialized stream resource. +pub(in crate::interpreter) fn eval_fread_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + if let Some(result) = eval_user_wrapper_fread_result(id, length, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read(id, length) { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs new file mode 100644 index 0000000000..71bcee6310 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Declarative eval registry entry for `fscanf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - The current eval implementation returns parsed values and ignores output vars. + +eval_builtin! { + name: "fscanf", + area: Filesystem, + params: [stream, format], + variadic: vars, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fscanf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fscanf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fscanf(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fscanf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fscanf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fscanf_result(stream, format, context, values) +} + +/// Reads one line from a stream and scans it with the eval `sscanf()` subset. +pub(in crate::interpreter) fn eval_fscanf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let Some(line) = context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + else { + return values.bool_value(false); + }; + let input = values.string_bytes_value(&line)?; + eval_sscanf_result(input, format, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs new file mode 100644 index 0000000000..3100ac0cab --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Declarative eval registry entry for `fseek`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream seek helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fseek", + area: Filesystem, + params: [stream, offset, whence = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fseek` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fseek_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fseek(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fseek` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fseek_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values), + [stream, offset, whence] => eval_fseek_result(*stream, *offset, Some(*whence), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fseek( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let offset = eval_expr(&args[1], context, scope, values)?; + let whence = match args.get(2) { + Some(whence) => Some(eval_expr(whence, context, scope, values)?), + None => None, + }; + eval_fseek_result(stream, offset, whence, context, values) +} + +/// Seeks a materialized stream and returns PHP's 0 or -1 status code. +pub(in crate::interpreter) fn eval_fseek_result( + stream: RuntimeCellHandle, + offset: RuntimeCellHandle, + whence: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let offset = eval_int_value(offset, values)?; + let whence = match whence { + Some(whence) => eval_int_value(whence, values)?, + None => 0, + }; + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, offset, whence, context, values)? { + return values.int(if seek_ok { 0 } else { -1 }); + } + let status = if context.stream_resources_mut().seek(id, offset, whence) { + 0 + } else { + -1 + }; + values.int(status) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs new file mode 100644 index 0000000000..ac98708234 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs @@ -0,0 +1,198 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `fsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for by-reference outputs. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference error-output path. +//! - `pfsockopen` shares this implementation because eval has no persistent sockets. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Evaluates a positional `fsockopen()` call without writable error outputs. +pub(in crate::interpreter) fn eval_fsockopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let host = eval_expr(&args[0], context, scope, values)?; + let port = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fsockopen_by_value_ref_warnings("fsockopen", args.len(), values)?; + eval_fsockopen_result(host, port, context, values) +} + +/// Evaluates a by-value `fsockopen()` call from already evaluated arguments. +pub(in crate::interpreter) fn eval_fsockopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fsockopen_by_value_ref_warnings("fsockopen", evaluated_args.len(), values)?; + eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_fsockopen_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + &evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let error_message_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + eval_write_socket_int_output_ref_target( + error_code_target.as_ref(), + error_code, + context, + values, + )?; + eval_write_socket_output_ref_target( + error_message_target.as_ref(), + Some(error_message), + context, + values, + )?; + Ok(result) +} + +/// Opens a connected TCP stream from host and port cells. +pub(in crate::interpreter) fn eval_fsockopen_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port(&host, port) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. +pub(in crate::interpreter) fn eval_fsockopen_with_error_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port_result(&host, port) + { + Ok(id) => Ok((values.resource(id)?, 0, String::new())), + Err(error) => { + let error_code = i64::from(error.raw_os_error().unwrap_or(0)); + Ok((values.bool_value(false)?, error_code, error.to_string())) + } + } +} + +/// Emits PHP by-reference warnings for by-value socket error outputs. +pub(in crate::interpreter) fn eval_fsockopen_by_value_ref_warnings( + name: &str, + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if supplied_count >= 3 { + values.warning(&format!( + "{name}(): Argument #3 ($error_code) must be passed by reference, value given" + ))?; + } + if supplied_count >= 4 { + values.warning(&format!( + "{name}(): Argument #4 ($error_message) must be passed by reference, value given" + ))?; + } + Ok(()) +} + +/// Writes a socket output string to a captured by-reference target when available. +pub(in crate::interpreter) fn eval_write_socket_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((target, value)) = target.zip(value) else { + return Ok(()); + }; + let value = values.string(&value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} + +/// Writes a socket output integer to a captured by-reference target when available. +pub(in crate::interpreter) fn eval_write_socket_int_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(target) = target else { + return Ok(()); + }; + let value = values.int(value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs new file mode 100644 index 0000000000..18bb208566 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `fstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fstat", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fstat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fstat(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fstat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_fstat_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fstat($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fstat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fstat_result(stream, context, values) +} + +/// Builds PHP's stat array for a materialized stream resource. +pub(in crate::interpreter) fn eval_fstat_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fstat_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources().metadata(id) { + Some(metadata) => super::stat::eval_stat_metadata_array(&metadata, values), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs new file mode 100644 index 0000000000..fba1e25a23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Declarative eval registry entry for `fsync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fsync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fsync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsync_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_sync("fsync", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fsync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsync_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_stream_sync_result("fsync", *stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fsync($stream)` or `fdatasync($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_sync( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_sync_result(name, stream, context, values) +} + +/// Synchronizes one materialized stream resource to storage. +pub(in crate::interpreter) fn eval_stream_sync_result( + name: &str, + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let ok = match name { + "fsync" => context.stream_resources_mut().sync_all(id), + "fdatasync" => context.stream_resources_mut().sync_data(id), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs new file mode 100644 index 0000000000..f3874ab049 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `ftell`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "ftell", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `ftell` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftell_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_ftell(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `ftell` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftell_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_ftell_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `ftell($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ftell( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_ftell_result(stream, context, values) +} + +/// Returns the current byte offset of a materialized stream resource. +pub(in crate::interpreter) fn eval_ftell_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_ftell_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().tell(id) { + Some(position) => values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs new file mode 100644 index 0000000000..7bb0793e20 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Declarative eval registry entry for `ftruncate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream truncate helper. + +eval_builtin! { + name: "ftruncate", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `ftruncate` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftruncate_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_ftruncate(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `ftruncate` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftruncate_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, size] => eval_ftruncate_result(*stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `ftruncate($stream, $size)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_ftruncate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_ftruncate_result(stream, size, context, values) +} + +/// Truncates a materialized stream resource. +pub(in crate::interpreter) fn eval_ftruncate_result( + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + let Ok(size) = u64::try_from(size) else { + return values.bool_value(false); + }; + if let Some(result) = eval_user_wrapper_ftruncate_result(id, size, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().truncate(id, size)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs new file mode 100644 index 0000000000..e3a914bfce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Declarative eval registry entry for `fwrite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream write helper. + +eval_builtin! { + name: "fwrite", + area: Filesystem, + params: [stream, data], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `fwrite` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fwrite_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_fwrite(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fwrite` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fwrite_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, data] => eval_fwrite_result(*stream, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fwrite($stream, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fwrite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_fwrite_result(stream, data, context, values) +} + +/// Writes bytes to a materialized stream resource. +pub(in crate::interpreter) fn eval_fwrite_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let data = values.string_bytes(data)?; + if let Some(result) = eval_user_wrapper_fwrite_result(id, &data, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().write(id, &data) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs new file mode 100644 index 0000000000..e61ecfbc80 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Declarative eval registry entry for `getcwd`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the current-working-directory helper. + +eval_builtin! { + name: "getcwd", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `getcwd` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_getcwd_declared_call( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_getcwd(args, values) +} + +/// Dispatches evaluated-argument calls for the `getcwd` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_getcwd_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_getcwd_result(values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `getcwd()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_getcwd( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values) +} + +/// Returns the process current working directory as a boxed PHP string. +pub(in crate::interpreter) fn eval_getcwd_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(cwd.to_string_lossy().as_ref()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs new file mode 100644 index 0000000000..e5cb5d9298 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs @@ -0,0 +1,168 @@ +//! Purpose: +//! Declarative eval registry entry for `glob`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the local glob helper. + +eval_builtin! { + name: "glob", + area: Filesystem, + params: [pattern], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `glob` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_glob_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_glob(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `glob` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_glob_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern] => eval_glob_result(*pattern, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `glob($pattern)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_glob( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +pub(in crate::interpreter) fn eval_glob_result( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = super::scandir::eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +pub(in crate::interpreter) fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !super::fnmatch::eval_fnmatch_bytes( + component.as_bytes(), + name.as_bytes(), + EVAL_FNM_PERIOD, + ) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs new file mode 100644 index 0000000000..b7dfe8c168 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_dir", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_dir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_dir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_dir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_dir", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs new file mode 100644 index 0000000000..e05c7e6e20 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_executable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_executable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_executable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_executable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_executable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_executable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_executable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_executable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs new file mode 100644 index 0000000000..ca1aedc25b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_file_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_file", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_file_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_file", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs new file mode 100644 index 0000000000..08a7007ca4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_link", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_link_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_link", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_link_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_link", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs new file mode 100644 index 0000000000..3ad31b05ec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_readable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_readable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_readable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_readable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_readable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_readable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_readable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_readable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs new file mode 100644 index 0000000000..3d51c845e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_writable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_writable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_writable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_writable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_writable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs new file mode 100644 index 0000000000..191229f8f3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_writeable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writeable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::file_exists::eval_builtin_file_probe("is_writeable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writeable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_writeable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs new file mode 100644 index 0000000000..380b0309f4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `lchgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "lchgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lchgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchgrp_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::chown::eval_builtin_chown_like("lchgrp", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lchgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchgrp_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("lchgrp", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs new file mode 100644 index 0000000000..11df55d24e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `lchown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "lchown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lchown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::chown::eval_builtin_chown_like("lchown", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lchown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("lchown", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs new file mode 100644 index 0000000000..f8fdab862c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "link", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_link_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::copy::eval_builtin_binary_path_bool("link", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_link_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("link", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs new file mode 100644 index 0000000000..1a5b477830 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `linkinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the symbolic-link metadata helper. + +eval_builtin! { + name: "linkinfo", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; + +/// Dispatches direct eval calls for the `linkinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_linkinfo_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_linkinfo(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `linkinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_linkinfo_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_linkinfo_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_linkinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +pub(in crate::interpreter) fn eval_linkinfo_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(-1); + }; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs new file mode 100644 index 0000000000..b1c3984b4b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `lstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stat-array helper. + +eval_builtin! { + name: "lstat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lstat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stat::eval_builtin_stat_array("lstat", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lstat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => super::stat::eval_stat_array_result("lstat", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs new file mode 100644 index 0000000000..abbeaf606e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `mkdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "mkdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `mkdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_mkdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::chdir::eval_builtin_unary_path_bool("mkdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `mkdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_mkdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => super::chdir::eval_unary_path_bool_result("mkdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs new file mode 100644 index 0000000000..774fad9725 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Groups filesystem, path, glob, stat, stream, socket, and fnmatch builtins for eval. +//! +//! Called from: +//! - `crate::interpreter::builtins` filesystem-related dispatch. +//! +//! Key details: +//! - Per-builtin leaf files register metadata and expose small dispatch wrappers. +//! - Shared helpers stay in focused owner modules for path, stream, socket, and +//! wrapper behavior. + +mod basename; +mod chdir; +mod chgrp; +mod chmod; +mod chown; +mod clearstatcache; +mod closedir; +mod copy; +mod direct_dispatch; +mod dirname; +mod disk_free_space; +mod disk_total_space; +mod fclose; +mod fdatasync; +mod feof; +mod fflush; +mod fgetc; +mod fgetcsv; +mod fgets; +mod file; +mod file_exists; +mod file_get_contents; +mod file_put_contents; +mod fileatime; +mod filectime; +mod filegroup; +mod fileinode; +mod filemtime; +mod fileowner; +mod fileperms; +mod filesize; +mod filetype; +mod flock; +mod fnmatch; +mod fopen; +mod fpassthru; +mod fprintf; +mod fputcsv; +mod fread; +mod fscanf; +mod fseek; +mod fsockopen; +mod fstat; +mod fsync; +mod ftell; +mod ftruncate; +mod fwrite; +mod getcwd; +mod glob; +mod is_dir; +mod is_executable; +mod is_file; +mod is_link; +mod is_readable; +mod is_writable; +mod is_writeable; +mod lchgrp; +mod lchown; +mod link; +mod linkinfo; +mod lstat; +mod mkdir; +mod opendir; +mod path; +mod pathinfo; +mod pclose; +mod pfsockopen; +mod popen; +mod readdir; +mod readfile; +mod readline; +mod readlink; +mod realpath; +mod realpath_cache_get; +mod realpath_cache_size; +mod rename; +mod rewind; +mod rewinddir; +mod rmdir; +mod scandir; +mod stat; +mod stream_bucket_append; +mod stream_bucket_make_writeable; +mod stream_bucket_new; +mod stream_bucket_prepend; +mod stream_context_create; +mod stream_context_get_default; +mod stream_context_get_options; +mod stream_context_get_params; +mod stream_context_set_default; +mod stream_context_set_option; +mod stream_context_set_params; +mod stream_copy_to_stream; +mod stream_filter_append; +mod stream_filter_prepend; +mod stream_filter_register; +mod stream_filter_remove; +mod stream_get_contents; +mod stream_get_line; +mod stream_get_meta_data; +mod stream_isatty; +mod stream_resolve_include_path; +mod stream_select; +mod stream_set_blocking; +mod stream_set_chunk_size; +mod stream_set_read_buffer; +mod stream_set_timeout; +mod stream_set_write_buffer; +mod stream_socket_accept; +mod stream_socket_client; +mod stream_socket_enable_crypto; +mod stream_socket_get_name; +mod stream_socket_pair; +mod stream_socket_recvfrom; +mod stream_socket_sendto; +mod stream_socket_server; +mod stream_socket_shutdown; +mod stream_wrapper_register; +mod stream_wrapper_restore; +mod stream_wrapper_unregister; +mod streams; +mod symlink; +mod sys_get_temp_dir; +mod tempnam; +mod tmpfile; +mod touch; +mod umask; +mod unlink; +mod user_wrapper_cast; +mod user_wrapper_controls; +mod user_wrapper_directories; +mod user_wrapper_file_io; +mod user_wrapper_lines; +mod user_wrapper_metadata; +mod user_wrapper_options; +mod user_wrapper_path_ops; +mod user_wrapper_stat; +mod user_wrapper_streams; +mod values_dispatch; +mod vfprintf; + +pub(in crate::interpreter) use path::*; +pub(in crate::interpreter) use streams::*; +pub(in crate::interpreter) use user_wrapper_cast::*; +pub(in crate::interpreter) use user_wrapper_controls::*; +pub(in crate::interpreter) use user_wrapper_directories::*; +pub(in crate::interpreter) use user_wrapper_file_io::*; +pub(in crate::interpreter) use user_wrapper_lines::*; +pub(in crate::interpreter) use user_wrapper_metadata::*; +pub(in crate::interpreter) use user_wrapper_options::*; +pub(in crate::interpreter) use user_wrapper_path_ops::*; +pub(in crate::interpreter) use user_wrapper_stat::*; +pub(in crate::interpreter) use user_wrapper_streams::*; +pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; +pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; +pub(in crate::interpreter) use flock::{eval_builtin_flock, eval_flock_result}; +pub(in crate::interpreter) use fsockopen::{ + eval_builtin_fsockopen_call, eval_fsockopen_with_error_result, +}; +pub(in crate::interpreter) use stream_select::{ + eval_builtin_stream_select_call, eval_stream_select_result, +}; +pub(in crate::interpreter) use stream_socket_accept::{ + eval_builtin_stream_socket_accept_call, eval_stream_socket_accept_with_peer_result, +}; +pub(in crate::interpreter) use stream_socket_recvfrom::{ + eval_builtin_stream_socket_recvfrom_call, eval_stream_socket_recvfrom_with_address_result, +}; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs new file mode 100644 index 0000000000..bf9cc52dbe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `opendir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource open helper. + +eval_builtin! { + name: "opendir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `opendir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_opendir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_opendir(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `opendir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_opendir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => eval_opendir_result(*directory, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `opendir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_opendir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_opendir_result(directory, context, values) +} + +/// Opens a local directory and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_opendir_result( + directory: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + if let Some(result) = eval_user_wrapper_opendir_result(&directory, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().open_directory(&directory) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs new file mode 100644 index 0000000000..b313415c73 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Shared filesystem path conversion and permission helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. + +use super::super::super::*; + +/// Converts one eval value to a filesystem path string. +pub(in crate::interpreter) fn eval_path_string( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = values.string_bytes(filename)?; + Ok(String::from_utf8_lossy(&filename).into_owned()) +} + +/// Returns whether a path can be opened for reading by the current process. +pub(in crate::interpreter) fn eval_path_is_readable(path: &std::path::Path) -> bool { + std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() +} + +/// Returns whether a path has any executable bit set in its Unix mode. +pub(in crate::interpreter) fn eval_path_is_executable(path: &std::path::Path) -> bool { + std::fs::metadata(path) + .map(|metadata| metadata.mode() & 0o111 != 0) + .unwrap_or(false) +} + +/// Returns whether a path can be written by the current process. +pub(in crate::interpreter) fn eval_path_is_writable(path: &std::path::Path) -> bool { + if path.is_file() { + return std::fs::OpenOptions::new().write(true).open(path).is_ok(); + } + if !path.is_dir() { + return false; + } + let probe = path.join(format!( + ".elephc_magician_writable_probe_{}", + std::process::id() + )); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe) + { + Ok(_) => { + let _ = std::fs::remove_file(probe); + true + } + Err(_) => false, + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs new file mode 100644 index 0000000000..9eabaf8824 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs @@ -0,0 +1,170 @@ +//! Purpose: +//! Declarative eval registry entry for `pathinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the pathinfo helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pathinfo", + area: Filesystem, + params: [path, flags = EvalBuiltinDefaultValue::Int(15)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `pathinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pathinfo_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_pathinfo(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `pathinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pathinfo_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values), + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_pathinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_pathinfo_result(path, None, values) + } + [path, flags] => { + let path = eval_expr(path, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_pathinfo_result(path, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `pathinfo()` as either an associative array or one component string. +pub(in crate::interpreter) fn eval_pathinfo_result( + path: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let Some(flags) = flags else { + return eval_pathinfo_array_result(&path, values); + }; + let flags = eval_int_value(flags, values)?; + if flags == EVAL_PATHINFO_ALL { + return eval_pathinfo_array_result(&path, values); + } + let component = eval_pathinfo_component_bytes(&path, flags); + values.string_bytes_value(&component) +} + +/// Builds the PHP `pathinfo()` associative-array result for all components. +pub(in crate::interpreter) fn eval_pathinfo_array_result( + path: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(4)?; + if !path.is_empty() { + let dirname = eval_pathinfo_dirname_bytes(path); + result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; + } + let parts = eval_pathinfo_parts(path); + result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; + if parts.has_extension { + result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; + } + eval_pathinfo_array_set(result, "filename", &parts.filename, values) +} + +/// Inserts one string component into a PHP `pathinfo()` associative result. +pub(in crate::interpreter) fn eval_pathinfo_array_set( + array: RuntimeCellHandle, + key: &str, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Returns one PHP `pathinfo()` component for a non-all bitmask. +pub(in crate::interpreter) fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { + if flags & EVAL_PATHINFO_DIRNAME != 0 { + return eval_pathinfo_dirname_bytes(path); + } + let parts = eval_pathinfo_parts(path); + if flags & EVAL_PATHINFO_BASENAME != 0 { + return parts.basename; + } + if flags & EVAL_PATHINFO_EXTENSION != 0 { + return parts.extension; + } + if flags & EVAL_PATHINFO_FILENAME != 0 { + return parts.filename; + } + Vec::new() +} + +/// Computes the dirname component with `pathinfo("")`'s empty-string exception. +pub(in crate::interpreter) fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { + if path.is_empty() { + Vec::new() + } else { + super::dirname::eval_dirname_once(path) + } +} + +/// Splits pathinfo basename, extension, and filename components. +pub(in crate::interpreter) fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { + let basename = super::basename::eval_basename_bytes(path, None); + let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { + return EvalPathInfoParts { + filename: basename.clone(), + basename, + extension: Vec::new(), + has_extension: false, + }; + }; + EvalPathInfoParts { + filename: basename[..dot].to_vec(), + extension: basename[dot + 1..].to_vec(), + basename, + has_extension: true, + } +} + +/// Pathinfo components derived from a basename. +pub(in crate::interpreter) struct EvalPathInfoParts { + /// Full basename component. + basename: Vec, + /// Extension component after the final dot, possibly empty for trailing-dot names. + extension: Vec, + /// Filename component before the final dot. + filename: Vec, + /// Whether the basename contained a dot and therefore has an extension key. + has_extension: bool, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs new file mode 100644 index 0000000000..d698f77c8e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `pclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process-pipe close helper. + +eval_builtin! { + name: "pclose", + area: Filesystem, + params: [handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `pclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pclose_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::popen::eval_builtin_pclose(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `pclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pclose_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [handle] => super::popen::eval_pclose_result(*handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs new file mode 100644 index 0000000000..687c71273f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `pfsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` through the fsockopen path. +//! +//! Key details: +//! - Eval has no persistent socket table, so runtime behavior delegates to `fsockopen`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pfsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a positional `pfsockopen()` call without writable error outputs. +pub(in crate::interpreter) fn eval_pfsockopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let host = eval_expr(&args[0], context, scope, values)?; + let port = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + super::fsockopen::eval_fsockopen_by_value_ref_warnings("pfsockopen", args.len(), values)?; + super::fsockopen::eval_fsockopen_result(host, port, context, values) +} + +/// Evaluates a by-value `pfsockopen()` call from already evaluated arguments. +pub(in crate::interpreter) fn eval_pfsockopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + super::fsockopen::eval_fsockopen_by_value_ref_warnings( + "pfsockopen", + evaluated_args.len(), + values, + )?; + super::fsockopen::eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs new file mode 100644 index 0000000000..9a542d9d46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs @@ -0,0 +1,125 @@ +//! Purpose: +//! Declarative eval registry entry for `popen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process-pipe open helper. + +eval_builtin! { + name: "popen", + area: Filesystem, + params: [command, mode], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `popen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_popen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_popen(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `popen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_popen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [command, mode] => eval_popen_result(*command, *mode, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `popen(command, mode)`. +pub(in crate::interpreter) fn eval_builtin_popen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [command, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let command = eval_expr(command, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_popen_result(command, mode, context, values) +} + +/// Opens a shell process pipe and returns a stream resource or false. +pub(in crate::interpreter) fn eval_popen_result( + command: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = eval_path_string(command, values)?; + let mode = eval_process_pipe_mode(mode, values)?; + match context + .stream_resources_mut() + .open_process_pipe(&command, &mode) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} +/// Evaluates `pclose(handle)`. +pub(in crate::interpreter) fn eval_builtin_pclose( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let handle = eval_expr(handle, context, scope, values)?; + eval_pclose_result(handle, context, values) +} + +/// Closes a process pipe and returns its exit code, or false for invalid handles. +pub(in crate::interpreter) fn eval_pclose_result( + handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_process_pipe_resource_id(handle, values)?; + match context.stream_resources_mut().pclose(id) { + Some(status) => values.int(status), + None => values.bool_value(false), + } +} + +/// Reads a `popen()` mode string, accepting read or write pipe modes. +fn eval_process_pipe_mode( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = values.string_bytes(mode)?; + let mode = String::from_utf8(mode).map_err(|_| EvalStatus::RuntimeFatal)?; + match mode.chars().next() { + Some('r' | 'w') => Ok(mode), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based process-pipe id. +fn eval_process_pipe_resource_id( + handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs new file mode 100644 index 0000000000..bb35307e51 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `readdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource read helper. + +eval_builtin! { + name: "readdir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `readdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::closedir::eval_builtin_unary_directory("readdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [dir_handle] => super::closedir::eval_unary_directory_result("readdir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs new file mode 100644 index 0000000000..62977e0b7e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Declarative eval registry entry for `readfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the streaming file output helper. + +eval_builtin! { + name: "readfile", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; +use crate::stream_wrappers; + +/// Dispatches direct eval calls for the `readfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readfile_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_readfile(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readfile_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_readfile_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, context, values) +} + +/// Streams one local file or supported wrapper to eval output. +pub(in crate::interpreter) fn eval_readfile_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_readfile_result(&path, context, values)? { + return Ok(result); + } + if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { + let path = std::path::Path::new(&local_path); + if path.is_dir() { + return values.int(-1); + } + } + let bytes = match super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs new file mode 100644 index 0000000000..5f28f73087 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs @@ -0,0 +1,85 @@ +//! Purpose: +//! Declarative eval registry entry for `readline`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Reads from host stdin and returns false at EOF. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "readline", + area: Filesystem, + params: [prompt = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `readline([prompt])`. +pub(in crate::interpreter) fn eval_readline_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_readline(args, context, scope, values) +} + +/// Evaluates `readline()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_readline_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let prompt = match evaluated_args { + [] => None, + [prompt] => Some(*prompt), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_readline_result(prompt, values) +} + +use std::io; + +/// Evaluates `readline([prompt])` from unevaluated eval expressions. +pub(in crate::interpreter) fn eval_builtin_readline( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let prompt = match args { + [] => None, + [prompt] => Some(eval_expr(prompt, context, scope, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_readline_result(prompt, values) +} + +/// Reads one line from host stdin after optionally echoing a prompt. +pub(in crate::interpreter) fn eval_readline_result( + prompt: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(prompt) = prompt { + values.echo(prompt)?; + } + let mut line = String::new(); + let read = io::stdin() + .read_line(&mut line) + .map_err(|_| EvalStatus::RuntimeFatal)?; + if read == 0 { + return values.bool_value(false); + } + if line.ends_with('\n') { + line.pop(); + if line.ends_with('\r') { + line.pop(); + } + } + values.string(&line) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs new file mode 100644 index 0000000000..cef215ce07 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Declarative eval registry entry for `readlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the symbolic-link target helper. + +eval_builtin! { + name: "readlink", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; + +/// Dispatches direct eval calls for the `readlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_readlink(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_readlink_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `readlink($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +pub(in crate::interpreter) fn eval_readlink_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs new file mode 100644 index 0000000000..650b5f5abb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the canonical path helper. + +eval_builtin! { + name: "realpath", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `realpath` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_realpath(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `realpath` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_realpath_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `realpath($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_realpath( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_realpath_result(path, values) +} + +/// Canonicalizes one path or returns PHP false when the path cannot be resolved. +pub(in crate::interpreter) fn eval_realpath_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let path = String::from_utf8_lossy(&path); + let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { + return values.bool_value(false); + }; + let canonical = canonical.to_string_lossy(); + values.string(canonical.as_ref()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs new file mode 100644 index 0000000000..39643e3d0f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `realpath_cache_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval does not maintain a PHP realpath cache, so this returns a stable empty array. + +eval_builtin! { + name: "realpath_cache_get", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `realpath_cache_get()` with no arguments. +pub(in crate::interpreter) fn eval_realpath_cache_get_declared_call( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_realpath_cache_get(args, values) +} + +/// Evaluates `realpath_cache_get()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_realpath_cache_get_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Evaluates PHP `realpath_cache_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +pub(in crate::interpreter) fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs new file mode 100644 index 0000000000..7f0a98e29b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `realpath_cache_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval does not maintain a PHP realpath cache, so this returns zero. + +eval_builtin! { + name: "realpath_cache_size", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `realpath_cache_size()` with no arguments. +pub(in crate::interpreter) fn eval_realpath_cache_size_declared_call( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_realpath_cache_size(args, values) +} + +/// Evaluates `realpath_cache_size()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_realpath_cache_size_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +pub(in crate::interpreter) fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs new file mode 100644 index 0000000000..52119bd758 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `rename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "rename", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rename_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::copy::eval_builtin_binary_path_bool("rename", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rename_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("rename", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs new file mode 100644 index 0000000000..3ef166dfcc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `rewind`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "rewind", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `rewind` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewind_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_rewind(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rewind` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewind_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_rewind_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `rewind($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_rewind( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_rewind_result(stream, context, values) +} + +/// Rewinds a materialized stream resource to byte offset zero. +pub(in crate::interpreter) fn eval_rewind_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, 0, 0, context, values)? { + return values.bool_value(seek_ok); + } + values.bool_value(context.stream_resources_mut().rewind(id)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs new file mode 100644 index 0000000000..a8b5ad7f99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `rewinddir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource rewind helper. + +eval_builtin! { + name: "rewinddir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rewinddir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewinddir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::closedir::eval_builtin_unary_directory("rewinddir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rewinddir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewinddir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [dir_handle] => super::closedir::eval_unary_directory_result("rewinddir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs new file mode 100644 index 0000000000..66e68aff59 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `rmdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "rmdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rmdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rmdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::chdir::eval_builtin_unary_path_bool("rmdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rmdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rmdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => super::chdir::eval_unary_path_bool_result("rmdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs new file mode 100644 index 0000000000..59b336036c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Declarative eval registry entry for `scandir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory listing helper. + +eval_builtin! { + name: "scandir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `scandir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_scandir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_scandir(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `scandir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_scandir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => eval_scandir_result(*directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `scandir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_scandir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +pub(in crate::interpreter) fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +pub(in crate::interpreter) fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs new file mode 100644 index 0000000000..675053d8ba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs @@ -0,0 +1,190 @@ +//! Purpose: +//! Declarative eval registry entry for `stat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stat-array helper. + +eval_builtin! { + name: "stat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `stat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stat_array("stat", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_stat_array_result("stat", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates one scalar PHP stat metadata builtin over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_stat_scalar( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_stat_scalar_result(name, filename, context, values) +} + +/// Returns scalar stat metadata, using PHP false for failure where native elephc does. +pub(in crate::interpreter) fn eval_file_stat_scalar_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_stat_scalar_from_stat(name, stat, values); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return match name { + "filemtime" => values.int(0), + _ => values.bool_value(false), + }; + }; + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(_) if name == "filemtime" => return values.int(0), + Err(_) => return values.bool_value(false), + }; + match name { + "fileatime" => values.int(metadata.atime()), + "filectime" => values.int(metadata.ctime()), + "filegroup" => values.int(i64::from(metadata.gid())), + "fileinode" => { + values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) + } + "filemtime" => values.int(metadata.mtime()), + "fileowner" => values.int(i64::from(metadata.uid())), + "fileperms" => values.int(i64::from(metadata.mode())), + _ => Err(EvalStatus::RuntimeFatal), + } +} +/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stat_array( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stat_array_result(name, filename, context, values) +} + +/// Builds PHP's stat array for one local path, or returns false on stat failure. +pub(in crate::interpreter) fn eval_stat_array_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return Ok(stat); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let metadata = match name { + "stat" => std::fs::metadata(path), + "lstat" => std::fs::symlink_metadata(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let metadata = match metadata { + Ok(metadata) => metadata, + Err(_) => return values.bool_value(false), + }; + eval_stat_metadata_array(&metadata, values) +} + +/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. +pub(in crate::interpreter) fn eval_stat_metadata_array( + metadata: &std::fs::Metadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let fields = [ + ("dev", eval_u64_to_i64(metadata.dev())?), + ("ino", eval_u64_to_i64(metadata.ino())?), + ("mode", i64::from(metadata.mode())), + ("nlink", eval_u64_to_i64(metadata.nlink())?), + ("uid", i64::from(metadata.uid())), + ("gid", i64::from(metadata.gid())), + ("rdev", eval_u64_to_i64(metadata.rdev())?), + ("size", eval_u64_to_i64(metadata.size())?), + ("atime", metadata.atime()), + ("mtime", metadata.mtime()), + ("ctime", metadata.ctime()), + ("blksize", eval_u64_to_i64(metadata.blksize())?), + ("blocks", eval_u64_to_i64(metadata.blocks())?), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Inserts one integer stat field under a numeric PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_int_key( + array: RuntimeCellHandle, + key: usize, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts one integer stat field under a string PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_string_key( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. +pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs new file mode 100644 index 0000000000..4af2773a41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_bucket_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Appends bucket objects to the brigade `_buckets` array used by eval filters. + +eval_builtin! { + name: "stream_bucket_append", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_bucket_append($brigade, $bucket)`. +pub(in crate::interpreter) fn eval_stream_bucket_append_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + let bucket = eval_expr(bucket, context, scope, values)?; + eval_stream_bucket_append_result(brigade, bucket, values) +} + +/// Appends an already evaluated bucket to an already evaluated brigade. +pub(in crate::interpreter) fn eval_stream_bucket_append_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_append_result(*brigade, *bucket, values) +} + +/// Adds a bucket object to the end of the brigade's `_buckets` array. +pub(in crate::interpreter) fn eval_stream_bucket_append_result( + brigade: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = eval_brigade_buckets(brigade, values)?; + let len = values.array_len(buckets)?; + let index = values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let buckets = values.array_set(buckets, index, bucket)?; + values.property_set(brigade, "_buckets", buckets)?; + values.null() +} + +/// Returns an existing brigade bucket array or creates an empty one. +pub(in crate::interpreter) fn eval_brigade_buckets( + brigade: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = values.property_get(brigade, "_buckets")?; + if values.is_array_like(buckets)? { + Ok(buckets) + } else { + values.array_new(0) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs new file mode 100644 index 0000000000..b6a06e45c4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_bucket_make_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Returns the first queued bucket from an eval brigade object. + +eval_builtin! { + name: "stream_bucket_make_writeable", + area: Filesystem, + params: [brigade], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_bucket_make_writeable($brigade)`. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + eval_stream_bucket_make_writeable_result(brigade, values) +} + +/// Returns the first bucket from an already evaluated brigade argument. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_make_writeable_result(*brigade, values) +} + +/// Returns the first bucket in a brigade, or null when none exists. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_result( + brigade: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = values.property_get(brigade, "_buckets")?; + if !values.is_array_like(buckets)? || values.array_len(buckets)? == 0 { + return values.null(); + } + let key = values.int(0)?; + values.array_get(buckets, key) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs new file mode 100644 index 0000000000..9aafce2033 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_bucket_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Creates stdClass bucket objects with `data` and `datalen` properties. + +eval_builtin! { + name: "stream_bucket_new", + area: Filesystem, + params: [stream, buffer], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_bucket_new($stream, $buffer)`. +pub(in crate::interpreter) fn eval_stream_bucket_new_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_stream_bucket_new_result(stream, buffer, context, values) +} + +/// Creates a bucket object from already evaluated stream and buffer arguments. +pub(in crate::interpreter) fn eval_stream_bucket_new_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_new_result(*stream, *buffer, context, values) +} + +/// Creates a stdClass-backed stream bucket object. +pub(in crate::interpreter) fn eval_stream_bucket_new_result( + stream: RuntimeCellHandle, + buffer: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let stream_id = eval_stream_extension_resource_id(stream, values)?; + if !context.stream_resources().has_stream(stream_id) { + return values.null(); + } + let bytes = values.string_bytes(buffer)?; + let bucket = values.new_object("stdClass")?; + let data = values.string_bytes_value(&bytes)?; + let datalen = values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + values.property_set(bucket, "data", data)?; + values.property_set(bucket, "datalen", datalen)?; + Ok(bucket) +} + +/// Converts a runtime resource cell into eval's zero-based stream-extension id. +pub(in crate::interpreter) fn eval_stream_extension_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs new file mode 100644 index 0000000000..2d452c3460 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_bucket_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Prepends bucket objects to the brigade `_buckets` array used by eval filters. + +eval_builtin! { + name: "stream_bucket_prepend", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_bucket_prepend($brigade, $bucket)`. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + let bucket = eval_expr(bucket, context, scope, values)?; + eval_stream_bucket_prepend_result(brigade, bucket, values) +} + +/// Prepends an already evaluated bucket to an already evaluated brigade. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_prepend_result(*brigade, *bucket, values) +} + +/// Adds a bucket object to the front of the brigade's `_buckets` array. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_result( + brigade: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = super::stream_bucket_append::eval_brigade_buckets(brigade, values)?; + let buckets = eval_bucket_prepend(buckets, bucket, values)?; + values.property_set(brigade, "_buckets", buckets)?; + values.null() +} + +/// Builds a new bucket array with the provided bucket at index zero. +fn eval_bucket_prepend( + buckets: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(buckets)?; + let mut result = values.array_new(len + 1)?; + let zero = values.int(0)?; + result = values.array_set(result, zero, bucket)?; + for index in 0..len { + let old_key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.array_get(buckets, old_key)?; + let new_key = + values.int(i64::try_from(index + 1).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, new_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs new file mode 100644 index 0000000000..98dd0d81b0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs @@ -0,0 +1,85 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_create`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Owns context resource creation and validates optional options arrays before storage. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_create", + area: Filesystem, + params: [ + options = EvalBuiltinDefaultValue::Null, + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_create($options = null, $params = null)`. +pub(in crate::interpreter) fn eval_stream_context_create_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let options = match args.first() { + Some(options) => Some(eval_expr(options, context, scope, values)?), + None => None, + }; + if let Some(params) = args.get(1) { + eval_expr(params, context, scope, values)?; + } + eval_stream_context_create_result(options, context, values) +} + +/// Creates a stream context resource from already evaluated optional options. +pub(in crate::interpreter) fn eval_stream_context_create_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_stream_context_create_result(None, context, values), + [options] => eval_stream_context_create_result(Some(*options), context, values), + [options, _params] => eval_stream_context_create_result(Some(*options), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates a stream context resource from materialized optional options. +pub(in crate::interpreter) fn eval_stream_context_create_result( + options: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let options = eval_stream_context_options_arg(options, values)?; + let id = context.stream_resources_mut().open_stream_context(options); + values.resource(id) +} + +/// Converts an optional options argument into a stored context option handle. +pub(in crate::interpreter) fn eval_stream_context_options_arg( + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(options) = options else { + return Ok(None); + }; + if values.type_tag(options)? == EVAL_TAG_NULL { + return Ok(None); + } + if !values.is_array_like(options)? { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(options)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs new file mode 100644 index 0000000000..4eed88761e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Returns eval's shared default stream context resource. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_get_default", + area: Filesystem, + params: [options = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_get_default($options = null)`. +pub(in crate::interpreter) fn eval_stream_context_get_default_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + eval_stream_context_get_default_result(context, values) +} + +/// Returns the default context after validating already evaluated arguments. +pub(in crate::interpreter) fn eval_stream_context_get_default_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_context_get_default_result(context, values) +} + +/// Returns eval's default stream context resource. +pub(in crate::interpreter) fn eval_stream_context_get_default_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = context.stream_resources_mut().default_stream_context(); + values.resource(id) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs new file mode 100644 index 0000000000..5fa2390d46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_options`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Returns persisted context options or an empty associative array. + +eval_builtin! { + name: "stream_context_get_options", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_get_options($context)`. +pub(in crate::interpreter) fn eval_stream_context_get_options_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_stream_context_get_options_result(stream_context, context, values) +} + +/// Returns options for an already evaluated stream context resource. +pub(in crate::interpreter) fn eval_stream_context_get_options_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_options_result(*stream_context, context, values) +} + +/// Returns persisted stream context options or an empty associative array. +pub(in crate::interpreter) fn eval_stream_context_get_options_result( + stream_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + match context.stream_resources().stream_context_options(id) { + Some(options) => Ok(options), + None => values.assoc_new(0), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs new file mode 100644 index 0000000000..7de0673e7d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval validates the context resource and mirrors the main backend's empty-params behavior. + +eval_builtin! { + name: "stream_context_get_params", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_get_params($context)` to an empty associative array. +pub(in crate::interpreter) fn eval_stream_context_get_params_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_stream_context_get_params_result(stream_context, values) +} + +/// Returns empty params for an already evaluated stream context resource. +pub(in crate::interpreter) fn eval_stream_context_get_params_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_params_result(*stream_context, values) +} + +/// Validates the stream context resource and returns the current empty params array. +pub(in crate::interpreter) fn eval_stream_context_get_params_result( + stream_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + values.assoc_new(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs new file mode 100644 index 0000000000..a8bb8c7183 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Current eval behavior validates arity/evaluation and returns the default context resource. + +eval_builtin! { + name: "stream_context_set_default", + area: Filesystem, + params: [options], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_set_default($options)`. +pub(in crate::interpreter) fn eval_stream_context_set_default_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [options] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_expr(options, context, scope, values)?; + super::stream_context_get_default::eval_stream_context_get_default_result(context, values) +} + +/// Returns the default context after validating already evaluated arguments. +pub(in crate::interpreter) fn eval_stream_context_set_default_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [_options] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + super::stream_context_get_default::eval_stream_context_get_default_result(context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs new file mode 100644 index 0000000000..9022d91afd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_option`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Owns both `stream_context_set_option($context, $options)` and the +//! four-argument nested option form. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_set_option", + area: Filesystem, + params: [ + context, + wrapper_or_options, + option_name = EvalBuiltinDefaultValue::Null, + value = EvalBuiltinDefaultValue::Null + ], + required: 2, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_set_option($context, ...)`. +pub(in crate::interpreter) fn eval_stream_context_set_option_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [stream_context, options] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let options = eval_expr(options, context, scope, values)?; + eval_stream_context_set_options_result(stream_context, options, context, values) + } + [stream_context, wrapper, option, value] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let wrapper = eval_expr(wrapper, context, scope, values)?; + let option = eval_expr(option, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_stream_context_set_option_result( + stream_context, + wrapper, + option, + value, + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Stores context options from already evaluated arguments. +pub(in crate::interpreter) fn eval_stream_context_set_option_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream_context, options] => { + eval_stream_context_set_options_result(*stream_context, *options, context, values) + } + [stream_context, wrapper, option, value] => eval_stream_context_set_option_result( + *stream_context, + *wrapper, + *option, + *value, + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Stores a materialized options array on a stream context resource. +pub(in crate::interpreter) fn eval_stream_context_set_options_result( + stream_context: RuntimeCellHandle, + options: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let options = super::stream_context_create::eval_stream_context_options_arg(Some(options), values)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, options), + ) +} + +/// Stores one nested `options[wrapper][option] = value` entry on a stream context. +pub(in crate::interpreter) fn eval_stream_context_set_option_result( + stream_context: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + option: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let wrapper = values.cast_string(wrapper)?; + let option = values.cast_string(option)?; + let options = match context.stream_resources().stream_context_options(id) { + Some(options) => options, + None => values.assoc_new(1)?, + }; + let wrapper_options = eval_stream_context_wrapper_options(options, wrapper, values)?; + let wrapper_options = values.array_set(wrapper_options, option, value)?; + let options = values.array_set(options, wrapper, wrapper_options)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, Some(options)), + ) +} + +/// Converts a runtime resource cell into eval's zero-based stream context id. +pub(in crate::interpreter) fn eval_stream_context_resource_id( + stream_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns the nested wrapper options array, creating one when missing or scalar. +fn eval_stream_context_wrapper_options( + options: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = values.array_key_exists(wrapper, options)?; + if values.truthy(exists)? { + let wrapper_options = values.array_get(options, wrapper)?; + if values.is_array_like(wrapper_options)? { + return Ok(wrapper_options); + } + } + values.assoc_new(1) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs new file mode 100644 index 0000000000..0c4c089ead --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval validates the context resource and accepts params as a no-op. + +eval_builtin! { + name: "stream_context_set_params", + area: Filesystem, + params: [context, params], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_context_set_params($context, $params)` as an accepted no-op. +pub(in crate::interpreter) fn eval_stream_context_set_params_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context, params] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_expr(params, context, scope, values)?; + eval_stream_context_set_params_result(stream_context, values) +} + +/// Returns true after validating already evaluated stream context params. +pub(in crate::interpreter) fn eval_stream_context_set_params_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context, _params] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_set_params_result(*stream_context, values) +} + +/// Validates the stream context resource and returns true for accepted params. +pub(in crate::interpreter) fn eval_stream_context_set_params_result( + stream_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs new file mode 100644 index 0000000000..486e866e35 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_copy_to_stream`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream copy helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_copy_to_stream", + area: Filesystem, + params: [ + from, + to, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_copy_to_stream(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values), + [from, to, length] => eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values), + [from, to, length, offset] => eval_stream_copy_to_stream_result(*from, *to, Some(*length), Some(*offset), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_copy_to_stream($from, $to, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_copy_to_stream( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let from = eval_expr(&args[0], context, scope, values)?; + let to = eval_expr(&args[1], context, scope, values)?; + let length = match args.get(2) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(3) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_copy_to_stream_result(from, to, length, offset, context, values) +} + +/// Copies bytes between two materialized stream resources. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( + from: RuntimeCellHandle, + to: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_stream_resource_id(from, values)?; + let to = eval_stream_resource_id(to, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_copy_to_stream_result(from, to, length, offset, context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .copy_to_stream(from, to, length, offset) + { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs new file mode 100644 index 0000000000..21466a1308 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_filter_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Creates eval-local filter resources without transforming stream bytes. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_append", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_filter_append($stream, $filtername, $read_write = 3, $params = null)`. +pub(in crate::interpreter) fn eval_stream_filter_append_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let filter_name = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_filter_attach_result("stream_filter_append", stream, filter_name, context, values) +} + +/// Appends a filter from already evaluated stream filter arguments. +pub(in crate::interpreter) fn eval_stream_filter_append_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_filter_attach_result( + "stream_filter_append", + evaluated_args[0], + evaluated_args[1], + context, + values, + ) +} + +/// Creates an eval-local filter resource for a materialized stream filter attach. +pub(in crate::interpreter) fn eval_stream_filter_attach_result( + name: &str, + stream: RuntimeCellHandle, + filter_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "stream_filter_append" | "stream_filter_prepend") { + return Err(EvalStatus::RuntimeFatal); + } + let stream_id = super::stream_bucket_new::eval_stream_extension_resource_id(stream, values)?; + let _ = values.string_bytes(filter_name)?; + if !context.stream_resources().has_stream(stream_id) { + return values.bool_value(false); + } + let filter_id = context.stream_resources_mut().open_filter_resource(); + values.resource(filter_id) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs new file mode 100644 index 0000000000..639f620b70 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_filter_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Creates eval-local filter resources without transforming stream bytes. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_prepend", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_filter_prepend($stream, $filtername, $read_write = 3, $params = null)`. +pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let filter_name = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + super::stream_filter_append::eval_stream_filter_attach_result( + "stream_filter_prepend", + stream, + filter_name, + context, + values, + ) +} + +/// Prepends a filter from already evaluated stream filter arguments. +pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + super::stream_filter_append::eval_stream_filter_attach_result( + "stream_filter_prepend", + evaluated_args[0], + evaluated_args[1], + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs new file mode 100644 index 0000000000..05a1a00fdf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_filter_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval conservatively accepts registrations without mutating stream bytes. + +eval_builtin! { + name: "stream_filter_register", + area: Filesystem, + params: [filter_name, r#class], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_filter_register($filter_name, $class)`. +pub(in crate::interpreter) fn eval_stream_filter_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filter_name, class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filter_name = eval_expr(filter_name, context, scope, values)?; + let class = eval_expr(class, context, scope, values)?; + eval_stream_filter_register_result(filter_name, class, values) +} + +/// Registers an already evaluated stream filter name and class pair. +pub(in crate::interpreter) fn eval_stream_filter_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filter_name, class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_register_result(*filter_name, *class, values) +} + +/// Evaluates a materialized `stream_filter_register()` call. +pub(in crate::interpreter) fn eval_stream_filter_register_result( + filter_name: RuntimeCellHandle, + class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.string_bytes(filter_name)?; + let _ = values.string_bytes(class)?; + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs new file mode 100644 index 0000000000..7d7757304b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_filter_remove`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Closes eval-local filter resources created by append/prepend. + +eval_builtin! { + name: "stream_filter_remove", + area: Filesystem, + params: [stream_filter], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_filter_remove($stream_filter)`. +pub(in crate::interpreter) fn eval_stream_filter_remove_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_filter] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_filter = eval_expr(stream_filter, context, scope, values)?; + eval_stream_filter_remove_result(stream_filter, context, values) +} + +/// Removes an already evaluated eval-local filter resource. +pub(in crate::interpreter) fn eval_stream_filter_remove_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_filter] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_remove_result(*stream_filter, context, values) +} + +/// Removes an eval-local filter resource. +pub(in crate::interpreter) fn eval_stream_filter_remove_result( + stream_filter: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_bucket_new::eval_stream_extension_resource_id(stream_filter, values)?; + values.bool_value(context.stream_resources_mut().close_filter_resource(id)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs new file mode 100644 index 0000000000..42f34f4369 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs @@ -0,0 +1,96 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the bounded stream read helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_contents", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_get_contents(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_stream_get_contents_result(*stream, None, None, context, values), + [stream, length] => eval_stream_get_contents_result(*stream, Some(*length), None, context, values), + [stream, length, offset] => eval_stream_get_contents_result(*stream, Some(*length), Some(*offset), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_get_contents($stream, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(2) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_get_contents_result(stream, length, offset, context, values) +} + +/// Reads the remaining or bounded contents from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_contents_result( + stream: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_get_contents_result(id, length, offset, context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .get_contents(id, length, offset) + { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs new file mode 100644 index 0000000000..4882a80c37 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs @@ -0,0 +1,94 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_line`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the delimiter-aware stream line helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_line", + area: Filesystem, + params: [stream, length, ending = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_get_line` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_line_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_get_line(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_line` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_line_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values), + [stream, length, ending] => eval_stream_get_line_result(*stream, *length, Some(*ending), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_get_line($stream, $length, $ending = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_line( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + let ending = match args.get(2) { + Some(ending) => Some(eval_expr(ending, context, scope, values)?), + None => None, + }; + eval_stream_get_line_result(stream, length, ending, context, values) +} + +/// Reads one line-like byte sequence from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_line_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + ending: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + let ending = match ending { + Some(ending) if values.type_tag(ending)? != EVAL_TAG_NULL => { + Some(values.string_bytes(ending)?) + } + _ => None, + }; + if let Some(result) = + eval_user_wrapper_stream_get_line_result(id, length, ending.as_deref(), context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .read_line(id, length, ending.as_deref(), false, false) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs new file mode 100644 index 0000000000..804ac8fb3a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs @@ -0,0 +1,122 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_meta_data`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "stream_get_meta_data", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_get_meta_data(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_stream_get_meta_data_handle_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's stream metadata array for one eval-local stream resource. +pub(in crate::interpreter) fn eval_stream_get_meta_data_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(meta) = context.stream_resources().meta_data(id) else { + return values.bool_value(false); + }; + let mut result = values.assoc_new(9)?; + result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; + result = eval_stream_meta_set_bool(result, "blocked", true, values)?; + result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; + result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; + result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; + result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; + result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; + result = eval_stream_meta_set_bool(result, "seekable", true, values)?; + eval_stream_meta_set_string(result, "uri", &meta.uri, values) +} + +/// Inserts a boolean field into the stream metadata array. +fn eval_stream_meta_set_bool( + array: RuntimeCellHandle, + key: &str, + value: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.bool_value(value)?; + values.array_set(array, key, value) +} + +/// Inserts an integer field into the stream metadata array. +fn eval_stream_meta_set_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts a string field into the stream metadata array. +fn eval_stream_meta_set_string( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} + +/// Evaluates PHP `stream_get_meta_data($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_get_meta_data( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_get_meta_data_handle_result(stream, context, values) +} + +/// Builds PHP metadata for one materialized stream resource handle. +pub(in crate::interpreter) fn eval_stream_get_meta_data_handle_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + eval_stream_get_meta_data_result(id, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs new file mode 100644 index 0000000000..97b6f29b9e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs @@ -0,0 +1,65 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_isatty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream descriptor predicate helper. + +eval_builtin! { + name: "stream_isatty", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_isatty` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_isatty_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_isatty(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_isatty` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_isatty_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream] => eval_stream_isatty_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_isatty($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_isatty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_isatty_result(stream, context, values) +} + +/// Returns whether a materialized stream resource is attached to a terminal. +pub(in crate::interpreter) fn eval_stream_isatty_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + values.bool_value(context.stream_resources().isatty(id).unwrap_or(false)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs new file mode 100644 index 0000000000..4bb9824f55 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_resolve_include_path`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the include-path resolution helper. + +eval_builtin! { + name: "stream_resolve_include_path", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_resolve_include_path` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_resolve_include_path(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_resolve_include_path` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_stream_resolve_include_path_result(*filename, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_resolve_include_path($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_resolve_include_path( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stream_resolve_include_path_result(filename, values) +} + +/// Resolves one filename using elephc's realpath-equivalent include-path semantics. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::realpath::eval_realpath_result(filename, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs new file mode 100644 index 0000000000..0f63f12342 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_select`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for by-reference arrays. +//! +//! Key details: +//! - `stream_select()` rewrites read/write/except arrays through by-reference +//! targets after validating resource handles. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_select", + area: Filesystem, + params: [ + read: by_ref, + write: by_ref, + except: by_ref, + seconds, + microseconds = EvalBuiltinDefaultValue::Int(0) + ], + by_ref: [read, write, except], + direct: none, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Evaluates a positional `stream_select()` call without writable array outputs. +pub(in crate::interpreter) fn eval_stream_select_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; + eval_stream_select_result(&evaluated_args, context, values) +} + +/// Evaluates `stream_select()` from already evaluated by-value arguments. +pub(in crate::interpreter) fn eval_stream_select_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; + eval_stream_select_result(evaluated_args, context, values) +} + +/// Evaluates `stream_select()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_select_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + &evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = vec![ + read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + eval_write_stream_select_empty_arrays(&targets, context, values)?; + Ok(result) +} + +/// Evaluates materialized `stream_select(...)` arguments. +pub(in crate::interpreter) fn eval_stream_select_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + for array in evaluated_args.iter().take(3) { + eval_stream_select_cast_array(*array, context, values)?; + } + values.int(0) +} + +/// Emits PHP by-reference warnings for by-value `stream_select()` array outputs. +fn eval_stream_select_by_value_ref_warnings( + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (index, param_name) in ["read", "write", "except"].iter().enumerate() { + if supplied_count <= index { + continue; + } + values.warning(&format!( + "stream_select(): Argument #{} (${param_name}) must be passed by reference, value given", + index + 1 + ))?; + } + Ok(()) +} + +/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. +fn eval_write_stream_select_empty_arrays( + targets: &[EvalReferenceTarget], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(()) +} + +/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. +fn eval_stream_select_cast_array( + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.is_array_like(array)? { + return Ok(()); + } + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + eval_stream_select_cast_value(value, context, values)?; + } + Ok(()) +} + +/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. +fn eval_stream_select_cast_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_RESOURCE { + return Ok(()); + } + let display_id = eval_int_value(value, values)?; + let Some(id) = display_id.checked_sub(1) else { + return Ok(()); + }; + let Some(result) = + eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? + else { + return Ok(()); + }; + values.release(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs new file mode 100644 index 0000000000..18871d979f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_blocking`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream blocking-mode helper. + +eval_builtin! { + name: "stream_set_blocking", + area: Filesystem, + params: [stream, enable], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_blocking_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_set_blocking(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_blocking_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_set_blocking($stream, $enable)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_blocking( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, enable] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let enable = eval_expr(enable, context, scope, values)?; + eval_stream_set_blocking_result(stream, enable, context, values) +} + +/// Toggles blocking mode on a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_blocking_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let enable = values.truthy(enable)?; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_BLOCKING, + if enable { 1 } else { 0 }, + 0, + context, + values, + )? { + return Ok(result); + } + values.bool_value(context.stream_resources().set_blocking(id, enable).unwrap_or(false)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs new file mode 100644 index 0000000000..f31b1e5e38 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_chunk_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream chunk-size metadata helper. + +eval_builtin! { + name: "stream_set_chunk_size", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_set_buffer_like("stream_set_chunk_size", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, size] => eval_stream_set_buffer_like_result("stream_set_chunk_size", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates chunk/read/write buffer setting builtins. +pub(in crate::interpreter) fn eval_builtin_stream_set_buffer_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_stream_set_buffer_like_result(name, stream, size, context, values) +} + +/// Applies a materialized chunk/read/write buffer setting. +pub(in crate::interpreter) fn eval_stream_set_buffer_like_result( + name: &str, + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + match name { + "stream_set_chunk_size" => match context.stream_resources_mut().set_chunk_size(id, size) { + Some(previous) => values.int(previous), + None => values.bool_value(false), + }, + "stream_set_read_buffer" | "stream_set_write_buffer" => { + match context.stream_resources().set_buffer(id, size) { + Some(status) => values.int(status), + None => values.bool_value(false), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs new file mode 100644 index 0000000000..635e6c616e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_read_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_read_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_read_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_set_chunk_size::eval_builtin_stream_set_buffer_like("stream_set_read_buffer", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_read_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, size] => super::stream_set_chunk_size::eval_stream_set_buffer_like_result("stream_set_read_buffer", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs new file mode 100644 index 0000000000..94cf86cddd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs @@ -0,0 +1,95 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_timeout`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream timeout-setting helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_set_timeout", + area: Filesystem, + params: [stream, seconds, microseconds = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_timeout_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_stream_set_timeout(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_timeout_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, seconds] => eval_stream_set_timeout_result(*stream, *seconds, None, context, values), + [stream, seconds, microseconds] => eval_stream_set_timeout_result(*stream, *seconds, Some(*microseconds), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_set_timeout($stream, $seconds, $microseconds = 0)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_timeout( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let seconds = eval_expr(&args[1], context, scope, values)?; + let microseconds = match args.get(2) { + Some(microseconds) => Some(eval_expr(microseconds, context, scope, values)?), + None => None, + }; + eval_stream_set_timeout_result(stream, seconds, microseconds, context, values) +} + +/// Applies a timeout request to a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_timeout_result( + stream: RuntimeCellHandle, + seconds: RuntimeCellHandle, + microseconds: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let seconds = eval_int_value(seconds, values)?; + let microseconds = match microseconds { + Some(microseconds) => eval_int_value(microseconds, values)?, + None => 0, + }; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_READ_TIMEOUT, + seconds, + microseconds, + context, + values, + )? { + return Ok(result); + } + values.bool_value( + context + .stream_resources() + .set_timeout(id, seconds, microseconds) + .unwrap_or(false), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs new file mode 100644 index 0000000000..51ffe897c7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_write_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_write_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_write_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_set_chunk_size::eval_builtin_stream_set_buffer_like("stream_set_write_buffer", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_write_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, size] => super::stream_set_chunk_size::eval_stream_set_buffer_like_result("stream_set_write_buffer", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs new file mode 100644 index 0000000000..bb880c721d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_accept`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for peer-name writeback. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference peer-name path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_accept", + area: Filesystem, + params: [ + socket, + timeout = EvalBuiltinDefaultValue::Null, + peer_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [peer_name], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a positional `stream_socket_accept()` call without writable peer output. +pub(in crate::interpreter) fn eval_stream_socket_accept_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let socket = eval_expr(&args[0], context, scope, values)?; + for arg in &args[1..] { + eval_expr(arg, context, scope, values)?; + } + if args.len() >= 3 { + values.warning( + "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", + )?; + } + eval_stream_socket_accept_result(socket, context, values) +} + +/// Accepts a socket from already evaluated by-value arguments. +pub(in crate::interpreter) fn eval_stream_socket_accept_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 3 { + values.warning( + "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", + )?; + } + eval_stream_socket_accept_result(evaluated_args[0], context, values) +} + +/// Evaluates `stream_socket_accept()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = + bind_evaluated_ref_builtin_args(&["socket", "timeout", "peer_name"], &evaluated_args, false)?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let peer_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + super::fsockopen::eval_write_socket_output_ref_target( + peer_name_target.as_ref(), + peer_name, + context, + values, + )?; + Ok(result) +} + +/// Accepts one pending TCP connection from a listener resource. +pub(in crate::interpreter) fn eval_stream_socket_accept_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(socket, values)?; + match context.stream_resources_mut().accept_tcp(id) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = super::stream_socket_get_name::eval_socket_resource_id(socket, values)?; + let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { + return values.bool_value(false).map(|result| (result, None)); + }; + let peer_name = context.stream_resources().socket_name(accepted_id, true); + let result = values.resource(accepted_id)?; + Ok((result, peer_name)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs new file mode 100644 index 0000000000..f2a6a57f45 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_client`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Opened sockets enter eval's normal stream table. + +eval_builtin! { + name: "stream_socket_client", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Evaluates `stream_socket_client($address)`. +pub(in crate::interpreter) fn eval_stream_socket_client_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_client_result(address, context, values) +} + +/// Opens a connected stream from an already evaluated address argument. +pub(in crate::interpreter) fn eval_stream_socket_client_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_client_result(*address, context, values) +} + +/// Opens a connected TCP stream resource. +pub(in crate::interpreter) fn eval_stream_socket_client_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_stream(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs new file mode 100644 index 0000000000..12b849bdf8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_enable_crypto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - TLS enablement is conservative: disabling succeeds for valid streams while +//! enabling reports false because eval does not manage TLS state. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_enable_crypto", + area: Filesystem, + params: [ + stream, + enable, + crypto_method = EvalBuiltinDefaultValue::Null, + session_stream = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_socket_enable_crypto($stream, $enable, ...)`. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let enable = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_enable_crypto_result(stream, enable, context, values) +} + +/// Evaluates crypto status from already evaluated stream crypto arguments. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_enable_crypto_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Returns TLS enablement status for eval socket streams. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + if !context.stream_resources().has_stream(id) { + return values.bool_value(false); + } + let disabled = !values.truthy(enable)?; + values.bool_value(disabled) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs new file mode 100644 index 0000000000..b0e537456b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_get_name`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Socket resources use eval's one-based displayed resource ids and zero-based +//! internal stream table ids. + +eval_builtin! { + name: "stream_socket_get_name", + area: Filesystem, + params: [socket, remote], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_socket_get_name($socket, $remote)`. +pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [socket, remote] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let socket = eval_expr(socket, context, scope, values)?; + let remote = eval_expr(remote, context, scope, values)?; + eval_stream_socket_get_name_result(socket, remote, context, values) +} + +/// Returns a socket name for already evaluated socket and remote arguments. +pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [socket, remote] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_get_name_result(*socket, *remote, context, values) +} + +/// Returns a tracked local or remote socket endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_get_name_result( + socket: RuntimeCellHandle, + remote: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + let remote = values.truthy(remote)?; + match context.stream_resources().socket_name(id, remote) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } +} + +/// Converts a runtime resource cell into eval's zero-based socket id. +pub(in crate::interpreter) fn eval_socket_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs new file mode 100644 index 0000000000..444166302e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_pair`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Creates connected local stream resources in eval's stream resource table. + +eval_builtin! { + name: "stream_socket_pair", + area: Filesystem, + params: [domain, r#type, protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_socket_pair($domain, $type, $protocol)`. +pub(in crate::interpreter) fn eval_stream_socket_pair_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [domain, socket_type, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let _ = eval_expr(domain, context, scope, values)?; + let _ = eval_expr(socket_type, context, scope, values)?; + let _ = eval_expr(protocol, context, scope, values)?; + eval_stream_socket_pair_result(context, values) +} + +/// Creates a socket pair after validating already evaluated arguments. +pub(in crate::interpreter) fn eval_stream_socket_pair_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [_domain, _socket_type, _protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_pair_result(context, values) +} + +/// Creates a pair of connected local stream resources. +pub(in crate::interpreter) fn eval_stream_socket_pair_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { + return values.bool_value(false); + }; + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.resource(left)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.resource(right)?; + values.array_set(result, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs new file mode 100644 index 0000000000..5a3101acf4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs @@ -0,0 +1,124 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_recvfrom`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for address writeback. +//! +//! Key details: +//! - Reads delegate to `fread`, while optional address writeback uses tracked +//! remote endpoint metadata from eval's stream table. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_recvfrom", + area: Filesystem, + params: [ + socket, + length, + flags = EvalBuiltinDefaultValue::Int(0), + address: by_ref = EvalBuiltinDefaultValue::String("") + ], + by_ref: [address], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates a positional `stream_socket_recvfrom()` call without writable address output. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + if args.len() >= 4 { + values.warning( + "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", + )?; + } + eval_stream_socket_recvfrom_result(stream, length, context, values) +} + +/// Reads bytes from already evaluated socket receive arguments. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 4 { + values.warning( + "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", + )?; + } + eval_stream_socket_recvfrom_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let address_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, address) = eval_stream_socket_recvfrom_with_address_result( + socket.value, + length.value, + context, + values, + )?; + super::fsockopen::eval_write_socket_output_ref_target( + address_target.as_ref(), + address, + context, + values, + )?; + Ok(result) +} + +/// Reads bytes from a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::fread::eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + let address = context.stream_resources().socket_name(id, true); + let result = super::fread::eval_fread_result(stream, length, context, values)?; + Ok((result, address)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs new file mode 100644 index 0000000000..9485d2f3bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs @@ -0,0 +1,65 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_sendto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Connected socket writes delegate to the same eval stream write path as `fwrite`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_sendto", + area: Filesystem, + params: [ + socket, + data, + flags = EvalBuiltinDefaultValue::Int(0), + address = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_socket_sendto($socket, $data, $flags = 0, $address = "")`. +pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let data = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_sendto_result(stream, data, context, values) +} + +/// Writes bytes from already evaluated socket send arguments. +pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_sendto_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Writes bytes to a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_sendto_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::fwrite::eval_fwrite_result(stream, data, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs new file mode 100644 index 0000000000..557989e9bb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_server`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Opened listeners enter eval's normal stream table. + +eval_builtin! { + name: "stream_socket_server", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Evaluates `stream_socket_server($address)`. +pub(in crate::interpreter) fn eval_stream_socket_server_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_server_result(address, context, values) +} + +/// Opens a listener from an already evaluated address argument. +pub(in crate::interpreter) fn eval_stream_socket_server_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_server_result(*address, context, values) +} + +/// Opens a TCP listener resource. +pub(in crate::interpreter) fn eval_stream_socket_server_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_listener(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs new file mode 100644 index 0000000000..328d4aeabe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_socket_shutdown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Applies shutdown modes through eval's stream resource table. + +eval_builtin! { + name: "stream_socket_shutdown", + area: Filesystem, + params: [stream, mode], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_socket_shutdown($stream, $mode)`. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_stream_socket_shutdown_result(stream, mode, context, values) +} + +/// Shuts down an already evaluated socket stream argument. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, mode] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_shutdown_result(*stream, *mode, context, values) +} + +/// Applies a socket shutdown mode to a stream resource. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( + stream: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + let mode = eval_int_value(mode, values)?; + values.bool_value( + context + .stream_resources() + .socket_shutdown(id, mode) + .unwrap_or(false), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs new file mode 100644 index 0000000000..a2218111a1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_wrapper_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Registers protocols in the eval stream wrapper registry. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_wrapper_register", + area: Filesystem, + params: [ + protocol, + r#class, + flags = EvalBuiltinDefaultValue::Int(0) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_wrapper_register($protocol, $class, $flags = 0)`. +pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_stream_wrapper_register_result(&evaluated_args, context, values) +} + +/// Registers an already evaluated stream wrapper protocol and class. +pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_stream_wrapper_register_result(evaluated_args, context, values) +} + +/// Registers a materialized stream wrapper protocol and class. +pub(in crate::interpreter) fn eval_stream_wrapper_register_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let protocol = eval_stream_wrapper_protocol(evaluated_args[0], values)?; + let class_name = eval_stream_wrapper_class(evaluated_args[1], context, values)?; + values.bool_value(context.stream_resources_mut().register_stream_wrapper( + &protocol, + &class_name, + EVAL_STREAM_WRAPPERS, + )) +} + +/// Coerces one stream wrapper protocol argument into an owned string. +pub(in crate::interpreter) fn eval_stream_wrapper_protocol( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(protocol)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Coerces one stream wrapper class argument into a resolved class-name string. +fn eval_stream_wrapper_class( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8_lossy(&bytes).into_owned(); + Ok(context + .resolve_class_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs new file mode 100644 index 0000000000..8ea767eecc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_wrapper_restore`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Restores protocols in the eval stream wrapper registry. + +eval_builtin! { + name: "stream_wrapper_restore", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_wrapper_restore($protocol)`. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_stream_wrapper_restore_result(protocol, context, values) +} + +/// Restores an already evaluated stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_wrapper_restore_result(*protocol, context, values) +} + +/// Restores a materialized stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_result( + protocol: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = super::stream_wrapper_register::eval_stream_wrapper_protocol(protocol, values)?; + values.bool_value( + context + .stream_resources_mut() + .restore_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs new file mode 100644 index 0000000000..b8647cae1f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `stream_wrapper_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Unregisters protocols in the eval stream wrapper registry. + +eval_builtin! { + name: "stream_wrapper_unregister", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `stream_wrapper_unregister($protocol)`. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_stream_wrapper_unregister_result(protocol, context, values) +} + +/// Unregisters an already evaluated stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_wrapper_unregister_result(*protocol, context, values) +} + +/// Unregisters a materialized stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_result( + protocol: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = super::stream_wrapper_register::eval_stream_wrapper_protocol(protocol, values)?; + values.bool_value( + context + .stream_resources_mut() + .unregister_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs new file mode 100644 index 0000000000..c62e7149ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -0,0 +1,94 @@ +//! Purpose: +//! Shared stream argument coercions for eval filesystem stream builtins. +//! +//! Called from: +//! - Leaf filesystem stream builtin files that need stream-resource coercions. +//! +//! Key details: +//! - Runtime resource payloads are zero-based while PHP-visible ids are +//! one-based resource handles. + +use super::super::super::*; +/// Converts a runtime resource cell into eval's zero-based stream id. +pub(in crate::interpreter) fn eval_stream_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a stream length argument into a non-negative `usize`. +pub(in crate::interpreter) fn eval_nonnegative_usize( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts an optional stream length where null and -1 mean "read all". +pub(in crate::interpreter) fn eval_optional_stream_length( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value == -1 { + return Ok(None); + } + Ok(Some( + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, + )) +} + +/// Converts an optional absolute stream offset where null and -1 mean no seek. +pub(in crate::interpreter) fn eval_optional_stream_offset( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value < 0 { + Ok(None) + } else { + Ok(Some(value)) + } +} + +/// Converts one runtime cell to a UTF-8 string for stream mode arguments. +pub(in crate::interpreter) fn eval_stream_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Converts an optional one-byte delimiter argument to a byte value. +pub(in crate::interpreter) fn eval_optional_delimiter( + value: Option, + default: u8, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(default); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(default); + } + Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs new file mode 100644 index 0000000000..5783332496 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `symlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "symlink", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `symlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_symlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::copy::eval_builtin_binary_path_bool("symlink", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `symlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_symlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("symlink", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs new file mode 100644 index 0000000000..789b1d91c2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `sys_get_temp_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Returns the same temporary directory literal as the native static builtin. + +eval_builtin! { + name: "sys_get_temp_dir", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Evaluates `sys_get_temp_dir()` with no arguments. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_call( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_sys_get_temp_dir(args, values) +} + +/// Evaluates `sys_get_temp_dir()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs new file mode 100644 index 0000000000..241a0369a7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs @@ -0,0 +1,94 @@ +//! Purpose: +//! Declarative eval registry entry for `tempnam`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the temporary-name helper. + +eval_builtin! { + name: "tempnam", + area: Filesystem, + params: [directory, prefix], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use super::*; + +/// Dispatches direct eval calls for the `tempnam` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tempnam_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_tempnam(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `tempnam` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tempnam_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory, prefix] => eval_tempnam_result(*directory, *prefix, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_tempnam( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +pub(in crate::interpreter) fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +pub(in crate::interpreter) fn eval_tempnam_filename( + prefix: &str, + nonce: u128, + attempt: u32, +) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs new file mode 100644 index 0000000000..db27dd868b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Declarative eval registry entry for `tmpfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the temporary stream helper. + +eval_builtin! { + name: "tmpfile", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `tmpfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tmpfile_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_tmpfile(args, context, values) +} + +/// Dispatches evaluated-argument calls for the `tmpfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tmpfile_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_tmpfile_result(context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `tmpfile()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_tmpfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_tmpfile_result(context, values) +} + +/// Creates an anonymous temporary file stream resource or returns PHP false. +pub(in crate::interpreter) fn eval_tmpfile_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match context.stream_resources_mut().open_tmpfile() { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs new file mode 100644 index 0000000000..8bc09e7b09 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs @@ -0,0 +1,183 @@ +//! Purpose: +//! Declarative eval registry entry for `touch`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the timestamp mutation helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "touch", + area: Filesystem, + params: [ + filename, + mtime = EvalBuiltinDefaultValue::Null, + atime = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `touch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_touch_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_touch(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `touch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_touch_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, context, values), + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, context, values), + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, context, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, context, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let metadata_value = eval_touch_metadata_value(mtime, atime, values)?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_TOUCH, + metadata_value, + context, + values, + )? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Builds the `[mtime, atime]` array passed to wrapper `stream_metadata()`. +fn eval_touch_metadata_value( + mtime: std::time::SystemTime, + atime: std::time::SystemTime, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.int(eval_system_time_to_unix(mtime).ok_or(EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.int(eval_system_time_to_unix(atime).ok_or(EvalStatus::RuntimeFatal)?)?; + values.array_set(result, key, value) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +pub(in crate::interpreter) fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +pub(in crate::interpreter) fn eval_system_time_from_unix( + seconds: i64, +) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} + +/// Converts a `SystemTime` back to whole Unix seconds for wrapper metadata. +fn eval_system_time_to_unix(time: std::time::SystemTime) -> Option { + match time.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => i64::try_from(duration.as_secs()).ok(), + Err(error) => i64::try_from(error.duration().as_secs()) + .ok() + .map(|seconds| -seconds), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs new file mode 100644 index 0000000000..3b2f1c97c0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs @@ -0,0 +1,79 @@ +//! Purpose: +//! Declarative eval registry entry for `umask`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process umask helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "umask", + area: Filesystem, + params: [mask = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `umask` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_umask_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_umask(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `umask` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_umask_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_umask_result(None, values), + [mask] => eval_umask_result(Some(*mask), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +pub(in crate::interpreter) fn eval_builtin_umask( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +pub(in crate::interpreter) fn eval_umask_result( + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs new file mode 100644 index 0000000000..c0856464de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Declarative eval registry entry for `unlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unlink helper. + +eval_builtin! { + name: "unlink", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; +use crate::stream_wrappers; +use super::*; + +/// Dispatches direct eval calls for the `unlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_unlink(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `unlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_unlink_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, context, values) +} + +/// Deletes one path and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unlink_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if stream_wrappers::is_phar_stream(&path) { + return values.bool_value(elephc_phar::delete_url_bytes(path.as_bytes()).is_some()); + } + if let Some(result) = eval_user_wrapper_unlink_result(&path, context, values)? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + values.bool_value(std::fs::remove_file(path).is_ok()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs new file mode 100644 index 0000000000..af39e7e07d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Dispatches eval userspace stream wrappers to `stream_cast()`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_select`. +//! +//! Key details: +//! - Magician keeps `stream_select()` conservative and returns no ready streams, +//! but wrapper `stream_cast(STREAM_CAST_FOR_SELECT)` is still PHP-observable. + +use super::super::super::*; + +/// PHP's `STREAM_CAST_FOR_SELECT` value passed to wrapper `stream_cast()`. +pub(in crate::interpreter) const EVAL_STREAM_CAST_FOR_SELECT: i64 = 3; + +/// Invokes `stream_cast($cast_as)` for a userspace-wrapper stream resource. +pub(in crate::interpreter) fn eval_user_wrapper_stream_cast_result( + id: i64, + cast_as: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_cast)) = + eval_user_wrapper_method(&info.class_name, "stream_cast", context) + else { + return Ok(None); + }; + let cast_as = values.int(cast_as)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_cast, + info.object, + positional_args(vec![cast_as]), + context, + values, + )?; + Ok(Some(result)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs new file mode 100644 index 0000000000..592cbc9d0f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs @@ -0,0 +1,140 @@ +//! Purpose: +//! Dispatches control-style userspace stream wrapper methods for eval streams. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for flush, tell, and +//! truncate builtins on userspace wrapper resources. +//! +//! Key details: +//! - File-backed resources keep using `EvalStreamResources`; these helpers only +//! intercept resources created by `stream_wrapper_register()` + `fopen()`. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `fflush()` to a wrapper object's `stream_flush()`. +pub(in crate::interpreter) fn eval_user_wrapper_fflush_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(result) = eval_user_wrapper_call_no_arg_method(id, "stream_flush", context, values)? + else { + return values.bool_value(false).map(Some); + }; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} + +/// Dispatches `ftell()` to a wrapper object's `stream_tell()`. +pub(in crate::interpreter) fn eval_user_wrapper_ftell_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(result) = eval_user_wrapper_call_no_arg_method(id, "stream_tell", context, values)? + else { + return values.bool_value(false).map(Some); + }; + if values.type_tag(result)? == EVAL_TAG_BOOL && !values.truthy(result)? { + values.release(result)?; + return values.bool_value(false).map(Some); + } + let position = eval_int_value(result, values)?; + values.release(result)?; + values.int(position).map(Some) +} + +/// Dispatches `ftruncate()` to a wrapper object's `stream_truncate()`. +pub(in crate::interpreter) fn eval_user_wrapper_ftruncate_result( + id: i64, + size: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_truncate)) = + eval_user_wrapper_method(&info.class_name, "stream_truncate", context) + else { + return values.bool_value(false).map(Some); + }; + let size = values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_truncate, + info.object, + positional_args(vec![size]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} + +/// Dispatches `flock()` to a wrapper object's `stream_lock()`. +pub(in crate::interpreter) fn eval_user_wrapper_flock_result( + id: i64, + operation: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_lock)) = + eval_user_wrapper_method(&info.class_name, "stream_lock", context) + else { + return Ok(Some(false)); + }; + let operation = values.int(operation)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_lock, + info.object, + positional_args(vec![operation]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + Ok(Some(ok)) +} + +/// Calls one no-argument userspace wrapper method on a stream resource. +fn eval_user_wrapper_call_no_arg_method( + id: i64, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, method)) = + eval_user_wrapper_method(&info.class_name, method_name, context) + else { + return Ok(None); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &method, + info.object, + Vec::new(), + context, + values, + )?; + Ok(Some(result)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs new file mode 100644 index 0000000000..1987339909 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs @@ -0,0 +1,146 @@ +//! Purpose: +//! Dispatches eval directory builtins to userspace stream-wrapper directory methods. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::opendir` and `closedir` +//! builtin owners for directory-resource operations. +//! +//! Key details: +//! - `dir_opendir()` owns the wrapper object for the directory resource lifetime; +//! `dir_closedir()` releases it, while `readdir()`/`rewinddir()` reuse it. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `opendir($path)` to a wrapper object's `dir_opendir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_opendir_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, dir_opendir)) = + eval_user_wrapper_method(class.name(), "dir_opendir", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path_arg = values.string(path)?; + let options = values.int(0)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &dir_opendir, + object, + positional_args(vec![path_arg, options]), + context, + values, + )?; + let opened = values.truthy(result)?; + values.release(result)?; + if !opened { + values.release(object)?; + return values.bool_value(false).map(Some); + } + let id = context + .stream_resources_mut() + .open_user_wrapper_directory(object, class.name()); + values.resource(id).map(Some) +} + +/// Dispatches `closedir($handle)` to a wrapper object's `dir_closedir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_closedir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, dir_closedir)) = + eval_user_wrapper_method(&info.class_name, "dir_closedir", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_closedir, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + if let Some(info) = context + .stream_resources_mut() + .close_user_wrapper_directory(id) + { + values.release(info.object)?; + } + values.null().map(Some) +} + +/// Dispatches `readdir($handle)` to a wrapper object's `dir_readdir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_readdir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + let Some((declaring_class, dir_readdir)) = + eval_user_wrapper_method(&info.class_name, "dir_readdir", context) + else { + return values.bool_value(false).map(Some); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_readdir, + info.object, + Vec::new(), + context, + values, + )?; + if values.type_tag(result)? == EVAL_TAG_STRING && values.string_bytes(result)?.is_empty() { + values.release(result)?; + return values.bool_value(false).map(Some); + } + Ok(Some(result)) +} + +/// Dispatches `rewinddir($handle)` to a wrapper object's `dir_rewinddir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_rewinddir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, dir_rewinddir)) = + eval_user_wrapper_method(&info.class_name, "dir_rewinddir", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_rewinddir, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs new file mode 100644 index 0000000000..e40395b2c0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs @@ -0,0 +1,129 @@ +//! Purpose: +//! Dispatches one-shot file I/O builtins through eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::file`, `file_get_contents`, +//! `readfile`, and `file_put_contents` for one-shot wrapper I/O. +//! +//! Key details: +//! - These helpers open a temporary wrapper stream, perform the requested read or +//! write through stream methods, and close the wrapper resource before returning. + +use super::super::super::*; + +/// Reads a full userspace-wrapper path into a PHP string cell. +pub(in crate::interpreter) fn eval_user_wrapper_file_get_contents_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "r", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_stream_get_contents_result(id, None, None, context, values)? + { + Some(result) => result, + None => values.bool_value(false)?, + }; + eval_user_wrapper_close_one_shot(id, context, values)?; + Ok(Some(result)) +} + +/// Streams one userspace-wrapper path to eval output and returns the byte count. +pub(in crate::interpreter) fn eval_user_wrapper_readfile_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "r", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_stream_get_contents_result(id, None, None, context, values)? + { + Some(result) => result, + None => values.bool_value(false)?, + }; + if values.type_tag(result)? != EVAL_TAG_STRING { + eval_user_wrapper_close_one_shot(id, context, values)?; + return Ok(Some(result)); + } + let bytes = values.string_bytes(result)?; + values.echo(result)?; + eval_user_wrapper_close_one_shot(id, context, values)?; + values + .int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) + .map(Some) +} + +/// Writes bytes to one userspace-wrapper path and returns the wrapper byte count. +pub(in crate::interpreter) fn eval_user_wrapper_file_put_contents_result( + path: &str, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "w", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_fwrite_result(id, data, context, values)? { + Some(result) => result, + None => values.bool_value(false)?, + }; + eval_user_wrapper_close_one_shot(id, context, values)?; + Ok(Some(result)) +} + +/// Opens one userspace-wrapper file path and returns its resource id and cell. +fn eval_user_wrapper_open_file_path( + path: &str, + mode: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut scope = ElephcEvalScope::new(); + let Some(opened) = eval_user_wrapper_fopen_result(path, mode, context, &mut scope, values)? + else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some((-1, opened))); + } + let id = eval_user_wrapper_file_resource_id(opened, values)?; + Ok(Some((id, opened))) +} + +/// Closes a one-shot wrapper stream and releases the close result cell. +fn eval_user_wrapper_close_one_shot( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if id < 0 { + return Ok(()); + } + if let Some(closed) = eval_user_wrapper_fclose_result(id, context, values)? { + values.release(closed)?; + } + Ok(()) +} + +/// Converts a PHP resource cell into eval's zero-based stream resource id. +fn eval_user_wrapper_file_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs new file mode 100644 index 0000000000..186bf31100 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Implements line-oriented reads for eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for `fgets()` and +//! `stream_get_line()` on userspace wrapper resources. +//! +//! Key details: +//! - Reads flow through the wrapper object's `stream_read()`/`stream_eof()` +//! methods, preserving the same wrapper state used by aggregate reads. + +use super::super::super::*; +use super::user_wrapper_streams::{ + eval_user_wrapper_eof_bool, eval_user_wrapper_read_bytes, +}; + +/// Dispatches `fgets()` to a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_fgets_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_line_bytes(id, usize::MAX, None, true, true, context, values)? + else { + return values.bool_value(false).map(Some); + }; + if bytes.is_empty() { + return values.bool_value(false).map(Some); + } + values.string_bytes_value(&bytes).map(Some) +} + +/// Dispatches `stream_get_line()` to a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_stream_get_line_result( + id: i64, + length: usize, + ending: Option<&[u8]>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_line_bytes(id, length, ending, false, false, context, values)? + else { + return values.bool_value(false).map(Some); + }; + if bytes.is_empty() { + return values.bool_value(false).map(Some); + } + values.string_bytes_value(&bytes).map(Some) +} + +/// Reads one wrapper line up to a limit, newline, or custom delimiter. +fn eval_user_wrapper_line_bytes( + id: i64, + length: usize, + ending: Option<&[u8]>, + include_ending: bool, + stop_at_newline: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result>, EvalStatus> { + let mut output = Vec::new(); + while output.len() < length { + if eval_user_wrapper_eof_bool(id, context, values)? { + break; + } + let Some(chunk) = eval_user_wrapper_read_bytes(id, 1, context, values)? else { + return Ok(None); + }; + if chunk.is_empty() { + break; + } + output.push(chunk[0]); + if let Some(ending) = ending { + if !ending.is_empty() && output.ends_with(ending) { + if !include_ending { + output.truncate(output.len().saturating_sub(ending.len())); + } + break; + } + } else if stop_at_newline && chunk[0] == b'\n' { + break; + } + } + Ok(Some(output)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs new file mode 100644 index 0000000000..f6e06e80e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Dispatches path-based metadata changes to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::touch`, `chmod`, and `chown` +//! builtin owners when the path scheme is a registered wrapper. +//! +//! Key details: +//! - Mirrors the AOT stream-wrapper metadata options used by filesystem +//! builtins; non-wrapper paths return `None` so local host operations continue. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +pub(in crate::interpreter) const EVAL_STREAM_META_TOUCH: i64 = 1; +pub(in crate::interpreter) const EVAL_STREAM_META_OWNER_NAME: i64 = 2; +pub(in crate::interpreter) const EVAL_STREAM_META_OWNER: i64 = 3; +pub(in crate::interpreter) const EVAL_STREAM_META_GROUP_NAME: i64 = 4; +pub(in crate::interpreter) const EVAL_STREAM_META_GROUP: i64 = 5; +pub(in crate::interpreter) const EVAL_STREAM_META_ACCESS: i64 = 6; + +/// Dispatches one `stream_metadata($path, $option, $value)` wrapper call. +pub(in crate::interpreter) fn eval_user_wrapper_stream_metadata_result( + path: &str, + option: i64, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, stream_metadata)) = + eval_user_wrapper_method(class.name(), "stream_metadata", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let option = values.int(option)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &stream_metadata, + object, + positional_args(vec![path, option, value]), + context, + values, + )?; + values.release(object)?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs new file mode 100644 index 0000000000..9c00d25c6b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Dispatches stream option builtins to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_set_blocking` and +//! `stream_set_timeout` when the stream is wrapper-backed. +//! +//! Key details: +//! - Mirrors the generated runtime's `stream_set_option($option, $arg1, $arg2)` +//! dispatch for synthetic wrapper descriptors. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +pub(in crate::interpreter) const EVAL_STREAM_OPTION_BLOCKING: i64 = 1; +pub(in crate::interpreter) const EVAL_STREAM_OPTION_READ_TIMEOUT: i64 = 4; + +/// Dispatches a stream option update to a wrapper object's `stream_set_option()`. +pub(in crate::interpreter) fn eval_user_wrapper_stream_set_option_result( + id: i64, + option: i64, + arg1: i64, + arg2: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_set_option)) = + eval_user_wrapper_method(&info.class_name, "stream_set_option", context) + else { + return values.bool_value(false).map(Some); + }; + let option = values.int(option)?; + let arg1 = values.int(arg1)?; + let arg2 = values.int(arg2)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_set_option, + info.object, + positional_args(vec![option, arg1, arg2]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs new file mode 100644 index 0000000000..fc6b9942bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Dispatches path mutation builtins to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::unlink`, `copy`, and `chdir` +//! builtin owners when the source path scheme is registered. +//! +//! Key details: +//! - Path methods use a throwaway wrapper instance, matching the generated +//! runtime's path-op helpers instead of reusing open stream resources. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `unlink($path)` to a wrapper object's `unlink()` method. +pub(in crate::interpreter) fn eval_user_wrapper_unlink_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_user_wrapper_path_method_result(path, "unlink", context, values, |_| Ok(Vec::new())) +} + +/// Dispatches `mkdir($path)` or `rmdir($path)` to the registered wrapper. +pub(in crate::interpreter) fn eval_user_wrapper_single_path_op_result( + name: &str, + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match name { + "mkdir" => eval_user_wrapper_path_method_result(path, name, context, values, |values| { + Ok(vec![values.int(0)?, values.int(0)?]) + }), + "rmdir" => eval_user_wrapper_path_method_result(path, name, context, values, |values| { + Ok(vec![values.int(0)?]) + }), + _ => Ok(None), + } +} + +/// Dispatches `rename($from, $to)` using the source path's wrapper scheme. +pub(in crate::interpreter) fn eval_user_wrapper_rename_result( + from: &str, + to: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_user_wrapper_path_method_result(from, "rename", context, values, |values| { + Ok(vec![values.string(to)?]) + }) +} + +/// Instantiates the wrapper for one path and invokes a boolean path method. +fn eval_user_wrapper_path_method_result( + path: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut V, + extra_args: impl FnOnce(&mut V) -> Result, EvalStatus>, +) -> Result, EvalStatus> +where + V: RuntimeValueOps, +{ + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, method)) = + eval_user_wrapper_method(class.name(), method_name, context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let extra_args = match extra_args(values) { + Ok(args) => args, + Err(status) => { + values.release(object)?; + values.release(path)?; + return Err(status); + } + }; + let mut args = Vec::with_capacity(extra_args.len() + 1); + args.push(path); + args.extend(extra_args); + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &method, + object, + positional_args(args), + context, + values, + )?; + values.release(object)?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs new file mode 100644 index 0000000000..a96813d125 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Interprets userspace stream-wrapper stat results for path and stream builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::file_exists`, `stat`, +//! `filesize`, and `filetype` for wrapper-backed paths. +//! - `crate::interpreter::builtins::filesystem::streams` when `fstat()` sees a +//! userspace-wrapper stream resource. +//! +//! Key details: +//! - The wrapper owns the stat array shape. These helpers read the PHP-standard +//! string keys used by file probes, scalar stat builtins, and `filetype()`. + +use super::super::super::*; + +/// Dispatches `fstat()` to a wrapper object's `stream_stat()`. +pub(in crate::interpreter) fn eval_user_wrapper_fstat_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_stat)) = + eval_user_wrapper_method(&info.class_name, "stream_stat", context) + else { + return values.bool_value(false).map(Some); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_stat, + info.object, + Vec::new(), + context, + values, + )?; + Ok(Some(result)) +} + +/// Computes one filesystem predicate from a userspace wrapper `url_stat()` result. +pub(in crate::interpreter) fn eval_user_wrapper_file_probe_from_stat( + name: &str, + stat: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.truthy(stat)? { + return values.bool_value(false); + } + let mode = eval_user_wrapper_stat_int_field(stat, "mode", values)?.unwrap_or(0); + let result = match name { + "file_exists" => true, + "is_dir" => eval_mode_kind(mode) == libc::S_IFDIR as i64, + "is_executable" => mode & 0o111 != 0, + "is_file" => eval_mode_kind(mode) == libc::S_IFREG as i64, + "is_link" => eval_mode_kind(mode) == libc::S_IFLNK as i64, + "is_readable" => mode & 0o444 != 0, + "is_writable" | "is_writeable" => mode & 0o222 != 0, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} + +/// Returns one scalar stat builtin value from a userspace wrapper stat array. +pub(in crate::interpreter) fn eval_user_wrapper_file_stat_scalar_from_stat( + name: &str, + stat: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let field = match name { + "fileatime" => "atime", + "filectime" => "ctime", + "filegroup" => "gid", + "fileinode" => "ino", + "filemtime" => "mtime", + "fileowner" => "uid", + "fileperms" => "mode", + _ => return Err(EvalStatus::RuntimeFatal), + }; + match eval_user_wrapper_stat_int_field(stat, field, values)? { + Some(value) => values.int(value), + None if name == "filemtime" => values.int(0), + None => values.bool_value(false), + } +} + +/// Extracts one integer field from a userspace wrapper stat result. +pub(in crate::interpreter) fn eval_user_wrapper_stat_int_field( + stat: RuntimeCellHandle, + field: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.truthy(stat)? { + return Ok(None); + } + let key = values.string(field)?; + let value = values.array_get(stat, key)?; + Ok(Some(eval_int_value(value, values)?)) +} + +/// Maps one POSIX mode value to PHP's `filetype()` label. +pub(in crate::interpreter) fn eval_filetype_label_from_mode(mode: i64) -> &'static str { + match eval_mode_kind(mode) { + kind if kind == libc::S_IFREG as i64 => "file", + kind if kind == libc::S_IFDIR as i64 => "dir", + kind if kind == libc::S_IFLNK as i64 => "link", + kind if kind == libc::S_IFCHR as i64 => "char", + kind if kind == libc::S_IFBLK as i64 => "block", + kind if kind == libc::S_IFIFO as i64 => "fifo", + kind if kind == libc::S_IFSOCK as i64 => "socket", + _ => "unknown", + } +} + +/// Masks one POSIX mode value down to its file-kind bits. +fn eval_mode_kind(mode: i64) -> i64 { + mode & (libc::S_IFMT as i64) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs new file mode 100644 index 0000000000..7cb4969cdf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -0,0 +1,475 @@ +//! Purpose: +//! Dispatches eval userspace stream wrapper resources into wrapper class methods. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for `fopen()`, +//! `fread()`, `fwrite()`, `feof()`, and `fclose()`. +//! +//! Key details: +//! - Registered wrapper resources keep the wrapper object in eval-local resource +//! state; file-backed streams continue to use the normal host-file path. +//! - `stream_open()` receives a synthetic by-ref `opened_path` cell. Magician +//! accepts writes to it, but currently keeps the original URL as the stream URI. + +use super::super::super::*; + +/// Opens a registered eval userspace stream wrapper, or reports no match. +pub(in crate::interpreter) fn eval_user_wrapper_fopen_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(filename) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, stream_open)) = + eval_user_wrapper_method(class.name(), "stream_open", context) + else { + return values.bool_value(false).map(Some); + }; + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, scope, values)?; + let open_args = eval_user_wrapper_stream_open_args(filename, mode, values)?; + let open_result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &stream_open, + object, + open_args, + context, + values, + )?; + let opened = values.truthy(open_result)?; + values.release(open_result)?; + if !opened { + values.release(object)?; + return values.bool_value(false).map(Some); + } + let id = + context + .stream_resources_mut() + .open_user_wrapper_stream(object, class.name(), filename, mode); + values.resource(id).map(Some) +} + +/// Dispatches `fclose()` to `stream_close()` for a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_fclose_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, stream_close)) = + eval_user_wrapper_method(&info.class_name, "stream_close", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_close, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + let closed = context.stream_resources_mut().close(id); + values.release(info.object)?; + values.bool_value(closed).map(Some) +} + +/// Dispatches `fread()` or `fgetc()` to a wrapper object's `stream_read()`. +pub(in crate::interpreter) fn eval_user_wrapper_fread_result( + id: i64, + length: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(bytes) = eval_user_wrapper_read_bytes(id, length, context, values)? else { + return Ok(None); + }; + values.string_bytes_value(&bytes).map(Some) +} + +/// Dispatches `fwrite()` to a wrapper object's `stream_write()`. +pub(in crate::interpreter) fn eval_user_wrapper_fwrite_result( + id: i64, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(written) = eval_user_wrapper_write_bytes(id, data, context, values)? else { + return Ok(None); + }; + values.int(written).map(Some) +} + +/// Dispatches `feof()` to a wrapper object's `stream_eof()`. +pub(in crate::interpreter) fn eval_user_wrapper_feof_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let eof = eval_user_wrapper_eof_bool(id, context, values)?; + values.bool_value(eof).map(Some) +} + +/// Dispatches `fseek()` or `rewind()` to a wrapper object's `stream_seek()`. +pub(in crate::interpreter) fn eval_user_wrapper_fseek_result( + id: i64, + offset: i64, + whence: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + eval_user_wrapper_seek_bool(id, offset, whence, context, values).map(Some) +} + +/// Reads the remaining or bounded contents from a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_stream_get_contents_result( + id: i64, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_contents_bytes(id, length, offset, context, values)? + else { + return values.bool_value(false).map(Some); + }; + values.string_bytes_value(&bytes).map(Some) +} + +/// Copies bytes between streams when either endpoint is a userspace wrapper. +pub(in crate::interpreter) fn eval_user_wrapper_stream_copy_to_stream_result( + from: i64, + to: i64, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let from_is_wrapper = context.stream_resources().user_wrapper_stream_info(from).is_some(); + let to_is_wrapper = context.stream_resources().user_wrapper_stream_info(to).is_some(); + if !from_is_wrapper && !to_is_wrapper { + return Ok(None); + } + let bytes = if from_is_wrapper { + let Some(bytes) = + eval_user_wrapper_contents_bytes(from, length, offset, context, values)? + else { + return values.bool_value(false).map(Some); + }; + bytes + } else { + let Some(bytes) = context + .stream_resources_mut() + .get_contents(from, length, offset) + else { + return values.bool_value(false).map(Some); + }; + bytes + }; + let written = if to_is_wrapper { + let Some(written) = eval_user_wrapper_write_bytes(to, &bytes, context, values)? else { + return values.bool_value(false).map(Some); + }; + written + } else { + let Some(written) = context.stream_resources_mut().write(to, &bytes) else { + return values.bool_value(false).map(Some); + }; + i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)? + }; + values.int(written).map(Some) +} + +/// Streams remaining wrapper bytes to eval output and returns the byte count. +pub(in crate::interpreter) fn eval_user_wrapper_fpassthru_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = eval_user_wrapper_contents_bytes(id, None, None, context, values)? else { + return values.bool_value(false).map(Some); + }; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(len).map(Some) +} + +/// Dispatches path-based filesystem probes to a wrapper object's `url_stat()`. +pub(in crate::interpreter) fn eval_user_wrapper_url_stat_result( + path: &str, + flags: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, url_stat)) = + eval_user_wrapper_method(class.name(), "url_stat", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let flags = values.int(flags)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &url_stat, + object, + positional_args(vec![path, flags]), + context, + values, + )?; + values.release(object)?; + Ok(Some(result)) +} + +/// Reads one chunk from a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_read_bytes( + id: i64, + length: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result>, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_read)) = + eval_user_wrapper_method(&info.class_name, "stream_read", context) + else { + return Ok(None); + }; + let length = values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_read, + info.object, + positional_args(vec![length]), + context, + values, + )?; + let bytes = values.string_bytes(result)?; + values.release(result)?; + context + .stream_resources_mut() + .set_user_wrapper_eof(id, bytes.is_empty()); + Ok(Some(bytes)) +} + +/// Writes one byte slice to a userspace-wrapper stream. +fn eval_user_wrapper_write_bytes( + id: i64, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_write)) = + eval_user_wrapper_method(&info.class_name, "stream_write", context) + else { + return Ok(None); + }; + let data = values.string_bytes_value(data)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_write, + info.object, + positional_args(vec![data]), + context, + values, + )?; + let written = eval_int_value(result, values)?; + values.release(result)?; + context.stream_resources_mut().set_user_wrapper_eof(id, false); + Ok(Some(written)) +} + +/// Reads wrapper bytes until EOF or a finite length cap. +fn eval_user_wrapper_contents_bytes( + id: i64, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result>, EvalStatus> { + if let Some(offset) = offset { + if !eval_user_wrapper_seek_bool(id, offset, 0, context, values)? { + return Ok(None); + } + } + let mut bytes = Vec::new(); + loop { + if length.is_some_and(|limit| bytes.len() >= limit) { + break; + } + if eval_user_wrapper_eof_bool(id, context, values)? { + break; + } + let remaining = length + .map(|limit| limit.saturating_sub(bytes.len())) + .unwrap_or(8192); + let Some(mut chunk) = + eval_user_wrapper_read_bytes(id, remaining, context, values)? + else { + return Ok(None); + }; + if chunk.is_empty() { + break; + } + if let Some(limit) = length { + chunk.truncate(limit.saturating_sub(bytes.len())); + } + bytes.extend_from_slice(&chunk); + } + Ok(Some(bytes)) +} + +/// Returns a userspace-wrapper EOF result, falling back to cached EOF state. +pub(in crate::interpreter) fn eval_user_wrapper_eof_bool( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(false); + }; + let Some((declaring_class, stream_eof)) = + eval_user_wrapper_method(&info.class_name, "stream_eof", context) + else { + return Ok(info.eof); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_eof, + info.object, + Vec::new(), + context, + values, + )?; + let eof = values.truthy(result)?; + values.release(result)?; + context.stream_resources_mut().set_user_wrapper_eof(id, eof); + Ok(eof) +} + +/// Returns a userspace-wrapper seek result, or false when the method is absent. +fn eval_user_wrapper_seek_bool( + id: i64, + offset: i64, + whence: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(false); + }; + let Some((declaring_class, stream_seek)) = + eval_user_wrapper_method(&info.class_name, "stream_seek", context) + else { + return Ok(false); + }; + let offset = values.int(offset)?; + let whence = values.int(whence)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_seek, + info.object, + positional_args(vec![offset, whence]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + if ok { + context.stream_resources_mut().set_user_wrapper_eof(id, false); + } + Ok(ok) +} + +/// Builds the four PHP arguments passed to a wrapper `stream_open()` method. +fn eval_user_wrapper_stream_open_args( + filename: &str, + mode: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let path = values.string(filename)?; + let mode = values.string(mode)?; + let options = values.int(0)?; + let opened_path = values.null()?; + Ok(vec![ + EvaluatedCallArg { + name: None, + value: path, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: mode, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: options, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: opened_path, + ref_target: Some(EvalReferenceTarget::Cell { cell: opened_path }), + }, + ]) +} + +/// Returns a callable eval-declared wrapper method. +pub(in crate::interpreter) fn eval_user_wrapper_method( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + let (declaring_class, method) = context.class_method(class_name, method_name)?; + if method.visibility() != EvalVisibility::Public || method.is_static() || method.is_abstract() { + return None; + } + Some((declaring_class, method)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs new file mode 100644 index 0000000000..6eef8321e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Routes evaluated-argument filesystem registry hooks to focused value dispatchers. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. +//! +//! Key details: +//! - Values hooks run after named/default argument binding has produced PHP +//! parameter order. + +use super::super::super::*; + + +/// Routes evaluated-argument filesystem builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_filesystem_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => super::basename::eval_basename_declared_values_result(evaluated_args, context, values), + "chdir" => super::chdir::eval_chdir_declared_values_result(evaluated_args, context, values), + "chgrp" => super::chgrp::eval_chgrp_declared_values_result(evaluated_args, context, values), + "chmod" => super::chmod::eval_chmod_declared_values_result(evaluated_args, context, values), + "chown" => super::chown::eval_chown_declared_values_result(evaluated_args, context, values), + "clearstatcache" => super::clearstatcache::eval_clearstatcache_declared_values_result(evaluated_args, context, values), + "closedir" => super::closedir::eval_closedir_declared_values_result(evaluated_args, context, values), + "copy" => super::copy::eval_copy_declared_values_result(evaluated_args, context, values), + "dirname" => super::dirname::eval_dirname_declared_values_result(evaluated_args, context, values), + "disk_free_space" => super::disk_free_space::eval_disk_free_space_declared_values_result(evaluated_args, context, values), + "disk_total_space" => super::disk_total_space::eval_disk_total_space_declared_values_result(evaluated_args, context, values), + "fclose" => super::fclose::eval_fclose_declared_values_result(evaluated_args, context, values), + "fdatasync" => super::fdatasync::eval_fdatasync_declared_values_result(evaluated_args, context, values), + "feof" => super::feof::eval_feof_declared_values_result(evaluated_args, context, values), + "fflush" => super::fflush::eval_fflush_declared_values_result(evaluated_args, context, values), + "fgetc" => super::fgetc::eval_fgetc_declared_values_result(evaluated_args, context, values), + "fgetcsv" => super::fgetcsv::eval_fgetcsv_declared_values_result(evaluated_args, context, values), + "fgets" => super::fgets::eval_fgets_declared_values_result(evaluated_args, context, values), + "file" => super::file::eval_file_declared_values_result(evaluated_args, context, values), + "file_exists" => super::file_exists::eval_file_exists_declared_values_result(evaluated_args, context, values), + "file_get_contents" => super::file_get_contents::eval_file_get_contents_declared_values_result(evaluated_args, context, values), + "file_put_contents" => super::file_put_contents::eval_file_put_contents_declared_values_result(evaluated_args, context, values), + "fileatime" => super::fileatime::eval_fileatime_declared_values_result(evaluated_args, context, values), + "filectime" => super::filectime::eval_filectime_declared_values_result(evaluated_args, context, values), + "filegroup" => super::filegroup::eval_filegroup_declared_values_result(evaluated_args, context, values), + "fileinode" => super::fileinode::eval_fileinode_declared_values_result(evaluated_args, context, values), + "filemtime" => super::filemtime::eval_filemtime_declared_values_result(evaluated_args, context, values), + "fileowner" => super::fileowner::eval_fileowner_declared_values_result(evaluated_args, context, values), + "fileperms" => super::fileperms::eval_fileperms_declared_values_result(evaluated_args, context, values), + "filesize" => super::filesize::eval_filesize_declared_values_result(evaluated_args, context, values), + "filetype" => super::filetype::eval_filetype_declared_values_result(evaluated_args, context, values), + "flock" => super::flock::eval_flock_declared_values_result(evaluated_args, context, values), + "fnmatch" => super::fnmatch::eval_fnmatch_declared_values_result(evaluated_args, context, values), + "fopen" => super::fopen::eval_fopen_declared_values_result(evaluated_args, context, values), + "fpassthru" => super::fpassthru::eval_fpassthru_declared_values_result(evaluated_args, context, values), + "fprintf" => super::fprintf::eval_fprintf_declared_values_result(evaluated_args, context, values), + "fputcsv" => super::fputcsv::eval_fputcsv_declared_values_result(evaluated_args, context, values), + "fread" => super::fread::eval_fread_declared_values_result(evaluated_args, context, values), + "fscanf" => super::fscanf::eval_fscanf_declared_values_result(evaluated_args, context, values), + "fseek" => super::fseek::eval_fseek_declared_values_result(evaluated_args, context, values), + "fsockopen" => super::fsockopen::eval_fsockopen_declared_values_result(evaluated_args, context, values), + "fstat" => super::fstat::eval_fstat_declared_values_result(evaluated_args, context, values), + "fsync" => super::fsync::eval_fsync_declared_values_result(evaluated_args, context, values), + "ftell" => super::ftell::eval_ftell_declared_values_result(evaluated_args, context, values), + "ftruncate" => super::ftruncate::eval_ftruncate_declared_values_result(evaluated_args, context, values), + "fwrite" => super::fwrite::eval_fwrite_declared_values_result(evaluated_args, context, values), + "getcwd" => super::getcwd::eval_getcwd_declared_values_result(evaluated_args, context, values), + "glob" => super::glob::eval_glob_declared_values_result(evaluated_args, context, values), + "is_dir" => super::is_dir::eval_is_dir_declared_values_result(evaluated_args, context, values), + "is_executable" => super::is_executable::eval_is_executable_declared_values_result(evaluated_args, context, values), + "is_file" => super::is_file::eval_is_file_declared_values_result(evaluated_args, context, values), + "is_link" => super::is_link::eval_is_link_declared_values_result(evaluated_args, context, values), + "is_readable" => super::is_readable::eval_is_readable_declared_values_result(evaluated_args, context, values), + "is_writable" => super::is_writable::eval_is_writable_declared_values_result(evaluated_args, context, values), + "is_writeable" => super::is_writeable::eval_is_writeable_declared_values_result(evaluated_args, context, values), + "lchgrp" => super::lchgrp::eval_lchgrp_declared_values_result(evaluated_args, context, values), + "lchown" => super::lchown::eval_lchown_declared_values_result(evaluated_args, context, values), + "link" => super::link::eval_link_declared_values_result(evaluated_args, context, values), + "linkinfo" => super::linkinfo::eval_linkinfo_declared_values_result(evaluated_args, context, values), + "lstat" => super::lstat::eval_lstat_declared_values_result(evaluated_args, context, values), + "mkdir" => super::mkdir::eval_mkdir_declared_values_result(evaluated_args, context, values), + "opendir" => super::opendir::eval_opendir_declared_values_result(evaluated_args, context, values), + "pathinfo" => super::pathinfo::eval_pathinfo_declared_values_result(evaluated_args, context, values), + "pclose" => super::pclose::eval_pclose_declared_values_result(evaluated_args, context, values), + "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_values_result(evaluated_args, context, values), + "popen" => super::popen::eval_popen_declared_values_result(evaluated_args, context, values), + "readdir" => super::readdir::eval_readdir_declared_values_result(evaluated_args, context, values), + "readfile" => super::readfile::eval_readfile_declared_values_result(evaluated_args, context, values), + "readline" => super::readline::eval_readline_declared_values_result(evaluated_args, context, values), + "readlink" => super::readlink::eval_readlink_declared_values_result(evaluated_args, context, values), + "realpath" => super::realpath::eval_realpath_declared_values_result(evaluated_args, context, values), + "realpath_cache_get" => super::realpath_cache_get::eval_realpath_cache_get_declared_values_result(evaluated_args, context, values), + "realpath_cache_size" => super::realpath_cache_size::eval_realpath_cache_size_declared_values_result(evaluated_args, context, values), + "rename" => super::rename::eval_rename_declared_values_result(evaluated_args, context, values), + "rewind" => super::rewind::eval_rewind_declared_values_result(evaluated_args, context, values), + "rewinddir" => super::rewinddir::eval_rewinddir_declared_values_result(evaluated_args, context, values), + "rmdir" => super::rmdir::eval_rmdir_declared_values_result(evaluated_args, context, values), + "scandir" => super::scandir::eval_scandir_declared_values_result(evaluated_args, context, values), + "stat" => super::stat::eval_stat_declared_values_result(evaluated_args, context, values), + "stream_bucket_append" => super::stream_bucket_append::eval_stream_bucket_append_declared_values_result(evaluated_args, context, values), + "stream_bucket_make_writeable" => super::stream_bucket_make_writeable::eval_stream_bucket_make_writeable_declared_values_result(evaluated_args, context, values), + "stream_bucket_new" => super::stream_bucket_new::eval_stream_bucket_new_declared_values_result(evaluated_args, context, values), + "stream_bucket_prepend" => super::stream_bucket_prepend::eval_stream_bucket_prepend_declared_values_result(evaluated_args, context, values), + "stream_context_create" => super::stream_context_create::eval_stream_context_create_declared_values_result(evaluated_args, context, values), + "stream_context_get_default" => super::stream_context_get_default::eval_stream_context_get_default_declared_values_result(evaluated_args, context, values), + "stream_context_get_options" => super::stream_context_get_options::eval_stream_context_get_options_declared_values_result(evaluated_args, context, values), + "stream_context_get_params" => super::stream_context_get_params::eval_stream_context_get_params_declared_values_result(evaluated_args, context, values), + "stream_context_set_default" => super::stream_context_set_default::eval_stream_context_set_default_declared_values_result(evaluated_args, context, values), + "stream_context_set_option" => super::stream_context_set_option::eval_stream_context_set_option_declared_values_result(evaluated_args, context, values), + "stream_context_set_params" => super::stream_context_set_params::eval_stream_context_set_params_declared_values_result(evaluated_args, context, values), + "stream_copy_to_stream" => super::stream_copy_to_stream::eval_stream_copy_to_stream_declared_values_result(evaluated_args, context, values), + "stream_filter_append" => super::stream_filter_append::eval_stream_filter_append_declared_values_result(evaluated_args, context, values), + "stream_filter_prepend" => super::stream_filter_prepend::eval_stream_filter_prepend_declared_values_result(evaluated_args, context, values), + "stream_filter_register" => super::stream_filter_register::eval_stream_filter_register_declared_values_result(evaluated_args, context, values), + "stream_filter_remove" => super::stream_filter_remove::eval_stream_filter_remove_declared_values_result(evaluated_args, context, values), + "stream_get_contents" => super::stream_get_contents::eval_stream_get_contents_declared_values_result(evaluated_args, context, values), + "stream_get_line" => super::stream_get_line::eval_stream_get_line_declared_values_result(evaluated_args, context, values), + "stream_get_meta_data" => super::stream_get_meta_data::eval_stream_get_meta_data_declared_values_result(evaluated_args, context, values), + "stream_isatty" => super::stream_isatty::eval_stream_isatty_declared_values_result(evaluated_args, context, values), + "stream_resolve_include_path" => super::stream_resolve_include_path::eval_stream_resolve_include_path_declared_values_result(evaluated_args, context, values), + "stream_select" => super::stream_select::eval_stream_select_declared_values_result(evaluated_args, context, values), + "stream_set_blocking" => super::stream_set_blocking::eval_stream_set_blocking_declared_values_result(evaluated_args, context, values), + "stream_set_chunk_size" => super::stream_set_chunk_size::eval_stream_set_chunk_size_declared_values_result(evaluated_args, context, values), + "stream_set_read_buffer" => super::stream_set_read_buffer::eval_stream_set_read_buffer_declared_values_result(evaluated_args, context, values), + "stream_set_timeout" => super::stream_set_timeout::eval_stream_set_timeout_declared_values_result(evaluated_args, context, values), + "stream_set_write_buffer" => super::stream_set_write_buffer::eval_stream_set_write_buffer_declared_values_result(evaluated_args, context, values), + "stream_socket_accept" => super::stream_socket_accept::eval_stream_socket_accept_declared_values_result(evaluated_args, context, values), + "stream_socket_client" => super::stream_socket_client::eval_stream_socket_client_declared_values_result(evaluated_args, context, values), + "stream_socket_enable_crypto" => super::stream_socket_enable_crypto::eval_stream_socket_enable_crypto_declared_values_result(evaluated_args, context, values), + "stream_socket_get_name" => super::stream_socket_get_name::eval_stream_socket_get_name_declared_values_result(evaluated_args, context, values), + "stream_socket_pair" => super::stream_socket_pair::eval_stream_socket_pair_declared_values_result(evaluated_args, context, values), + "stream_socket_recvfrom" => super::stream_socket_recvfrom::eval_stream_socket_recvfrom_declared_values_result(evaluated_args, context, values), + "stream_socket_sendto" => super::stream_socket_sendto::eval_stream_socket_sendto_declared_values_result(evaluated_args, context, values), + "stream_socket_server" => super::stream_socket_server::eval_stream_socket_server_declared_values_result(evaluated_args, context, values), + "stream_socket_shutdown" => super::stream_socket_shutdown::eval_stream_socket_shutdown_declared_values_result(evaluated_args, context, values), + "stream_wrapper_register" => super::stream_wrapper_register::eval_stream_wrapper_register_declared_values_result(evaluated_args, context, values), + "stream_wrapper_restore" => super::stream_wrapper_restore::eval_stream_wrapper_restore_declared_values_result(evaluated_args, context, values), + "stream_wrapper_unregister" => super::stream_wrapper_unregister::eval_stream_wrapper_unregister_declared_values_result(evaluated_args, context, values), + "symlink" => super::symlink::eval_symlink_declared_values_result(evaluated_args, context, values), + "sys_get_temp_dir" => super::sys_get_temp_dir::eval_sys_get_temp_dir_declared_values_result(evaluated_args, context, values), + "tempnam" => super::tempnam::eval_tempnam_declared_values_result(evaluated_args, context, values), + "tmpfile" => super::tmpfile::eval_tmpfile_declared_values_result(evaluated_args, context, values), + "touch" => super::touch::eval_touch_declared_values_result(evaluated_args, context, values), + "umask" => super::umask::eval_umask_declared_values_result(evaluated_args, context, values), + "unlink" => super::unlink::eval_unlink_declared_values_result(evaluated_args, context, values), + "vfprintf" => super::vfprintf::eval_vfprintf_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs new file mode 100644 index 0000000000..2caf6ad3ba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entry for `vfprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the vprintf-family stream write helper. + +eval_builtin! { + name: "vfprintf", + area: Filesystem, + params: [stream, format, values], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `vfprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_vfprintf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_vfprintf(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `vfprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_vfprintf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [stream, format, array] => eval_vfprintf_result(*stream, *format, *array, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_vfprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, format, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_vfprintf_result(stream, format, array, context, values) +} + +/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_vfprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format_args = eval_sprintf_argument_array_values(array, values)?; + super::fprintf::eval_fprintf_result(stream, format, &format_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/common.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/common.rs new file mode 100644 index 0000000000..b3f346c2ab --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/common.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Shared scalar formatting coercions used by formatting-related eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` number-format and printf helpers. +//! +//! Key details: +//! - Float conversion delegates to runtime coercion first, then parses the runtime +//! string payload into the scalar value used by formatting logic. + +use super::super::super::*; + +/// Converts one eval value to PHP float and returns the scalar payload. +pub(in crate::interpreter) fn eval_float_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_float(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs new file mode 100644 index 0000000000..82aab429e9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Groups numeric formatting, printf-family, and scanf eval builtins. +//! Submodules are split by builtin family and shared formatting helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +mod common; +mod number_format; +mod printf; +mod sprintf; +mod sprintf_format; +mod sscanf; +mod vprintf; +mod vsprintf; + +pub(in crate::interpreter) use common::*; +pub(in crate::interpreter) use number_format::*; +pub(in crate::interpreter) use printf::*; +pub(in crate::interpreter) use sprintf::*; +pub(in crate::interpreter) use sprintf_format::*; +pub(in crate::interpreter) use sscanf::*; +pub(in crate::interpreter) use vprintf::*; +pub(in crate::interpreter) use vsprintf::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs new file mode 100644 index 0000000000..f00a01bff7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs @@ -0,0 +1,196 @@ +//! Purpose: +//! Implements PHP `number_format()` evaluation and decimal grouping helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` re-exports. +//! +//! Key details: +//! - Float coercion is shared with printf formatting, while separator coercion +//! remains local to `number_format()`. + +use super::super::super::*; +use super::super::*; +use super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "number_format", + area: Formatting, + params: [ + num, + decimals = EvalBuiltinDefaultValue::Int(0), + decimal_separator = EvalBuiltinDefaultValue::String("."), + thousands_separator = EvalBuiltinDefaultValue::String(","), + ], + direct: NumberFormat, + values: NumberFormat, +} + +/// Evaluates PHP `number_format(...)` over one number and optional separators. +pub(in crate::interpreter) fn eval_builtin_number_format( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_number_format_result(value, None, None, None, values) + } + [value, decimals] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + eval_number_format_result(value, Some(decimals), None, None, values) + } + [value, decimals, decimal_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) + } + [value, decimals, decimal_separator, thousands_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; + eval_number_format_result( + value, + Some(decimals), + Some(decimal_separator), + Some(thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one PHP numeric value with grouped thousands and fixed decimals. +pub(in crate::interpreter) fn eval_number_format_result( + value: RuntimeCellHandle, + decimals: Option, + decimal_separator: Option, + thousands_separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let decimals = match decimals { + Some(decimals) => eval_int_value(decimals, values)?, + None => 0, + }; + let decimal_separator = match decimal_separator { + Some(separator) => values.string_bytes(separator)?, + None => b".".to_vec(), + }; + let thousands_separator = match thousands_separator { + Some(separator) => values.string_bytes(separator)?, + None => b",".to_vec(), + }; + let output = + eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; + values.string_bytes_value(&output) +} + +/// Dispatches evaluated `number_format()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_number_format_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values), + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values) + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + ), + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Produces PHP `number_format()` bytes for finite scalar values. +pub(in crate::interpreter) fn eval_number_format_bytes( + value: f64, + decimals: i64, + decimal_separator: &[u8], + thousands_separator: &[u8], +) -> Result, EvalStatus> { + if !value.is_finite() { + return Ok(value.to_string().into_bytes()); + } + let decimals = decimals.clamp(-308, 308); + let display_decimals = decimals.max(0) as usize; + let abs_value = value.abs(); + let scaled = if decimals >= 0 { + let scale = 10_f64.powi(decimals as i32); + (abs_value * scale).round() + } else { + let scale = 10_f64.powi((-decimals) as i32); + (abs_value / scale).round() * scale + }; + if scaled > (u128::MAX as f64) { + return Err(EvalStatus::RuntimeFatal); + } + let scaled = scaled as u128; + let scale = 10_u128 + .checked_pow(display_decimals as u32) + .ok_or(EvalStatus::RuntimeFatal)?; + let integer = if display_decimals == 0 { + scaled + } else { + scaled / scale + }; + let fraction = if display_decimals == 0 { + 0 + } else { + scaled % scale + }; + + let mut output = Vec::new(); + if value.is_sign_negative() && scaled != 0 { + output.push(b'-'); + } + eval_append_grouped_decimal(&mut output, integer, thousands_separator); + if display_decimals > 0 { + output.extend_from_slice(decimal_separator); + let fraction = format!("{fraction:0display_decimals$}"); + output.extend_from_slice(fraction.as_bytes()); + } + Ok(output) +} + +/// Appends one unsigned decimal integer with optional three-digit grouping. +pub(in crate::interpreter) fn eval_append_grouped_decimal( + output: &mut Vec, + value: u128, + separator: &[u8], +) { + let digits = value.to_string(); + if separator.is_empty() { + output.extend_from_slice(digits.as_bytes()); + return; + } + let first_group = match digits.len() % 3 { + 0 => 3, + len => len, + }; + output.extend_from_slice(&digits.as_bytes()[..first_group]); + let mut index = first_group; + while index < digits.len() { + output.extend_from_slice(separator); + output.extend_from_slice(&digits.as_bytes()[index..index + 3]); + index += 3; + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs new file mode 100644 index 0000000000..8b4bd0bbe1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry and implementation for `printf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `printf()` reuses `sprintf` byte formatting, echoes the result, and returns +//! the emitted byte count. + +use super::super::super::*; + +eval_builtin! { + name: "printf", + area: Formatting, + params: [format], + variadic: values, + direct: Printf, + values: Printf, +} + +/// Evaluates direct positional `printf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_printf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_printf_result(&evaluated_args, values) +} + +/// Formats `printf()` arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_printf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = super::sprintf::eval_sprintf_bytes(&format, format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs new file mode 100644 index 0000000000..5b441cbd85 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Eval registry entry and implementation for `sprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns the string-returning printf-family implementation. +//! - Echoing and vector-argument variants reuse the byte formatter here because +//! they are behavior variants of `sprintf`. + +use super::super::super::*; +use super::*; + +eval_builtin! { + name: "sprintf", + area: Formatting, + params: [format], + variadic: values, + direct: Sprintf, + values: Sprintf, +} + +/// Evaluates direct positional `sprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_result(&evaluated_args, values) +} + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +pub(in crate::interpreter) fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf_format.rs new file mode 100644 index 0000000000..e86546cc47 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf_format.rs @@ -0,0 +1,260 @@ +//! Purpose: +//! Parses and applies printf-style format specifiers for eval printf-family calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::printf`. +//! +//! Key details: +//! - Formatting uses PHP runtime coercions through `RuntimeValueOps` before width, +//! precision, sign, and alternate-form handling are applied. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Parses flags, width, precision, and terminal type for one format specifier. +pub(in crate::interpreter) fn eval_parse_sprintf_spec( + format: &[u8], + mut index: usize, +) -> Result<(EvalSprintfSpec, usize), EvalStatus> { + let mut spec = EvalSprintfSpec { + left_align: false, + force_sign: false, + space_sign: false, + zero_pad: false, + alternate: false, + width: None, + precision: None, + specifier: 0, + }; + while index < format.len() { + match format[index] { + b'-' => spec.left_align = true, + b'+' => spec.force_sign = true, + b' ' => spec.space_sign = true, + b'0' => spec.zero_pad = true, + b'#' => spec.alternate = true, + _ => break, + } + index += 1; + } + let (width, next_index) = eval_parse_sprintf_number(format, index)?; + spec.width = width; + index = next_index; + if index < format.len() && format[index] == b'.' { + let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; + spec.precision = Some(precision.unwrap_or(0)); + index = next_index; + } + if index >= format.len() { + return Err(EvalStatus::RuntimeFatal); + } + spec.specifier = format[index]; + Ok((spec, index + 1)) +} + +/// Parses an unsigned decimal number from a format specifier component. +pub(in crate::interpreter) fn eval_parse_sprintf_number( + format: &[u8], + mut index: usize, +) -> Result<(Option, usize), EvalStatus> { + let start = index; + let mut value = 0usize; + while index < format.len() && format[index].is_ascii_digit() { + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) + .ok_or(EvalStatus::RuntimeFatal)?; + index += 1; + } + if index == start { + Ok((None, index)) + } else { + Ok((Some(value), index)) + } +} + +/// Formats one runtime value according to a parsed eval sprintf specifier. +pub(in crate::interpreter) fn eval_format_sprintf_arg( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match spec.specifier { + b's' => eval_format_sprintf_string(spec, value, values), + b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), + b'c' => eval_format_sprintf_char(spec, value, values), + _ => eval_format_sprintf_int(spec, value, values), + } +} + +/// Formats a `%s` operand after PHP string coercion. +pub(in crate::interpreter) fn eval_format_sprintf_string( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bytes = values.string_bytes(value)?; + if let Some(precision) = spec.precision { + bytes.truncate(precision); + } + Ok(eval_sprintf_apply_width(bytes, spec, false)) +} + +/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. +pub(in crate::interpreter) fn eval_format_sprintf_int( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + let mut output = Vec::new(); + match spec.specifier { + b'u' => { + let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + b'x' | b'X' => { + let unsigned = value as u64; + if spec.alternate && unsigned != 0 { + output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); + } + let digits = if spec.specifier == b'X' { + format!("{unsigned:X}").into_bytes() + } else { + format!("{unsigned:x}").into_bytes() + }; + output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); + } + b'o' => { + let unsigned = value as u64; + let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); + if spec.alternate && !digits.starts_with(b"0") { + output.push(b'0'); + } + output.append(&mut digits); + } + _ => { + let value = value as i128; + let magnitude = if value < 0 { + (-value) as u128 + } else { + value as u128 + }; + if value < 0 { + output.push(b'-'); + } else if spec.force_sign { + output.push(b'+'); + } else if spec.space_sign { + output.push(b' '); + } + let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Formats a `%c` operand as the low byte of its integer value. +pub(in crate::interpreter) fn eval_format_sprintf_char( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) +} + +/// Formats a floating-point operand for the eval printf-family subset. +pub(in crate::interpreter) fn eval_format_sprintf_float( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_float_value(value, values)?; + let precision = spec.precision.unwrap_or(6); + let mut output = if value.is_nan() { + b"NAN".to_vec() + } else if value.is_infinite() { + b"INF".to_vec() + } else { + match spec.specifier { + b'e' => format!("{value:.precision$e}").into_bytes(), + b'g' => format!("{value:.precision$}").into_bytes(), + _ => format!("{value:.precision$}").into_bytes(), + } + }; + if value.is_sign_negative() && !output.starts_with(b"-") { + output.insert(0, b'-'); + } else if value.is_sign_positive() && value.is_finite() { + if spec.force_sign { + output.insert(0, b'+'); + } else if spec.space_sign { + output.insert(0, b' '); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Applies integer precision by left-padding digits with zeros. +pub(in crate::interpreter) fn eval_sprintf_precision_pad( + mut digits: Vec, + spec: EvalSprintfSpec, +) -> Vec { + if matches!(spec.precision, Some(0)) && digits == b"0" { + digits.clear(); + return digits; + } + let Some(precision) = spec.precision else { + return digits; + }; + if digits.len() >= precision { + return digits; + } + let mut output = vec![b'0'; precision - digits.len()]; + output.append(&mut digits); + output +} + +/// Applies field width and alignment to one formatted eval sprintf replacement. +pub(in crate::interpreter) fn eval_sprintf_apply_width( + mut bytes: Vec, + spec: EvalSprintfSpec, + numeric: bool, +) -> Vec { + let Some(width) = spec.width else { + return bytes; + }; + if bytes.len() >= width { + return bytes; + } + let pad_len = width - bytes.len(); + if spec.left_align { + bytes.extend(std::iter::repeat_n(b' ', pad_len)); + return bytes; + } + if numeric && spec.zero_pad && spec.precision.is_none() { + let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); + let mut output = Vec::with_capacity(width); + output.extend_from_slice(&bytes[..prefix_len]); + output.extend(std::iter::repeat_n(b'0', pad_len)); + output.extend_from_slice(&bytes[prefix_len..]); + return output; + } + let mut output = Vec::with_capacity(width); + output.extend(std::iter::repeat_n(b' ', pad_len)); + output.append(&mut bytes); + output +} + +/// Returns the sign and alternate-prefix length that should precede zero padding. +pub(in crate::interpreter) fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { + let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); + if bytes.len() >= prefix_len + 2 + && bytes[prefix_len] == b'0' + && matches!(bytes[prefix_len + 1], b'x' | b'X') + { + prefix_len += 2; + } + prefix_len +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs new file mode 100644 index 0000000000..c168fc0482 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs @@ -0,0 +1,191 @@ +//! Purpose: +//! Implements eval support for PHP `sscanf()` and its small scanning subset. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Only the currently supported `%d`, `%f`, `%s`, and `%%` subset is parsed; +//! extra output variables are evaluated for side effects and ignored. +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! the scanning implementation. + +use super::super::super::*; + +eval_builtin! { + name: "sscanf", + area: Formatting, + params: [string, format], + variadic: vars, + direct: Sscanf, + values: Sscanf, +} + + +/// Evaluates direct positional `sscanf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let input = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_sscanf_result(input, format, values) +} + + +/// Dispatches by-value `sscanf()` calls after argument binding. +pub(in crate::interpreter) fn eval_sscanf_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values) +} + +/// Parses one string through the eval `sscanf()` subset and returns an indexed array. +pub(in crate::interpreter) fn eval_sscanf_result( + input: RuntimeCellHandle, + format: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(input)?; + let format = values.string_bytes(format)?; + let matches = eval_sscanf_matches(&input, &format); + let mut result = values.array_new(matches.len())?; + for (index, matched) in matches.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(matched)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. +pub(in crate::interpreter) fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { + let mut matches = Vec::new(); + let mut input_index = 0; + let mut format_index = 0; + + while format_index < format.len() { + if format[format_index] != b'%' { + if input_index >= input.len() || input[input_index] != format[format_index] { + break; + } + input_index += 1; + format_index += 1; + continue; + } + + format_index += 1; + if format_index >= format.len() { + break; + } + + match format[format_index] { + b'%' => { + if input_index >= input.len() || input[input_index] != b'%' { + break; + } + input_index += 1; + } + b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), + b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), + b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), + _ => {} + } + format_index += 1; + } + + matches +} + +/// Scans the native `sscanf()` `%d` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_int( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input.get(*input_index) == Some(&b'-') { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%f` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_float( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + if input.get(*input_index) == Some(&b'.') { + *input_index += 1; + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + *input_index += 1; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%s` subset as a non-space byte word. +pub(in crate::interpreter) fn eval_sscanf_scan_word( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + while input + .get(*input_index) + .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs new file mode 100644 index 0000000000..600156d53a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry and implementation for `vprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `vprintf()` reuses `sprintf` byte formatting and `vsprintf` argument-array +//! expansion, then echoes the result and returns its byte count. + +use super::super::super::*; + +eval_builtin! { + name: "vprintf", + area: Formatting, + params: [format, values], + direct: Vprintf, + values: Vprintf, +} + +/// Evaluates direct positional `vprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vprintf_result(&evaluated_args, values) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = super::vsprintf::eval_sprintf_argument_array_values(*array, values)?; + let output = super::sprintf::eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs new file mode 100644 index 0000000000..44a89bc606 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Eval registry entry and implementation for `vsprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `vsprintf()` owns array-to-argument expansion for printf-family vector +//! calls. `vprintf()` reuses that expansion from this file. + +use super::super::super::*; + +eval_builtin! { + name: "vsprintf", + area: Formatting, + params: [format, values], + direct: Vsprintf, + values: Vsprintf, +} + +/// Evaluates direct positional `vsprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vsprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_result(&evaluated_args, values) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = super::sprintf::eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +pub(in crate::interpreter) fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs new file mode 100644 index 0000000000..df391d4d9a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Shared fixed-arity helpers for declarative builtin values hooks. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - Helpers validate already-bound argument slices before delegating to the +//! concrete runtime-value operation. + +use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; + +/// Validates and dispatches one evaluated builtin argument. +pub(super) fn one_arg( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, &mut V) -> Result, +{ + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*value, values) +} + +/// Validates and dispatches two evaluated builtin arguments. +pub(super) fn two_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, RuntimeCellHandle, &mut V) -> Result, +{ + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*left, *right, values) +} + +/// Validates and dispatches three evaluated builtin arguments. +pub(super) fn three_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce( + RuntimeCellHandle, + RuntimeCellHandle, + RuntimeCellHandle, + &mut V, + ) -> Result, +{ + let [first, second, third] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*first, *second, *third, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs new file mode 100644 index 0000000000..d718fea12e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -0,0 +1,572 @@ +//! Purpose: +//! Direct expression-level dispatch hooks for eval builtins migrated into the +//! declarative registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::eval_declared_builtin_direct_call`. +//! +//! Key details: +//! - Direct hooks preserve source-order evaluation in existing builtin helpers. +//! - Hook variants remain static metadata referenced from per-builtin files. + +use super::super::*; +use super::super::super::{ + ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + RuntimeCellHandle, RuntimeValueOps, +}; + +/// Direct expression-level dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalDirectHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `array_sum(...)` and `array_product(...)`. + ArrayAggregate, + /// Dispatches non-mutating array and iterator builtins. + Array, + /// Dispatches `array_flip(...)`. + ArrayFlip, + /// Dispatches `array_key_exists(...)`. + ArrayKeyExists, + /// Dispatches `array_pad(...)`. + ArrayPad, + /// Dispatches `array_keys(...)`. + ArrayKeys, + /// Dispatches `array_rand(...)`. + ArrayRand, + /// Dispatches `array_reverse(...)`. + ArrayReverse, + /// Dispatches `array_search(...)` and `in_array(...)`. + ArraySearch, + /// Dispatches `array_slice(...)`. + ArraySlice, + /// Dispatches `array_unique(...)`. + ArrayUnique, + /// Dispatches `array_values(...)`. + ArrayValues, + /// Dispatches `base64_decode(...)`. + Base64Decode, + /// Dispatches `base64_encode(...)`. + Base64Encode, + /// Dispatches `bin2hex(...)`. + Bin2Hex, + /// Dispatches `boolval(...)`. + Boolval, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `chr(...)`. + Chr, + /// Dispatches `clamp(...)`. + Clamp, + /// Dispatches `count(...)`. + Count, + /// Dispatches core callable, constant, process-control, and debug-output builtins. + Core, + /// Dispatches `crc32(...)`. + Crc32, + /// Dispatches `ctype_*` predicates. + Ctype, + /// Dispatches filesystem and path builtins. + Filesystem, + /// Dispatches `acos(...)`. + Acos, + /// Dispatches `asin(...)`. + Asin, + /// Dispatches `atan(...)`. + Atan, + /// Dispatches `atan2(...)`. + Atan2, + /// Dispatches `cos(...)`. + Cos, + /// Dispatches `cosh(...)`. + Cosh, + /// Dispatches `deg2rad(...)`. + Deg2rad, + /// Dispatches `exp(...)`. + Exp, + /// Dispatches `fdiv(...)`. + Fdiv, + /// Dispatches `fmod(...)`. + Fmod, + /// Dispatches `hypot(...)`. + Hypot, + /// Dispatches `printf(...)`. + Printf, + /// Dispatches `sprintf(...)`. + Sprintf, + /// Dispatches `sscanf(...)`. + Sscanf, + /// Dispatches `vprintf(...)`. + Vprintf, + /// Dispatches `vsprintf(...)`. + Vsprintf, + /// Dispatches `floor(...)`. + Floor, + /// Dispatches `gettype(...)`. + Gettype, + /// Dispatches `floatval(...)`. + Floatval, + /// Dispatches `intval(...)`. + Intval, + /// Dispatches `is_array(...)`. + IsArray, + /// Dispatches `is_bool(...)`. + IsBool, + /// Dispatches `is_double(...)`. + IsDouble, + /// Dispatches `is_finite(...)`. + IsFinite, + /// Dispatches `is_float(...)`. + IsFloat, + /// Dispatches `is_infinite(...)`. + IsInfinite, + /// Dispatches `is_int(...)`. + IsInt, + /// Dispatches `is_integer(...)`. + IsInteger, + /// Dispatches `is_iterable(...)`. + IsIterable, + /// Dispatches `is_long(...)`. + IsLong, + /// Dispatches `is_nan(...)`. + IsNan, + /// Dispatches `is_null(...)`. + IsNull, + /// Dispatches `is_numeric(...)`. + IsNumeric, + /// Dispatches `is_object(...)`. + IsObject, + /// Dispatches `is_real(...)`. + IsReal, + /// Dispatches `is_resource(...)`. + IsResource, + /// Dispatches `is_scalar(...)`. + IsScalar, + /// Dispatches `is_string(...)`. + IsString, + /// Dispatches `grapheme_strrev(...)`. + GraphemeStrrev, + /// Dispatches gzip/zlib string builtins. + Gzip, + /// Dispatches `hash_algos()`. + HashAlgos, + /// Dispatches incremental hash-context builtins. + HashContext, + /// Dispatches `hash_equals(...)`. + HashEquals, + /// Dispatches one-shot hash digest builtins. + HashOneShot, + /// Dispatches `hex2bin(...)`. + Hex2Bin, + /// Dispatches HTML entity encode/decode builtins. + HtmlEntity, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `json_decode(...)`. + JsonDecode, + /// Dispatches `json_encode(...)`. + JsonEncode, + /// Dispatches `json_last_error()`. + JsonLastError, + /// Dispatches `json_last_error_msg()`. + JsonLastErrorMsg, + /// Dispatches `json_validate(...)`. + JsonValidate, + /// Dispatches `log(...)`. + Log, + /// Dispatches `log2(...)`. + Log2, + /// Dispatches `log10(...)`. + Log10, + /// Dispatches `max(...)`. + Max, + /// Dispatches `min(...)`. + Min, + /// Dispatches network, host, environment, and process builtins. + NetworkEnv, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `ord(...)`. + Ord, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `mt_rand(...)`. + MtRand, + /// Dispatches `rad2deg(...)`. + Rad2deg, + /// Dispatches `rand(...)`. + Rand, + /// Dispatches `random_int(...)`. + RandomInt, + /// Dispatches `round(...)`. + Round, + /// Dispatches `range(...)`. + Range, + /// Dispatches `mb_ereg_match(...)`. + MbEregMatch, + /// Dispatches `preg_match(...)`. + PregMatch, + /// Dispatches `preg_match_all(...)`. + PregMatchAll, + /// Dispatches `preg_replace(...)`. + PregReplace, + /// Dispatches `preg_replace_callback(...)`. + PregReplaceCallback, + /// Dispatches `preg_split(...)`. + PregSplit, + /// Dispatches `buffer_free(...)`. + BufferFree, + /// Dispatches `buffer_len(...)`. + BufferLen, + /// Dispatches `buffer_new(...)`. + BufferNew, + /// Dispatches `ptr(...)`. + Ptr, + /// Dispatches `ptr_get(...)`. + PtrGet, + /// Dispatches `ptr_is_null(...)`. + PtrIsNull, + /// Dispatches `ptr_null()`. + PtrNull, + /// Dispatches `ptr_offset(...)`. + PtrOffset, + /// Dispatches `ptr_read8(...)`. + PtrRead8, + /// Dispatches `ptr_read16(...)`. + PtrRead16, + /// Dispatches `ptr_read32(...)`. + PtrRead32, + /// Dispatches `ptr_read_string(...)`. + PtrReadString, + /// Dispatches `ptr_set(...)`. + PtrSet, + /// Dispatches `ptr_sizeof(...)`. + PtrSizeof, + /// Dispatches `ptr_write8(...)`. + PtrWrite8, + /// Dispatches `ptr_write16(...)`. + PtrWrite16, + /// Dispatches `ptr_write32(...)`. + PtrWrite32, + /// Dispatches `ptr_write_string(...)`. + PtrWriteString, + /// Dispatches `addslashes(...)` and `stripslashes(...)`. + Slashes, + /// Dispatches `sin(...)`. + Sin, + /// Dispatches `sinh(...)`. + Sinh, + /// Dispatches `sqrt(...)`. + Sqrt, + /// Dispatches string ASCII case-conversion builtins. + StringCase, + /// Dispatches string comparison builtins. + StringCompare, + /// Dispatches string position builtins. + StringPosition, + /// Dispatches string search predicate builtins. + StringSearch, + /// Dispatches `explode(...)` and `implode(...)`. + StringSplitJoin, + /// Dispatches stream boolean predicate builtins. + StreamBoolPredicate, + /// Dispatches stream introspection list builtins. + StreamIntrospection, + /// Dispatches `str_pad(...)`. + StrPad, + /// Dispatches `str_replace(...)` and `str_ireplace(...)`. + StrReplace, + /// Dispatches `str_split(...)`. + StrSplit, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `str_repeat(...)`. + StrRepeat, + /// Dispatches `strval(...)`. + Strval, + /// Dispatches `strrev(...)`. + Strrev, + /// Dispatches `strstr(...)`. + Strstr, + /// Dispatches `substr(...)`. + Substr, + /// Dispatches `substr_replace(...)`. + SubstrReplace, + /// Dispatches symbol, class metadata, SPL, and language-construct probes. + Symbols, + /// Dispatches `tan(...)`. + Tan, + /// Dispatches `tanh(...)`. + Tanh, + /// Dispatches date, time, and sleep builtins. + Time, + /// Dispatches trim-family builtins. + TrimLike, + /// Dispatches `ucwords(...)`. + Ucwords, + /// Dispatches `nl2br(...)`. + Nl2br, + /// Dispatches `wordwrap(...)`. + Wordwrap, + /// Dispatches URL decode builtins. + UrlDecode, + /// Dispatches URL encode builtins. + UrlEncode, +} + +impl EvalDirectHook { + /// Runs a direct expression-level builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::Acos => eval_builtin_acos(args, context, scope, values), + Self::ArrayAggregate + | Self::Array + | Self::ArrayFlip + | Self::ArrayKeyExists + | Self::ArrayPad + | Self::ArrayKeys + | Self::ArrayRand + | Self::ArrayReverse + | Self::ArraySearch + | Self::ArraySlice + | Self::ArrayUnique + | Self::ArrayValues + | Self::Count + | Self::Range => eval_builtin_array_declared_call(name, args, context, scope, values), + Self::Asin => eval_builtin_asin(args, context, scope, values), + Self::Atan => eval_builtin_atan(args, context, scope, values), + Self::Atan2 => eval_builtin_atan2(args, context, scope, values), + Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), + Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), + Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), + Self::Boolval => eval_builtin_boolval(args, context, scope, values), + Self::Ceil => eval_builtin_ceil(args, context, scope, values), + Self::Chr => eval_builtin_chr(args, context, scope, values), + Self::Clamp => eval_builtin_clamp(args, context, scope, values), + Self::Core => eval_builtin_core_call(name, args, context, scope, values), + Self::Cos => eval_builtin_cos(args, context, scope, values), + Self::Cosh => eval_builtin_cosh(args, context, scope, values), + Self::Crc32 => eval_builtin_crc32(args, context, scope, values), + Self::Ctype => match name { + "ctype_alnum" => eval_builtin_ctype_alnum(args, context, scope, values), + "ctype_alpha" => eval_builtin_ctype_alpha(args, context, scope, values), + "ctype_digit" => eval_builtin_ctype_digit(args, context, scope, values), + "ctype_space" => eval_builtin_ctype_space(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Deg2rad => eval_builtin_deg2rad(args, context, scope, values), + Self::Exp => eval_builtin_exp(args, context, scope, values), + Self::Fdiv => eval_builtin_fdiv(args, context, scope, values), + Self::Filesystem => eval_builtin_filesystem_call(name, args, context, scope, values), + Self::Fmod => eval_builtin_fmod(args, context, scope, values), + Self::Floor => eval_builtin_floor(args, context, scope, values), + Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Hypot => eval_builtin_hypot(args, context, scope, values), + Self::Floatval => eval_builtin_floatval(args, context, scope, values), + Self::Intval => eval_builtin_intval(args, context, scope, values), + Self::IsArray => eval_builtin_is_array(args, context, scope, values), + Self::IsBool => eval_builtin_is_bool(args, context, scope, values), + Self::IsDouble => eval_builtin_is_double(args, context, scope, values), + Self::IsFinite => eval_builtin_is_finite(args, context, scope, values), + Self::IsFloat => eval_builtin_is_float(args, context, scope, values), + Self::IsInfinite => eval_builtin_is_infinite(args, context, scope, values), + Self::IsInt => eval_builtin_is_int(args, context, scope, values), + Self::IsInteger => eval_builtin_is_integer(args, context, scope, values), + Self::IsIterable => eval_builtin_is_iterable(args, context, scope, values), + Self::IsLong => eval_builtin_is_long(args, context, scope, values), + Self::IsNan => eval_builtin_is_nan(args, context, scope, values), + Self::IsNull => eval_builtin_is_null(args, context, scope, values), + Self::IsNumeric => eval_builtin_is_numeric(args, context, scope, values), + Self::IsObject => eval_builtin_is_object(args, context, scope, values), + Self::IsReal => eval_builtin_is_real(args, context, scope, values), + Self::IsResource => eval_builtin_is_resource(args, context, scope, values), + Self::IsScalar => eval_builtin_is_scalar(args, context, scope, values), + Self::IsString => eval_builtin_is_string(args, context, scope, values), + Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), + Self::Gzip => match name { + "gzcompress" => eval_builtin_gzcompress(args, context, scope, values), + "gzdeflate" => eval_builtin_gzdeflate(args, context, scope, values), + "gzinflate" => eval_builtin_gzinflate(args, context, scope, values), + "gzuncompress" => eval_builtin_gzuncompress(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::HashAlgos => eval_builtin_hash_algos(args, values), + Self::HashContext => match name { + "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), + "hash_final" => eval_builtin_hash_final(args, context, scope, values), + "hash_init" => eval_builtin_hash_init(args, context, scope, values), + "hash_update" => eval_builtin_hash_update(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::HashEquals => eval_builtin_hash_equals(args, context, scope, values), + Self::HashOneShot => match name { + "hash" => eval_builtin_hash(args, context, scope, values), + "hash_file" => eval_builtin_hash_file(args, context, scope, values), + "hash_hmac" => eval_builtin_hash_hmac(args, context, scope, values), + "md5" => eval_builtin_md5(args, context, scope, values), + "sha1" => eval_builtin_sha1(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), + Self::HtmlEntity => match name { + "html_entity_decode" => { + eval_builtin_html_entity_decode(args, context, scope, values) + } + "htmlentities" => eval_builtin_htmlentities(args, context, scope, values), + "htmlspecialchars" => eval_builtin_htmlspecialchars(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), + Self::JsonDecode => eval_builtin_json_decode(args, context, scope, values), + Self::JsonEncode => eval_builtin_json_encode(args, context, scope, values), + Self::JsonLastError => eval_builtin_json_last_error(args, context, values), + Self::JsonLastErrorMsg => eval_builtin_json_last_error_msg(args, context, values), + Self::JsonValidate => eval_builtin_json_validate(args, context, scope, values), + Self::Log => eval_builtin_log(args, context, scope, values), + Self::Log2 => eval_builtin_log2(args, context, scope, values), + Self::Log10 => eval_builtin_log10(args, context, scope, values), + Self::Max => eval_builtin_max(args, context, scope, values), + Self::Min => eval_builtin_min(args, context, scope, values), + Self::MtRand => eval_builtin_mt_rand(args, context, scope, values), + Self::NetworkEnv => eval_builtin_network_env_call(name, args, context, scope, values), + Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), + Self::Ord => eval_builtin_ord(args, context, scope, values), + Self::Pi => eval_builtin_pi(args, values), + Self::Printf => eval_builtin_printf(args, context, scope, values), + Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::Rad2deg => eval_builtin_rad2deg(args, context, scope, values), + Self::Rand => eval_builtin_rand(args, context, scope, values), + Self::RandomInt => eval_builtin_random_int(args, context, scope, values), + Self::Round => eval_builtin_round(args, context, scope, values), + Self::MbEregMatch => eval_builtin_mb_ereg_match(args, context, scope, values), + Self::PregMatch => eval_builtin_preg_match(args, context, scope, values), + Self::PregMatchAll => eval_builtin_preg_match_all(args, context, scope, values), + Self::PregReplace => eval_builtin_preg_replace(args, context, scope, values), + Self::PregReplaceCallback => { + eval_builtin_preg_replace_callback(args, context, scope, values) + } + Self::PregSplit => eval_builtin_preg_split(args, context, scope, values), + Self::BufferFree => eval_builtin_buffer_free(args, context, scope, values), + Self::BufferLen => eval_builtin_buffer_len(args, context, scope, values), + Self::BufferNew => eval_builtin_buffer_new(args, context, scope, values), + Self::Ptr => eval_builtin_ptr(args, context, scope, values), + Self::PtrGet => eval_builtin_ptr_get(args, context, scope, values), + Self::PtrIsNull => eval_builtin_ptr_is_null(args, context, scope, values), + Self::PtrNull => eval_builtin_ptr_null(args, context, scope, values), + Self::PtrOffset => eval_builtin_ptr_offset(args, context, scope, values), + Self::PtrRead8 => eval_builtin_ptr_read8(args, context, scope, values), + Self::PtrRead16 => eval_builtin_ptr_read16(args, context, scope, values), + Self::PtrRead32 => eval_builtin_ptr_read32(args, context, scope, values), + Self::PtrReadString => eval_builtin_ptr_read_string(args, context, scope, values), + Self::PtrSet => eval_builtin_ptr_set(args, context, scope, values), + Self::PtrSizeof => eval_builtin_ptr_sizeof(args, context, scope, values), + Self::PtrWrite8 => eval_builtin_ptr_write8(args, context, scope, values), + Self::PtrWrite16 => eval_builtin_ptr_write16(args, context, scope, values), + Self::PtrWrite32 => eval_builtin_ptr_write32(args, context, scope, values), + Self::PtrWriteString => eval_builtin_ptr_write_string(args, context, scope, values), + Self::Sin => eval_builtin_sin(args, context, scope, values), + Self::Sinh => eval_builtin_sinh(args, context, scope, values), + Self::Slashes => match name { + "addslashes" => eval_builtin_addslashes(args, context, scope, values), + "stripslashes" => eval_builtin_stripslashes(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Sqrt => super::super::math::eval_builtin_sqrt(args, context, scope, values), + Self::Sprintf => eval_builtin_sprintf(args, context, scope, values), + Self::Sscanf => eval_builtin_sscanf(args, context, scope, values), + Self::StringCase => match name { + "lcfirst" => eval_builtin_lcfirst(args, context, scope, values), + "strtolower" => eval_builtin_strtolower(args, context, scope, values), + "strtoupper" => eval_builtin_strtoupper(args, context, scope, values), + "ucfirst" => eval_builtin_ucfirst(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringCompare => match name { + "strcasecmp" => eval_builtin_strcasecmp(args, context, scope, values), + "strcmp" => eval_builtin_strcmp(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringPosition => match name { + "strpos" => eval_builtin_strpos(args, context, scope, values), + "strrpos" => eval_builtin_strrpos(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringSearch => match name { + "str_contains" => eval_builtin_str_contains(args, context, scope, values), + "str_ends_with" => eval_builtin_str_ends_with(args, context, scope, values), + "str_starts_with" => eval_builtin_str_starts_with(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringSplitJoin => match name { + "explode" => eval_builtin_explode(args, context, scope, values), + "implode" => eval_builtin_implode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StreamBoolPredicate => match name { + "stream_is_local" => eval_builtin_stream_is_local(args, context, scope, values), + "stream_supports_lock" => { + eval_builtin_stream_supports_lock(args, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StreamIntrospection => match name { + "stream_get_filters" => eval_builtin_stream_get_filters(args, context, values), + "stream_get_transports" => { + eval_builtin_stream_get_transports(args, context, values) + } + "stream_get_wrappers" => eval_builtin_stream_get_wrappers(args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StrPad => eval_builtin_str_pad(args, context, scope, values), + Self::StrReplace => match name { + "str_ireplace" => eval_builtin_str_ireplace(args, context, scope, values), + "str_replace" => eval_builtin_str_replace(name, args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StrSplit => eval_builtin_str_split(args, context, scope, values), + Self::Strlen => eval_builtin_strlen(args, context, scope, values), + Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), + Self::Strval => eval_builtin_strval(args, context, scope, values), + Self::Strrev => eval_builtin_strrev(args, context, scope, values), + Self::Strstr => eval_builtin_strstr(args, context, scope, values), + Self::Substr => eval_builtin_substr(args, context, scope, values), + Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), + Self::Symbols => eval_builtin_symbols_call(name, args, context, scope, values), + Self::Tan => eval_builtin_tan(args, context, scope, values), + Self::Tanh => eval_builtin_tanh(args, context, scope, values), + Self::Time => eval_builtin_time_call(name, args, context, scope, values), + Self::TrimLike => match name { + "chop" => eval_builtin_chop(args, context, scope, values), + "ltrim" => eval_builtin_ltrim(args, context, scope, values), + "rtrim" => eval_builtin_rtrim(args, context, scope, values), + "trim" => eval_builtin_trim(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), + Self::Vprintf => eval_builtin_vprintf(args, context, scope, values), + Self::Vsprintf => eval_builtin_vsprintf(args, context, scope, values), + Self::Nl2br => eval_builtin_nl2br(args, context, scope, values), + Self::Wordwrap => eval_builtin_wordwrap(args, context, scope, values), + Self::UrlDecode => match name { + "rawurldecode" => eval_builtin_rawurldecode(args, context, scope, values), + "urldecode" => eval_builtin_urldecode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::UrlEncode => match name { + "rawurlencode" => eval_builtin_rawurlencode(args, context, scope, values), + "urlencode" => eval_builtin_urlencode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs new file mode 100644 index 0000000000..5dc1022135 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Groups declarative registry dispatch hooks for eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::spec` re-exports used by `eval_builtin!`. +//! +//! Key details: +//! - Direct expression dispatch and already-evaluated argument dispatch stay +//! split so each dispatcher remains focused. + +mod arity; +mod direct; +mod values; + +pub(in crate::interpreter) use direct::EvalDirectHook; +pub(in crate::interpreter) use values::EvalValuesHook; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs new file mode 100644 index 0000000000..ad08e58ffe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -0,0 +1,712 @@ +//! Purpose: +//! Already-evaluated argument dispatch hooks for eval builtins migrated into the +//! declarative registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::eval_declared_builtin_values_call`. +//! +//! Key details: +//! - Values hooks run after named/default argument binding has produced PHP +//! parameter order. +//! - Runtime-cell coercions stay in the existing builtin result helpers. + +use super::super::*; +use super::super::super::{ + ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +}; +use super::arity::{one_arg, three_args, two_args}; + +/// Evaluated-argument dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalValuesHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `array_sum(...)` and `array_product(...)`. + ArrayAggregate, + /// Dispatches non-mutating array and iterator builtins. + Array, + /// Dispatches by-value calls for mutating array builtins. + ArrayMutating, + /// Dispatches `array_flip(...)`. + ArrayFlip, + /// Dispatches `array_key_exists(...)`. + ArrayKeyExists, + /// Dispatches `array_pad(...)`. + ArrayPad, + /// Dispatches `array_keys(...)`. + ArrayKeys, + /// Dispatches `array_rand(...)`. + ArrayRand, + /// Dispatches `array_reverse(...)`. + ArrayReverse, + /// Dispatches `array_search(...)` and `in_array(...)`. + ArraySearch, + /// Dispatches `array_slice(...)`. + ArraySlice, + /// Dispatches `array_unique(...)`. + ArrayUnique, + /// Dispatches `array_values(...)`. + ArrayValues, + /// Dispatches `base64_decode(...)`. + Base64Decode, + /// Dispatches `base64_encode(...)`. + Base64Encode, + /// Dispatches `bin2hex(...)`. + Bin2Hex, + /// Dispatches `boolval(...)`. + Boolval, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `chr(...)`. + Chr, + /// Dispatches `clamp(...)`. + Clamp, + /// Dispatches `count(...)`. + Count, + /// Dispatches core callable, constant, process-control, and debug-output builtins. + Core, + /// Dispatches `crc32(...)`. + Crc32, + /// Dispatches `ctype_*` predicates. + Ctype, + /// Dispatches filesystem and path builtins. + Filesystem, + /// Dispatches `acos(...)`. + Acos, + /// Dispatches `asin(...)`. + Asin, + /// Dispatches `atan(...)`. + Atan, + /// Dispatches `atan2(...)`. + Atan2, + /// Dispatches `cos(...)`. + Cos, + /// Dispatches `cosh(...)`. + Cosh, + /// Dispatches `deg2rad(...)`. + Deg2rad, + /// Dispatches `exp(...)`. + Exp, + /// Dispatches `fdiv(...)`. + Fdiv, + /// Dispatches `fmod(...)`. + Fmod, + /// Dispatches `hypot(...)`. + Hypot, + /// Dispatches `printf(...)`. + Printf, + /// Dispatches `sprintf(...)`. + Sprintf, + /// Dispatches `sscanf(...)`. + Sscanf, + /// Dispatches `vprintf(...)`. + Vprintf, + /// Dispatches `vsprintf(...)`. + Vsprintf, + /// Dispatches `floor(...)`. + Floor, + /// Dispatches `gettype(...)`. + Gettype, + /// Dispatches `floatval(...)`. + Floatval, + /// Dispatches `intval(...)`. + Intval, + /// Dispatches `is_array(...)`. + IsArray, + /// Dispatches `is_bool(...)`. + IsBool, + /// Dispatches `is_double(...)`. + IsDouble, + /// Dispatches `is_finite(...)`. + IsFinite, + /// Dispatches `is_float(...)`. + IsFloat, + /// Dispatches `is_infinite(...)`. + IsInfinite, + /// Dispatches `is_int(...)`. + IsInt, + /// Dispatches `is_integer(...)`. + IsInteger, + /// Dispatches `is_iterable(...)`. + IsIterable, + /// Dispatches `is_long(...)`. + IsLong, + /// Dispatches `is_nan(...)`. + IsNan, + /// Dispatches `is_null(...)`. + IsNull, + /// Dispatches `is_numeric(...)`. + IsNumeric, + /// Dispatches `is_object(...)`. + IsObject, + /// Dispatches `is_real(...)`. + IsReal, + /// Dispatches `is_resource(...)`. + IsResource, + /// Dispatches `is_scalar(...)`. + IsScalar, + /// Dispatches `is_string(...)`. + IsString, + /// Dispatches `grapheme_strrev(...)`. + GraphemeStrrev, + /// Dispatches gzip/zlib string builtins. + Gzip, + /// Dispatches `hash_algos()`. + HashAlgos, + /// Dispatches incremental hash-context builtins. + HashContext, + /// Dispatches `hash_equals(...)`. + HashEquals, + /// Dispatches one-shot hash digest builtins. + HashOneShot, + /// Dispatches `hex2bin(...)`. + Hex2Bin, + /// Dispatches HTML entity encode/decode builtins. + HtmlEntity, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `json_decode(...)`. + JsonDecode, + /// Dispatches `json_encode(...)`. + JsonEncode, + /// Dispatches `json_last_error()`. + JsonLastError, + /// Dispatches `json_last_error_msg()`. + JsonLastErrorMsg, + /// Dispatches `json_validate(...)`. + JsonValidate, + /// Dispatches `log(...)`. + Log, + /// Dispatches `log2(...)`. + Log2, + /// Dispatches `log10(...)`. + Log10, + /// Dispatches `max(...)`. + Max, + /// Dispatches `min(...)`. + Min, + /// Dispatches network, host, environment, and process builtins. + NetworkEnv, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `ord(...)`. + Ord, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `mt_rand(...)`. + MtRand, + /// Dispatches `rad2deg(...)`. + Rad2deg, + /// Dispatches `rand(...)`. + Rand, + /// Dispatches `random_int(...)`. + RandomInt, + /// Dispatches `round(...)`. + Round, + /// Dispatches `range(...)`. + Range, + /// Dispatches `mb_ereg_match(...)`. + MbEregMatch, + /// Dispatches `preg_match(...)`. + PregMatch, + /// Dispatches `preg_match_all(...)`. + PregMatchAll, + /// Dispatches `preg_replace(...)`. + PregReplace, + /// Dispatches `preg_replace_callback(...)`. + PregReplaceCallback, + /// Dispatches `preg_split(...)`. + PregSplit, + /// Dispatches `buffer_free(...)`. + BufferFree, + /// Dispatches `buffer_len(...)`. + BufferLen, + /// Dispatches `buffer_new(...)`. + BufferNew, + /// Dispatches `ptr(...)`. + Ptr, + /// Dispatches `ptr_get(...)`. + PtrGet, + /// Dispatches `ptr_is_null(...)`. + PtrIsNull, + /// Dispatches `ptr_null()`. + PtrNull, + /// Dispatches `ptr_offset(...)`. + PtrOffset, + /// Dispatches `ptr_read8(...)`. + PtrRead8, + /// Dispatches `ptr_read16(...)`. + PtrRead16, + /// Dispatches `ptr_read32(...)`. + PtrRead32, + /// Dispatches `ptr_read_string(...)`. + PtrReadString, + /// Dispatches `ptr_set(...)`. + PtrSet, + /// Dispatches `ptr_sizeof(...)`. + PtrSizeof, + /// Dispatches `ptr_write8(...)`. + PtrWrite8, + /// Dispatches `ptr_write16(...)`. + PtrWrite16, + /// Dispatches `ptr_write32(...)`. + PtrWrite32, + /// Dispatches `ptr_write_string(...)`. + PtrWriteString, + /// Dispatches by-value `settype(...)` callable calls. + Settype, + /// Dispatches `addslashes(...)` and `stripslashes(...)`. + Slashes, + /// Dispatches `sin(...)`. + Sin, + /// Dispatches `sinh(...)`. + Sinh, + /// Dispatches `sqrt(...)`. + Sqrt, + /// Dispatches string ASCII case-conversion builtins. + StringCase, + /// Dispatches string comparison builtins. + StringCompare, + /// Dispatches string position builtins. + StringPosition, + /// Dispatches string search predicate builtins. + StringSearch, + /// Dispatches `explode(...)` and `implode(...)`. + StringSplitJoin, + /// Dispatches stream boolean predicate builtins. + StreamBoolPredicate, + /// Dispatches stream introspection list builtins. + StreamIntrospection, + /// Dispatches `str_pad(...)`. + StrPad, + /// Dispatches `str_replace(...)` and `str_ireplace(...)`. + StrReplace, + /// Dispatches `str_split(...)`. + StrSplit, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `str_repeat(...)`. + StrRepeat, + /// Dispatches `strval(...)`. + Strval, + /// Dispatches `strrev(...)`. + Strrev, + /// Dispatches `strstr(...)`. + Strstr, + /// Dispatches `substr(...)`. + Substr, + /// Dispatches `substr_replace(...)`. + SubstrReplace, + /// Dispatches symbol, class metadata, SPL, and language-construct probes. + Symbols, + /// Dispatches `tan(...)`. + Tan, + /// Dispatches `tanh(...)`. + Tanh, + /// Dispatches date, time, and sleep builtins. + Time, + /// Dispatches trim-family builtins. + TrimLike, + /// Dispatches `ucwords(...)`. + Ucwords, + /// Dispatches `nl2br(...)`. + Nl2br, + /// Dispatches `wordwrap(...)`. + Wordwrap, + /// Dispatches URL decode builtins. + UrlDecode, + /// Dispatches URL encode builtins. + UrlEncode, +} + +impl EvalValuesHook { + /// Runs an evaluated-argument builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => one_arg(evaluated_args, values, eval_abs_result), + Self::Acos => one_arg(evaluated_args, values, eval_acos_result), + Self::ArrayAggregate + | Self::Array + | Self::ArrayMutating + | Self::ArrayFlip + | Self::ArrayKeyExists + | Self::ArrayPad + | Self::ArrayKeys + | Self::ArrayRand + | Self::ArrayReverse + | Self::ArraySearch + | Self::ArraySlice + | Self::ArrayUnique + | Self::ArrayValues + | Self::Count + | Self::Range => { + eval_array_declared_values_result(name, evaluated_args, context, values) + } + Self::Asin => one_arg(evaluated_args, values, eval_asin_result), + Self::Atan => one_arg(evaluated_args, values, eval_atan_result), + Self::Atan2 => two_args(evaluated_args, values, eval_atan2_result), + Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), + Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), + Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), + Self::Boolval => one_arg(evaluated_args, values, eval_boolval_result), + Self::Ceil => one_arg(evaluated_args, values, eval_ceil_result), + Self::Chr => one_arg(evaluated_args, values, eval_chr_result), + Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), + Self::Core => eval_core_values_result(name, evaluated_args, context, values), + Self::Cos => one_arg(evaluated_args, values, eval_cos_result), + Self::Cosh => one_arg(evaluated_args, values, eval_cosh_result), + Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), + Self::Ctype => one_arg(evaluated_args, values, |value, values| match name { + "ctype_alnum" => eval_ctype_alnum_result(value, values), + "ctype_alpha" => eval_ctype_alpha_result(value, values), + "ctype_digit" => eval_ctype_digit_result(value, values), + "ctype_space" => eval_ctype_space_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + }), + Self::Deg2rad => one_arg(evaluated_args, values, eval_deg2rad_result), + Self::Exp => one_arg(evaluated_args, values, eval_exp_result), + Self::Fdiv => two_args(evaluated_args, values, eval_fdiv_result), + Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, context, values), + Self::Floor => one_arg(evaluated_args, values, eval_floor_result), + Self::Fmod => two_args(evaluated_args, values, eval_fmod_result), + Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), + Self::Hypot => two_args(evaluated_args, values, eval_hypot_result), + Self::Floatval => one_arg(evaluated_args, values, eval_floatval_result), + Self::Intval => one_arg(evaluated_args, values, eval_intval_result), + Self::IsArray => one_arg(evaluated_args, values, eval_is_array_result), + Self::IsBool => one_arg(evaluated_args, values, eval_is_bool_result), + Self::IsDouble => one_arg(evaluated_args, values, eval_is_double_result), + Self::IsFinite => one_arg(evaluated_args, values, eval_is_finite_result), + Self::IsFloat => one_arg(evaluated_args, values, eval_is_float_result), + Self::IsInfinite => one_arg(evaluated_args, values, eval_is_infinite_result), + Self::IsInt => one_arg(evaluated_args, values, eval_is_int_result), + Self::IsInteger => one_arg(evaluated_args, values, eval_is_integer_result), + Self::IsIterable => one_arg(evaluated_args, values, |value, values| { + eval_is_iterable_result(value, context, values) + }), + Self::IsLong => one_arg(evaluated_args, values, eval_is_long_result), + Self::IsNan => one_arg(evaluated_args, values, eval_is_nan_result), + Self::IsNull => one_arg(evaluated_args, values, eval_is_null_result), + Self::IsNumeric => one_arg(evaluated_args, values, eval_is_numeric_result), + Self::IsObject => one_arg(evaluated_args, values, eval_is_object_result), + Self::IsReal => one_arg(evaluated_args, values, eval_is_real_result), + Self::IsResource => one_arg(evaluated_args, values, eval_is_resource_result), + Self::IsScalar => one_arg(evaluated_args, values, eval_is_scalar_result), + Self::IsString => one_arg(evaluated_args, values, eval_is_string_result), + Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), + Self::Gzip => match name { + "gzcompress" => eval_gzcompress_result(evaluated_args, values), + "gzdeflate" => eval_gzdeflate_result(evaluated_args, values), + "gzinflate" => eval_gzinflate_result(evaluated_args, values), + "gzuncompress" => eval_gzuncompress_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::HashAlgos => eval_hash_algos_declared_values_result(evaluated_args, values), + Self::HashContext => match name { + "hash_copy" => { + eval_hash_copy_declared_values_result(evaluated_args, context, values) + } + "hash_final" => { + eval_hash_final_declared_values_result(evaluated_args, context, values) + } + "hash_init" => { + eval_hash_init_declared_values_result(evaluated_args, context, values) + } + "hash_update" => { + eval_hash_update_declared_values_result(evaluated_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::HashEquals => two_args(evaluated_args, values, eval_hash_equals_result), + Self::HashOneShot => match name { + "hash" => eval_hash_result(evaluated_args, values), + "hash_file" => eval_hash_file_result(evaluated_args, values), + "hash_hmac" => eval_hash_hmac_result(evaluated_args, values), + "md5" => eval_md5_result(evaluated_args, values), + "sha1" => eval_sha1_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Hex2Bin => one_arg(evaluated_args, values, eval_hex2bin_result), + Self::HtmlEntity => { + // htmlspecialchars/htmlentities accept optional flags/encoding args; + // like the static runtime they are accepted without effect (ENT_QUOTES). + let value = match (name, evaluated_args) { + (_, [value]) => *value, + ("htmlspecialchars" | "htmlentities", [value, _flags]) => *value, + ("htmlspecialchars" | "htmlentities", [value, _flags, _encoding]) => *value, + _ => return Err(EvalStatus::RuntimeFatal), + }; + match name { + "html_entity_decode" => eval_html_entity_decode_result(value, values), + "htmlentities" => eval_htmlentities_result(value, values), + "htmlspecialchars" => eval_htmlspecialchars_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + } + } + Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), + Self::JsonDecode => eval_json_decode_values_result(evaluated_args, context, values), + Self::JsonEncode => eval_json_encode_values_result(evaluated_args, context, values), + Self::JsonLastError => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_result(context, values) + } + Self::JsonLastErrorMsg => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_msg_result(context, values) + } + Self::JsonValidate => eval_json_validate_values_result(evaluated_args, context, values), + Self::Log => match evaluated_args { + [num] => eval_log_result(*num, None, values), + [num, base] => eval_log_result(*num, Some(*base), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Log2 => one_arg(evaluated_args, values, eval_log2_result), + Self::Log10 => one_arg(evaluated_args, values, eval_log10_result), + Self::Max => eval_max_result(evaluated_args, values), + Self::Min => eval_min_result(evaluated_args, values), + Self::MtRand => eval_mt_rand_values_result(evaluated_args, values), + Self::NetworkEnv => eval_network_env_values_result(name, evaluated_args, values), + Self::NumberFormat => { + eval_number_format_declared_values_result(evaluated_args, values) + } + Self::Ord => one_arg(evaluated_args, values, eval_ord_result), + Self::Pi => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_pi_result(values) + } + Self::Printf => eval_printf_result(evaluated_args, values), + Self::Pow => two_args(evaluated_args, values, eval_pow_result), + Self::Rad2deg => one_arg(evaluated_args, values, eval_rad2deg_result), + Self::Rand => eval_rand_values_result(evaluated_args, values), + Self::RandomInt => eval_random_int_values_result(evaluated_args, values), + Self::Round => match evaluated_args { + [value] => eval_round_result(*value, None, values), + [value, precision] => eval_round_result(*value, Some(*precision), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::MbEregMatch => eval_mb_ereg_match_values_result(evaluated_args, values), + Self::PregMatch => eval_preg_match_values_result(evaluated_args, values), + Self::PregMatchAll => eval_preg_match_all_values_result(evaluated_args, values), + Self::PregReplace => eval_preg_replace_values_result(evaluated_args, values), + Self::PregReplaceCallback => { + eval_preg_replace_callback_values_result(evaluated_args, context, values) + } + Self::PregSplit => eval_preg_split_values_result(evaluated_args, values), + Self::BufferFree => eval_buffer_free_values_result(evaluated_args, values), + Self::BufferLen => eval_buffer_len_values_result(evaluated_args, values), + Self::BufferNew => eval_buffer_new_values_result(evaluated_args, values), + Self::Ptr => eval_ptr_values_result(evaluated_args), + Self::PtrGet => eval_ptr_get_values_result(evaluated_args, values), + Self::PtrIsNull => eval_ptr_is_null_values_result(evaluated_args, values), + Self::PtrNull => eval_ptr_null_values_result(evaluated_args, values), + Self::PtrOffset => eval_ptr_offset_values_result(evaluated_args, values), + Self::PtrRead8 => eval_ptr_read8_values_result(evaluated_args, values), + Self::PtrRead16 => eval_ptr_read16_values_result(evaluated_args, values), + Self::PtrRead32 => eval_ptr_read32_values_result(evaluated_args, values), + Self::PtrReadString => eval_ptr_read_string_values_result(evaluated_args, values), + Self::PtrSet => eval_ptr_set_values_result(evaluated_args, values), + Self::PtrSizeof => eval_ptr_sizeof_values_result(evaluated_args, context, values), + Self::PtrWrite8 => eval_ptr_write8_values_result(evaluated_args, values), + Self::PtrWrite16 => eval_ptr_write16_values_result(evaluated_args, values), + Self::PtrWrite32 => eval_ptr_write32_values_result(evaluated_args, values), + Self::PtrWriteString => eval_ptr_write_string_values_result(evaluated_args, values), + Self::Settype => eval_settype_values_result(evaluated_args, values), + Self::Sin => one_arg(evaluated_args, values, eval_sin_result), + Self::Sinh => one_arg(evaluated_args, values, eval_sinh_result), + Self::Slashes => one_arg(evaluated_args, values, |value, values| match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + }), + Self::Sprintf => eval_sprintf_result(evaluated_args, values), + Self::Sqrt => one_arg(evaluated_args, values, eval_sqrt_result), + Self::Sscanf => eval_sscanf_values_result(evaluated_args, values), + Self::StringCase => one_arg(evaluated_args, values, |value, values| match name { + "lcfirst" => eval_lcfirst_result(value, values), + "strtolower" => eval_strtolower_result(value, values), + "strtoupper" => eval_strtoupper_result(value, values), + "ucfirst" => eval_ucfirst_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + }), + Self::StringCompare => two_args(evaluated_args, values, |left, right, values| { + match name { + "strcasecmp" => eval_strcasecmp_result(left, right, values), + "strcmp" => eval_strcmp_result(left, right, values), + _ => Err(EvalStatus::RuntimeFatal), + } + }), + Self::StringPosition => two_args(evaluated_args, values, |haystack, needle, values| { + match name { + "strpos" => eval_strpos_result(haystack, needle, values), + "strrpos" => eval_strrpos_result(haystack, needle, values), + _ => Err(EvalStatus::RuntimeFatal), + } + }), + Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { + match name { + "str_contains" => eval_str_contains_result(haystack, needle, values), + "str_ends_with" => eval_str_ends_with_result(haystack, needle, values), + "str_starts_with" => eval_str_starts_with_result(haystack, needle, values), + _ => Err(EvalStatus::RuntimeFatal), + } + }), + Self::StringSplitJoin => match name { + "explode" => eval_explode_declared_values_result(evaluated_args, values), + "implode" => eval_implode_declared_values_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StreamBoolPredicate => one_arg(evaluated_args, values, |stream, values| { + match name { + "stream_is_local" => eval_stream_is_local_result(stream, values), + "stream_supports_lock" => eval_stream_supports_lock_result(stream, values), + _ => Err(EvalStatus::RuntimeFatal), + } + }), + Self::StreamIntrospection => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "stream_get_filters" => eval_stream_get_filters_result(context, values), + "stream_get_transports" => eval_stream_get_transports_result(context, values), + "stream_get_wrappers" => eval_stream_get_wrappers_result(context, values), + _ => Err(EvalStatus::RuntimeFatal), + } + } + Self::StrPad => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values), + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StrReplace => { + three_args(evaluated_args, values, |search, replace, subject, values| { + match name { + "str_ireplace" => { + eval_str_ireplace_result(search, replace, subject, values) + } + "str_replace" => { + eval_str_replace_result(name, search, replace, subject, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } + }) + } + Self::StrSplit => match evaluated_args { + [value] => eval_str_split_result(*value, None, values), + [value, length] => eval_str_split_result(*value, Some(*length), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Strlen => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) + } + Self::StrRepeat => two_args(evaluated_args, values, eval_str_repeat_result), + Self::Strval => one_arg(evaluated_args, values, |value, values| { + eval_strval_result(value, context, values) + }), + Self::Strrev => one_arg(evaluated_args, values, eval_strrev_result), + Self::Strstr => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Substr => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values), + [value, offset, length] => { + eval_substr_result(*value, *offset, Some(*length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::SubstrReplace => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values) + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Symbols => eval_symbols_values_result(name, evaluated_args, context, values), + Self::Tan => one_arg(evaluated_args, values, eval_tan_result), + Self::Tanh => one_arg(evaluated_args, values, eval_tanh_result), + Self::Time => eval_time_values_result(name, evaluated_args, context, values), + Self::TrimLike => match (name, evaluated_args) { + ("chop", [value]) => eval_chop_result(*value, None, values), + ("chop", [value, mask]) => eval_chop_result(*value, Some(*mask), values), + ("ltrim", [value]) => eval_ltrim_result(*value, None, values), + ("ltrim", [value, mask]) => eval_ltrim_result(*value, Some(*mask), values), + ("rtrim", [value]) => eval_rtrim_result(*value, None, values), + ("rtrim", [value, mask]) => eval_rtrim_result(*value, Some(*mask), values), + ("trim", [value]) => eval_trim_result(*value, None, values), + ("trim", [value, mask]) => eval_trim_result(*value, Some(*mask), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Ucwords => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values), + [value, separators] => eval_ucwords_result(*value, Some(*separators), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Vprintf => eval_vprintf_result(evaluated_args, values), + Self::Vsprintf => eval_vsprintf_result(evaluated_args, values), + Self::Nl2br => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values), + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Wordwrap => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values), + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values), + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values) + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::UrlDecode => one_arg(evaluated_args, values, |value, values| match name { + "rawurldecode" => eval_rawurldecode_result(value, values), + "urldecode" => eval_urldecode_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + }), + Self::UrlEncode => one_arg(evaluated_args, values, |value, values| match name { + "rawurlencode" => eval_rawurlencode_result(value, values), + "urlencode" => eval_urlencode_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + }), + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs new file mode 100644 index 0000000000..4324e88625 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs @@ -0,0 +1,306 @@ +//! Purpose: +//! Eval registry entry and dispatch wrappers for `json_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns the decoder implementation, runtime-cell materialization, +//! direct wrapper, and by-value dispatch shape. +//! - JSON parse-error helpers live here and are reused by `json_validate`. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; + +eval_builtin! { + name: "json_decode", + area: Json, + params: [ + json, + associative = EvalBuiltinDefaultValue::Null, + depth = EvalBuiltinDefaultValue::Int(512), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: JsonDecode, + values: JsonDecode, +} + +/// Evaluates PHP `json_decode()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_decode_result(json, None, None, None, context, values) + } + [json, associative] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_json_decode_result(json, Some(associative), None, None, context, values) + } + [json, associative, depth] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) + } + [json, associative, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), Some(flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_decode()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_decode_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values), + [json, associative] => eval_json_decode_result(*json, Some(*associative), None, None, context, values), + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + ), + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. +fn eval_json_decode_result( + json: RuntimeCellHandle, + associative: Option, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_BIGINT_AS_STRING + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let objects_as_assoc = associative + .map(|associative| values.truthy(associative)) + .transpose()? + .unwrap_or(false); + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { + json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) + } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => { + let (code, message) = eval_json_parse_error_details(error, &bytes); + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(code, &message, context, values); + } + context.set_json_error(code, message); + return values.null(); + } + }; + context.clear_json_error(); + eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) +} + +/// Materializes one parsed JSON value as an eval runtime cell. +fn eval_json_decode_to_cell( + value: JsonValue, + flags: i64, + objects_as_assoc: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + JsonValue::Null => values.null(), + JsonValue::Bool(value) => values.bool_value(value), + JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), + JsonValue::String(value) => values.string_bytes_value(&value), + JsonValue::Array(elements) => { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.into_iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, element)?; + } + Ok(result) + } + JsonValue::Object(entries) => { + if !objects_as_assoc { + return eval_json_decode_object_to_cell(entries, flags, values); + } + let mut result = values.assoc_new(entries.len())?; + for (key, value) in entries { + let key = values.string_bytes_value(&key)?; + let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) + } + } +} + +/// Materializes a parsed JSON object as a `stdClass` runtime object. +fn eval_json_decode_object_to_cell( + entries: Vec<(Vec, JsonValue)>, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + for (key, value) in entries { + let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = eval_json_decode_to_cell(value, flags, false, values)?; + values.property_set(object, key, value)?; + } + Ok(object) +} + +/// Materializes one JSON number as an int when possible and as a float otherwise. +fn eval_json_decode_number_to_cell( + value: &[u8], + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { + return values.string_bytes_value(value); + } + let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; + if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { + if let Ok(integer) = value.parse::() { + return values.int(integer); + } + } + let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; + values.float(float) +} + +/// Returns true when one integer-grammar JSON number exceeds PHP's int range. +fn eval_json_number_overflows_i64(value: &[u8]) -> bool { + if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { + return false; + } + let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { + (true, digits) + } else { + (false, value) + }; + let threshold = if negative { + b"9223372036854775808".as_slice() + } else { + b"9223372036854775807".as_slice() + }; + digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold +} + +/// Records one parser error into the eval-local PHP JSON error slots. +pub(super) fn eval_record_json_parse_error( + context: &mut ElephcEvalContext, + error: JsonParseError, + bytes: &[u8], +) { + let (code, message) = eval_json_parse_error_details(error, bytes); + context.set_json_error(code, message); +} + +/// Builds the PHP JSON error code and message for one parser failure. +fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { + let (code, message) = eval_json_parse_error_status(error.kind()); + let message = eval_json_error_message_with_location(message, bytes, error.offset()); + (code, message) +} + +/// Creates and schedules a `JsonException` through eval's normal Throwable channel. +pub(super) fn eval_throw_json_exception( + code: i64, + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.set_json_error(code, message.to_string()); + let exception = values.new_object("JsonException")?; + let message = values.string(message)?; + let code = values.int(code)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. +fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { + match error { + JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), + JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), + JsonParseErrorKind::ControlChar => ( + EVAL_JSON_ERROR_CTRL_CHAR, + "Control character error, possibly incorrectly encoded", + ), + JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), + JsonParseErrorKind::Utf16 => ( + EVAL_JSON_ERROR_UTF16, + "Single unpaired UTF-16 surrogate in unicode escape", + ), + } +} + +/// Adds PHP's JSON line/column suffix to one base error message. +fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { + let (line, column) = eval_json_error_location(bytes, offset); + format!("{message} near location {line}:{column}") +} + +/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. +fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { + let mut line = 1; + let mut column = 1; + let offset = offset.min(bytes.len()); + for byte in &bytes[..offset] { + if *byte == b'\n' { + line += 1; + column = 1; + } else { + column += 1; + } + } + (line, column) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs new file mode 100644 index 0000000000..ffc5f0b3fb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs @@ -0,0 +1,602 @@ +//! Purpose: +//! Eval registry entry and dispatch wrappers for `json_encode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns the encoder implementation, direct wrapper, and by-value +//! dispatch shape. +//! - Shared `JSON_THROW_ON_ERROR` exception construction is reused from +//! `json_decode` instead of a separate area-level helper module. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "json_encode", + area: Json, + params: [ + value, + flags = EvalBuiltinDefaultValue::Int(0), + depth = EvalBuiltinDefaultValue::Int(512), + ], + direct: JsonEncode, + values: JsonEncode, +} + +/// Evaluates PHP `json_encode()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_json_encode_result(value, None, None, context, values) + } + [value, flags] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_encode_result(value, Some(flags), None, context, values) + } + [value, flags, depth] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_encode_result(value, Some(flags), Some(depth), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_encode()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_encode_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values), + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values), + [value, flags, depth] => eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Encodes one runtime cell as a JSON string for eval's supported flag subset. +fn eval_json_encode_result( + value: RuntimeCellHandle, + flags: Option, + depth: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_HEX_TAG + | EVAL_JSON_HEX_AMP + | EVAL_JSON_HEX_APOS + | EVAL_JSON_HEX_QUOT + | EVAL_JSON_UNESCAPED_SLASHES + | EVAL_JSON_UNESCAPED_UNICODE + | EVAL_JSON_FORCE_OBJECT + | EVAL_JSON_PRETTY_PRINT + | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR + | EVAL_JSON_PRESERVE_ZERO_FRACTION + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let mut output = Vec::new(); + let mut error = None; + eval_json_encode_append( + value, + values, + flags, + depth as usize, + 0, + &mut Vec::new(), + &mut error, + &mut output, + )?; + if let Some(error) = error { + context.set_json_error(error.code, error.message); + if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return super::json_decode::eval_throw_json_exception(error.code, error.message, context, values); + } + return values.bool_value(false); + } + } else { + context.clear_json_error(); + } + values.string_bytes_value(&output) +} + +/// Appends one JSON value to the output buffer. +fn eval_json_encode_append( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_FLOAT => { + eval_json_encode_append_float(value, values, flags, error, output)?; + } + EVAL_TAG_STRING => eval_json_encode_append_string( + &values.string_bytes(value)?, + flags, + EvalJsonStringPosition::Value, + error, + output, + )?, + EVAL_TAG_BOOL => { + if values.truthy(value)? { + output.extend_from_slice(b"true"); + } else { + output.extend_from_slice(b"false"); + } + } + EVAL_TAG_ARRAY => { + eval_json_encode_append_indexed_array( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_ASSOC => { + eval_json_encode_append_assoc( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_OBJECT => { + eval_json_encode_append_object( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct EvalJsonEncodeError { + code: i64, + message: &'static str, +} + +/// Marks whether a JSON string is being encoded as a value or as an object key. +#[derive(Clone, Copy)] +enum EvalJsonStringPosition { + Value, + Key, +} + +/// Appends one JSON float while preserving a `.0` suffix when requested. +fn eval_json_encode_append_float( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let float = eval_float_value(value, values)?; + if !float.is_finite() { + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_INF_OR_NAN, + message: EVAL_JSON_INF_OR_NAN_MESSAGE, + }); + output.push(b'0'); + return Ok(()); + } + let bytes = values.string_bytes(value)?; + output.extend_from_slice(&bytes); + if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 + && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + { + output.extend_from_slice(b".0"); + } + Ok(()) +} + +/// Appends one indexed eval array as a JSON array or forced JSON object. +fn eval_json_encode_append_indexed_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(if force_object { b'{' } else { b'[' }); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + if force_object { + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + } + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(if force_object { b'}' } else { b']' }); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one associative eval array as a JSON object. +fn eval_json_encode_append_assoc( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one eval runtime object as a JSON object. +fn eval_json_encode_append_object( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.object_property_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.object_property_iter_key(value, position)?; + let key_bytes = values.string_bytes(key)?; + eval_json_encode_append_string( + &key_bytes, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let element = values.property_get(value, property)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends a JSON object colon, including pretty-print spacing when active. +fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { + if flags & EVAL_JSON_PRETTY_PRINT != 0 { + output.extend_from_slice(b": "); + } else { + output.push(b':'); + } +} + +/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. +fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + +/// Records entry into one JSON array/object, rejecting depth overrun and recursion. +fn eval_json_encode_enter_array( + value: RuntimeCellHandle, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, +) -> Result<(), EvalStatus> { + if depth >= depth_limit { + return Err(EvalStatus::RuntimeFatal); + } + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Err(EvalStatus::RuntimeFatal); + } + arrays_seen.push(address); + Ok(()) +} + +/// Appends one JSON string with eval-supported PHP flag handling. +fn eval_json_encode_append_string( + bytes: &[u8], + flags: i64, + position: EvalJsonStringPosition, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + if flags & EVAL_JSON_NUMERIC_CHECK != 0 { + if let Some(number) = eval_json_numeric_check_bytes(bytes) { + output.extend_from_slice(&number); + return Ok(()); + } + } + let start_len = output.len(); + output.push(b'"'); + if let Ok(value) = std::str::from_utf8(bytes) { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { + output.truncate(start_len); + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_UTF8, + message: EVAL_JSON_UTF8_MESSAGE, + }); + match position { + EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), + EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), + } + return Ok(()); + } else { + eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; + } + output.push(b'"'); + Ok(()) +} + +/// Appends one valid UTF-8 character using PHP JSON string escaping rules. +fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { + if character.is_ascii() { + eval_json_encode_append_ascii_byte(character as u8, flags, output); + } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { + let mut buffer = [0_u8; 4]; + output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); + } else { + eval_json_encode_append_unicode_escape(character as u32, output); + } +} + +/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. +fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { + match byte { + b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), + b'"' => output.extend_from_slice(b"\\\""), + b'\\' => output.extend_from_slice(b"\\\\"), + b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { + output.extend_from_slice(b"\\/"); + } + b'/' => output.push(b'/'), + b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), + b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), + b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), + b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), + b'\x08' => output.extend_from_slice(b"\\b"), + b'\x0c' => output.extend_from_slice(b"\\f"), + b'\n' => output.extend_from_slice(b"\\n"), + b'\r' => output.extend_from_slice(b"\\r"), + b'\t' => output.extend_from_slice(b"\\t"), + control @ 0x00..=0x1f => { + output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); + } + _ => output.push(byte), + } +} + +/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. +fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { + if codepoint <= 0xffff { + output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); + return; + } + + let codepoint = codepoint - 0x1_0000; + let high = 0xd800 + ((codepoint >> 10) & 0x3ff); + let low = 0xdc00 + (codepoint & 0x3ff); + output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); +} + +/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. +fn eval_json_encode_append_invalid_utf8_bytes( + mut bytes: &[u8], + flags: i64, + output: &mut Vec, +) -> Result<(), EvalStatus> { + while !bytes.is_empty() { + match std::str::from_utf8(bytes) { + Ok(value) => { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + return Ok(()); + } + Err(error) => { + let valid = &bytes[..error.valid_up_to()]; + for character in std::str::from_utf8(valid) + .map_err(|_| EvalStatus::RuntimeFatal)? + .chars() + { + eval_json_encode_append_char(character, flags, output); + } + let invalid_len = error + .error_len() + .unwrap_or(bytes.len() - valid.len()) + .max(1); + if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { + eval_json_encode_append_char('\u{fffd}', flags, output); + } + bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; + } + } + } + Ok(()) +} + +/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. +fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { + let value = std::str::from_utf8(bytes).ok()?.trim(); + if value.is_empty() { + return None; + } + let integer_grammar = value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); + if integer_grammar { + if let Ok(integer) = value.parse::() { + return Some(integer.to_string().into_bytes()); + } + } + let number = value.parse::().ok()?; + if number.is_finite() { + Some(number.to_string().into_bytes()) + } else { + None + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs new file mode 100644 index 0000000000..1418be0600 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Eval registry entry and implementation for `json_last_error`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The result reads the current eval JSON error code from `ElephcEvalContext`. + +use super::super::super::*; + +eval_builtin! { + name: "json_last_error", + area: Json, + params: [], + direct: JsonLastError, + values: JsonLastError, +} + +/// Evaluates PHP `json_last_error()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_json_last_error( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_result(context, values) +} + +/// Returns the current JSON error code. +pub(in crate::interpreter) fn eval_json_last_error_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(context.json_last_error()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs new file mode 100644 index 0000000000..9d3c41d3ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Eval registry entry and implementation for `json_last_error_msg`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The result reads the current eval JSON error message from `ElephcEvalContext`. + +use super::super::super::*; + +eval_builtin! { + name: "json_last_error_msg", + area: Json, + params: [], + direct: JsonLastErrorMsg, + values: JsonLastErrorMsg, +} + +/// Evaluates PHP `json_last_error_msg()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_json_last_error_msg( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_msg_result(context, values) +} + +/// Returns the current JSON error message. +pub(in crate::interpreter) fn eval_json_last_error_msg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(context.json_last_error_msg()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs new file mode 100644 index 0000000000..6633c045f2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! Eval registry entry and dispatch wrappers for `json_validate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns the validation implementation, direct wrapper, and by-value +//! dispatch shape. +//! - JSON parse-error recording is reused from `json_decode` instead of a +//! separate area-level helper module. + +use super::json_decode::eval_record_json_parse_error; +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use crate::json_validate; + +eval_builtin! { + name: "json_validate", + area: Json, + params: [ + json, + depth = EvalBuiltinDefaultValue::Int(512), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: JsonValidate, + values: JsonValidate, +} + +/// Evaluates PHP `json_validate()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_validate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_validate_result(json, None, None, context, values) + } + [json, depth] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_validate_result(json, Some(depth), None, context, values) + } + [json, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_validate_result(json, Some(depth), Some(flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_validate()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_validate_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values), + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values), + [json, depth, flags] => eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. +fn eval_json_validate_result( + json: RuntimeCellHandle, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + match result { + Ok(_) => { + context.clear_json_error(); + values.bool_value(true) + } + Err(error) => { + eval_record_json_parse_error(context, error, &bytes); + values.bool_value(false) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/mod.rs b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs new file mode 100644 index 0000000000..d5676f8b5f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Groups eval registry entries and dispatch wrappers for JSON builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers and implementation code. +//! - Helper reuse stays between builtin files instead of an area-level +//! implementation module when one builtin naturally owns the behavior. + +mod json_decode; +mod json_encode; +mod json_last_error; +mod json_last_error_msg; +mod json_validate; + +pub(in crate::interpreter) use json_decode::*; +pub(in crate::interpreter) use json_encode::*; +pub(in crate::interpreter) use json_last_error::*; +pub(in crate::interpreter) use json_last_error_msg::*; +pub(in crate::interpreter) use json_validate::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs new file mode 100644 index 0000000000..e2a7e74e3b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -0,0 +1,250 @@ +//! Purpose: +//! Declarative helpers for registering eval-side PHP builtins. +//! The macro keeps per-builtin files compact while preserving the interpreter +//! registry as the runtime lookup surface. +//! +//! Called from: +//! - `crate::interpreter::builtins::::` home files. +//! +//! Key details: +//! - Macro expansion submits static metadata to `inventory`. +//! - Dispatch hooks are magician-specific enums so handlers can stay generic +//! over `RuntimeValueOps`. + +macro_rules! eval_builtin { + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + by_ref: [$($by_ref:ident),* $(,)?], + direct: none, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: None, + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, + direct: None, + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + by_ref: [$($by_ref:ident),* $(,)?], + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: None, + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + variadic: $variadic:ident, + by_ref: [$($by_ref:ident),* $(,)?], + direct: none, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param),)* eval_builtin!(@name_str $variadic)], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: Some(eval_builtin!(@name_str $variadic)), + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, + direct: None, + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + variadic: $variadic:ident, + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param),)* eval_builtin!(@name_str $variadic)], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: Some(eval_builtin!(@name_str $variadic)), + by_ref_params: &[], + required_param_count: None, + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + required: $required:expr, + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: None, + by_ref_params: &[], + required_param_count: Some($required), + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: None, + by_ref_params: &[], + required_param_count: None, + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), + } + } + }; + + (@default) => { + None + }; + + (@default $default:expr) => { + Some($default) + }; + + (@param_by_ref) => { + false + }; + + (@param_by_ref by_ref) => { + true + }; + + (@name_str r#break) => { + "break" + }; + + (@name_str r#class) => { + "class" + }; + + (@name_str r#enum) => { + "enum" + }; + + (@name_str r#return) => { + "return" + }; + + (@name_str r#trait) => { + "trait" + }; + + (@name_str r#type) => { + "type" + }; + + (@name_str $name:ident) => { + stringify!($name) + }; +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/abs.rs b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs new file mode 100644 index 0000000000..b46f4cd1de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation for `abs`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime scalar absolute-value coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "abs", + area: Math, + params: [num], + direct: Abs, + values: Abs, +} + +/// Evaluates PHP `abs()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_abs_result(num, values) +} + +/// Applies PHP `abs()` to one already evaluated value. +pub(in crate::interpreter) fn eval_abs_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.abs(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/acos.rs b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs new file mode 100644 index 0000000000..93e3693dbc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `acos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "acos", + area: Math, + params: [num], + direct: Acos, + values: Acos, +} + +/// Evaluates PHP `acos()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_acos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_acos_result(num, values) +} + +/// Applies PHP `acos()` to one already evaluated value. +pub(in crate::interpreter) fn eval_acos_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.acos()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/asin.rs b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs new file mode 100644 index 0000000000..c1b785a651 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `asin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "asin", + area: Math, + params: [num], + direct: Asin, + values: Asin, +} + +/// Evaluates PHP `asin()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_asin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_asin_result(num, values) +} + +/// Applies PHP `asin()` to one already evaluated value. +pub(in crate::interpreter) fn eval_asin_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.asin()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs new file mode 100644 index 0000000000..54d240e989 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `atan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "atan", + area: Math, + params: [num], + direct: Atan, + values: Atan, +} + +/// Evaluates PHP `atan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_atan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_atan_result(num, values) +} + +/// Applies PHP `atan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_atan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.atan()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs new file mode 100644 index 0000000000..474cf1dee9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `atan2`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Both arguments are evaluated in source order before float coercion. + +use super::super::super::*; + +eval_builtin! { + name: "atan2", + area: Math, + params: [y, x], + direct: Atan2, + values: Atan2, +} + +/// Evaluates PHP `atan2()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_atan2( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_atan2_result(left, right, values) +} + +/// Applies PHP `atan2()` to two already evaluated values. +pub(in crate::interpreter) fn eval_atan2_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + values.float(left.atan2(right)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs new file mode 100644 index 0000000000..9a15ceb4d9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation for `ceil`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "ceil", + area: Math, + params: [num], + direct: Ceil, + values: Ceil, +} + +/// Evaluates PHP `ceil()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_ceil_result(num, values) +} + +/// Applies PHP `ceil()` to one already evaluated value. +pub(in crate::interpreter) fn eval_ceil_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.ceil(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs new file mode 100644 index 0000000000..11de92df83 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Eval registry entry and implementation for `clamp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Bounds are validated before comparison and NaN bounds are runtime fatals. + +use super::super::super::*; + +eval_builtin! { + name: "clamp", + area: Math, + params: [value, min, max], + direct: Clamp, + values: Clamp, +} + +/// Evaluates PHP `clamp()` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +pub(in crate::interpreter) fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cos.rs b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs new file mode 100644 index 0000000000..9db085ed6b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `cos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "cos", + area: Math, + params: [num], + direct: Cos, + values: Cos, +} + +/// Evaluates PHP `cos()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_cos_result(num, values) +} + +/// Applies PHP `cos()` to one already evaluated value. +pub(in crate::interpreter) fn eval_cos_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.cos()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs new file mode 100644 index 0000000000..132a8010fe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `cosh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "cosh", + area: Math, + params: [num], + direct: Cosh, + values: Cosh, +} + +/// Evaluates PHP `cosh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cosh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_cosh_result(num, values) +} + +/// Applies PHP `cosh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_cosh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.cosh()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs new file mode 100644 index 0000000000..106c4f199b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `deg2rad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "deg2rad", + area: Math, + params: [num], + direct: Deg2rad, + values: Deg2rad, +} + +/// Evaluates PHP `deg2rad()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_deg2rad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_deg2rad_result(num, values) +} + +/// Applies PHP `deg2rad()` to one already evaluated value. +pub(in crate::interpreter) fn eval_deg2rad_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.to_radians()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/exp.rs b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs new file mode 100644 index 0000000000..a65da62683 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `exp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "exp", + area: Math, + params: [num], + direct: Exp, + values: Exp, +} + +/// Evaluates PHP `exp()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_exp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_exp_result(num, values) +} + +/// Applies PHP `exp()` to one already evaluated value. +pub(in crate::interpreter) fn eval_exp_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.exp()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs new file mode 100644 index 0000000000..3c61b4c601 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `fdiv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "fdiv", + area: Math, + params: [num1, num2], + direct: Fdiv, + values: Fdiv, +} + +/// Evaluates PHP `fdiv()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_fdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_fdiv_result(left, right, values) +} + +/// Applies PHP `fdiv()` to two already evaluated values. +pub(in crate::interpreter) fn eval_fdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.fdiv(left, right) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/floor.rs b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs new file mode 100644 index 0000000000..9f51b03dad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation for `floor`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "floor", + area: Math, + params: [num], + direct: Floor, + values: Floor, +} + +/// Evaluates PHP `floor()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_floor_result(num, values) +} + +/// Applies PHP `floor()` to one already evaluated value. +pub(in crate::interpreter) fn eval_floor_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.floor(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs new file mode 100644 index 0000000000..3284726764 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `fmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "fmod", + area: Math, + params: [num1, num2], + direct: Fmod, + values: Fmod, +} + +/// Evaluates PHP `fmod()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_fmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_fmod_result(left, right, values) +} + +/// Applies PHP `fmod()` to two already evaluated values. +pub(in crate::interpreter) fn eval_fmod_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.fmod(left, right) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs new file mode 100644 index 0000000000..b302f7821b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `hypot`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Both arguments are evaluated in source order before float coercion. + +use super::super::super::*; + +eval_builtin! { + name: "hypot", + area: Math, + params: [x, y], + direct: Hypot, + values: Hypot, +} + +/// Evaluates PHP `hypot()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hypot( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_hypot_result(left, right, values) +} + +/// Applies PHP `hypot()` to two already evaluated values. +pub(in crate::interpreter) fn eval_hypot_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + values.float(left.hypot(right)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs new file mode 100644 index 0000000000..aeb28395ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `intdiv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Division by zero and overflowing signed division remain runtime fatals. + +use super::super::super::*; + +eval_builtin! { + name: "intdiv", + area: Math, + params: [num1, num2], + direct: Intdiv, + values: Intdiv, +} + +/// Evaluates PHP `intdiv()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Applies PHP `intdiv()` to two already evaluated values. +pub(in crate::interpreter) fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log.rs b/crates/elephc-magician/src/interpreter/builtins/math/log.rs new file mode 100644 index 0000000000..87f56fa14e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Eval registry entry and implementation for `log`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The optional base defaults through registry metadata; direct calls still +//! preserve source-order argument evaluation. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "log", + area: Math, + params: [num, base = EvalBuiltinDefaultValue::Float(std::f64::consts::E)], + direct: Log, + values: Log, +} + +/// Evaluates PHP `log()` over one value and an optional base expression. +pub(in crate::interpreter) fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `log()` to already evaluated arguments. +pub(in crate::interpreter) fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log10.rs b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs new file mode 100644 index 0000000000..332965b76d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `log10`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "log10", + area: Math, + params: [num], + direct: Log10, + values: Log10, +} + +/// Evaluates PHP `log10()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_log10( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_log10_result(num, values) +} + +/// Applies PHP `log10()` to one already evaluated value. +pub(in crate::interpreter) fn eval_log10_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.log10()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log2.rs b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs new file mode 100644 index 0000000000..46bc9fce02 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `log2`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "log2", + area: Math, + params: [num], + direct: Log2, + values: Log2, +} + +/// Evaluates PHP `log2()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_log2( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_log2_result(num, values) +} + +/// Applies PHP `log2()` to one already evaluated value. +pub(in crate::interpreter) fn eval_log2_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.log2()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/max.rs b/crates/elephc-magician/src/interpreter/builtins/math/max.rs new file mode 100644 index 0000000000..e8bd71de20 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/max.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `max`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Variadic inputs are evaluated in PHP source order before runtime comparison. + +use super::super::super::*; + +eval_builtin! { + name: "max", + area: Math, + params: [value], + variadic: values, + direct: Max, + values: Max, +} + +/// Evaluates PHP `max()` over two or more eval expressions. +pub(in crate::interpreter) fn eval_builtin_max( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_max_result(&evaluated_args, values) +} + +/// Applies PHP `max()` to already evaluated values. +pub(in crate::interpreter) fn eval_max_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_min_max_selected(evaluated_args, EvalBinOp::Gt, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/min.rs b/crates/elephc-magician/src/interpreter/builtins/math/min.rs new file mode 100644 index 0000000000..32f689d885 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/min.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `min`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Variadic inputs are evaluated in PHP source order before runtime comparison. + +use super::super::super::*; + +eval_builtin! { + name: "min", + area: Math, + params: [value], + variadic: values, + direct: Min, + values: Min, +} + +/// Evaluates PHP `min()` over two or more eval expressions. +pub(in crate::interpreter) fn eval_builtin_min( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_result(&evaluated_args, values) +} + +/// Applies PHP `min()` to already evaluated values. +pub(in crate::interpreter) fn eval_min_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_min_max_selected(evaluated_args, EvalBinOp::Lt, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs new file mode 100644 index 0000000000..ac4de3582a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Per-builtin eval registry entries and implementations for numeric functions. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers. + +mod abs; +mod acos; +mod asin; +mod atan; +mod atan2; +mod ceil; +mod clamp; +mod cos; +mod cosh; +mod deg2rad; +mod exp; +mod fdiv; +mod floor; +mod fmod; +mod hypot; +mod intdiv; +mod log; +mod log10; +mod log2; +mod max; +mod min; +mod mt_rand; +mod pi; +mod pow; +mod rand; +mod random_int; +mod rad2deg; +mod round; +mod sin; +mod sinh; +mod sqrt; +mod tan; +mod tanh; + +pub(in crate::interpreter) use abs::*; +pub(in crate::interpreter) use acos::*; +pub(in crate::interpreter) use asin::*; +pub(in crate::interpreter) use atan::*; +pub(in crate::interpreter) use atan2::*; +pub(in crate::interpreter) use ceil::*; +pub(in crate::interpreter) use clamp::*; +pub(in crate::interpreter) use cos::*; +pub(in crate::interpreter) use cosh::*; +pub(in crate::interpreter) use deg2rad::*; +pub(in crate::interpreter) use exp::*; +pub(in crate::interpreter) use fdiv::*; +pub(in crate::interpreter) use floor::*; +pub(in crate::interpreter) use fmod::*; +pub(in crate::interpreter) use hypot::*; +pub(in crate::interpreter) use intdiv::*; +pub(in crate::interpreter) use log::*; +pub(in crate::interpreter) use log10::*; +pub(in crate::interpreter) use log2::*; +pub(in crate::interpreter) use max::*; +pub(in crate::interpreter) use min::*; +pub(in crate::interpreter) use mt_rand::*; +pub(in crate::interpreter) use pi::*; +pub(in crate::interpreter) use pow::*; +pub(in crate::interpreter) use rad2deg::*; +pub(in crate::interpreter) use rand::*; +pub(in crate::interpreter) use random_int::*; +pub(in crate::interpreter) use round::*; +pub(in crate::interpreter) use sin::*; +pub(in crate::interpreter) use sinh::*; +pub(in crate::interpreter) use sqrt::*; +pub(in crate::interpreter) use tan::*; +pub(in crate::interpreter) use tanh::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs new file mode 100644 index 0000000000..fbe37e9d5f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation for `mt_rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Eval mirrors the existing `rand()` range behavior for `mt_rand()`. + +use super::super::super::*; + +eval_builtin! { + name: "mt_rand", + area: Math, + params: [min, max], + direct: MtRand, + values: MtRand, +} + +/// Evaluates PHP `mt_rand()` over zero args or an inclusive range. +pub(in crate::interpreter) fn eval_builtin_mt_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_rand(args, context, scope, values) +} + +/// Dispatches by-value `mt_rand()` calls after argument binding. +pub(in crate::interpreter) fn eval_mt_rand_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_rand_values_result(evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pi.rs b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs new file mode 100644 index 0000000000..7d0461d321 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation for `pi`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `pi()` accepts no arguments and returns the platform `f64` PI constant. + +use super::super::super::*; + +eval_builtin! { + name: "pi", + area: Math, + params: [], + direct: Pi, + values: Pi, +} + +/// Evaluates PHP `pi()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_pi_result(values) +} + +/// Returns PHP `pi()` as an already evaluated builtin result. +pub(in crate::interpreter) fn eval_pi_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.float(std::f64::consts::PI) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pow.rs b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs new file mode 100644 index 0000000000..54c7857f12 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `pow`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "pow", + area: Math, + params: [num, exponent], + direct: Pow, + values: Pow, +} + +/// Evaluates PHP `pow()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_pow_result(left, right, values) +} + +/// Applies PHP `pow()` to two already evaluated values. +pub(in crate::interpreter) fn eval_pow_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.pow(left, right) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs new file mode 100644 index 0000000000..cce11e09ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `rad2deg`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "rad2deg", + area: Math, + params: [num], + direct: Rad2deg, + values: Rad2deg, +} + +/// Evaluates PHP `rad2deg()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_rad2deg( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_rad2deg_result(num, values) +} + +/// Applies PHP `rad2deg()` to one already evaluated value. +pub(in crate::interpreter) fn eval_rad2deg_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.to_degrees()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs new file mode 100644 index 0000000000..13703b59b1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Eval registry entry and implementation for `rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `rand()` accepts either no arguments or an inclusive min/max range. + +use super::super::super::*; + +eval_builtin! { + name: "rand", + area: Math, + params: [min, max], + direct: Rand, + values: Rand, +} + +/// Evaluates PHP `rand()` over zero args or an inclusive range. +pub(in crate::interpreter) fn eval_builtin_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_rand_result(None, None, values), + [min, max] => { + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_rand_result(Some(min), Some(max), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `rand()` calls after argument binding. +pub(in crate::interpreter) fn eval_rand_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_rand_result(None, None, values), + [min, max] => eval_rand_result(Some(*min), Some(*max), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns one non-cryptographic random integer using PHP's inclusive range rules. +pub(in crate::interpreter) fn eval_rand_result( + min: Option, + max: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (min, max) = match (min, max) { + (None, None) => (0, i64::from(i32::MAX)), + (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let low = min.min(max); + let high = min.max(max); + let width = (i128::from(high) - i128::from(low) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(low) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs new file mode 100644 index 0000000000..0bc4846bdf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `random_int`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Eval uses the same process-local pseudo-random source as the existing +//! interpreter implementation; invalid ranges are runtime fatals. + +use super::super::super::*; + +eval_builtin! { + name: "random_int", + area: Math, + params: [min, max], + direct: RandomInt, + values: RandomInt, +} + +/// Evaluates PHP `random_int()` over an inclusive integer range. +pub(in crate::interpreter) fn eval_builtin_random_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_random_int_result(min, max, values) +} + +/// Dispatches by-value `random_int()` calls after argument binding. +pub(in crate::interpreter) fn eval_random_int_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values) +} + +/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. +pub(in crate::interpreter) fn eval_random_int_result( + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let min = eval_int_value(min, values)?; + let max = eval_int_value(max, values)?; + if min > max { + return Err(EvalStatus::RuntimeFatal); + } + let width = (i128::from(max) - i128::from(min) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(min) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/round.rs b/crates/elephc-magician/src/interpreter/builtins/math/round.rs new file mode 100644 index 0000000000..c93d7e02f0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/round.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry and implementation for `round`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The optional precision defaults through registry metadata; direct calls +//! still evaluate arguments in source order. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "round", + area: Math, + params: [num, precision = EvalBuiltinDefaultValue::Int(0)], + direct: Round, + values: Round, +} + +/// Evaluates PHP `round()` over one value and an optional precision expression. +pub(in crate::interpreter) fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_round_result(num, None, values) + } + [num, precision] => { + let num = eval_expr(num, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + eval_round_result(num, Some(precision), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `round()` to already evaluated arguments. +pub(in crate::interpreter) fn eval_round_result( + num: RuntimeCellHandle, + precision: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + values.round(num, precision) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sin.rs b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs new file mode 100644 index 0000000000..7364a981ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `sin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "sin", + area: Math, + params: [num], + direct: Sin, + values: Sin, +} + +/// Evaluates PHP `sin()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sin_result(num, values) +} + +/// Applies PHP `sin()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sin_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.sin()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs new file mode 100644 index 0000000000..173a36fc3a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `sinh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "sinh", + area: Math, + params: [num], + direct: Sinh, + values: Sinh, +} + +/// Evaluates PHP `sinh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sinh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sinh_result(num, values) +} + +/// Applies PHP `sinh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sinh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.sinh()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs new file mode 100644 index 0000000000..8e9574321d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation for `sqrt`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "sqrt", + area: Math, + params: [num], + direct: Sqrt, + values: Sqrt, +} + +/// Evaluates PHP `sqrt()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sqrt_result(num, values) +} + +/// Applies PHP `sqrt()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sqrt_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.sqrt(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tan.rs b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs new file mode 100644 index 0000000000..143ad07098 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `tan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "tan", + area: Math, + params: [num], + direct: Tan, + values: Tan, +} + +/// Evaluates PHP `tan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_tan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_tan_result(num, values) +} + +/// Applies PHP `tan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_tan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.tan()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs new file mode 100644 index 0000000000..aaaf840110 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation for `tanh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; + +eval_builtin! { + name: "tanh", + area: Math, + params: [num], + direct: Tanh, + values: Tanh, +} + +/// Evaluates PHP `tanh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_tanh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_tanh_result(num, values) +} + +/// Applies PHP `tanh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_tanh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.tanh()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs new file mode 100644 index 0000000000..25e111e44b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Groups eval implementations for PHP-visible builtins and related helpers. +//! Submodules are organized by builtin domain while this module re-exports the +//! callable surface expected by the core interpreter. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` and positional builtin dispatch paths. +//! +//! Key details: +//! - Builtin modules are children of `interpreter`, so they can reuse core EvalIR +//! execution helpers without widening crate-level visibility. +//! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. + +#[macro_use] +mod macros; + +mod array; +mod class_metadata; +mod core; +mod filesystem; +mod formatting; +mod hooks; +mod json; +mod math; +mod network_env; +mod raw_memory; +mod random; +mod ref_targets; +mod regex; +mod registry; +mod scalars; +mod spec; +mod string; +mod symbols; +mod time; +mod types; + +pub(super) use array::*; +pub(super) use class_metadata::*; +pub(super) use core::*; +pub(super) use filesystem::*; +pub(super) use formatting::*; +pub(super) use json::*; +pub(super) use math::*; +pub(super) use network_env::*; +pub(super) use raw_memory::*; +pub(super) use random::*; +pub(super) use ref_targets::*; +pub(super) use regex::*; +pub(super) use registry::*; +pub(super) use scalars::*; +pub(super) use string::*; +pub(super) use symbols::*; +pub(super) use time::*; +pub(super) use types::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs new file mode 100644 index 0000000000..0f0dddcd5a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Eval registry entry and implementation for `exec` plus shared shell runner helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - `shell_exec`, `system`, and `passthru` call the runner owned by this file. + +use std::process::Command; + +use super::*; + +eval_builtin! { + name: "exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `exec($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_exec( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("exec", args, context, scope, values) +} + +/// Evaluates already materialized `exec()` command arguments. +pub(in crate::interpreter) fn eval_exec_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("exec", command, values) +} + +/// Evaluates one eval process-control builtin over a command expression. +pub(in crate::interpreter) fn eval_builtin_process_command( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [command] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let command = eval_expr(command, context, scope, values)?; + eval_process_command_result(name, command, values) +} + +/// Evaluates one already materialized process-control command argument. +pub(in crate::interpreter) fn eval_process_command_result( + name: &str, + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = eval_shell_command_string(command, values)?; + let output = eval_shell_command_output(&command); + match name { + "exec" | "shell_exec" => values.string_bytes_value(&output), + "system" => { + eval_echo_process_output(&output, values)?; + values.string("") + } + "passthru" => { + eval_echo_process_output(&output, values)?; + values.null() + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Converts a PHP command cell into the host shell string accepted by `Command`. +fn eval_shell_command_string( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = values.string_bytes(command)?; + Ok(String::from_utf8_lossy(&command).into_owned()) +} + +/// Executes a shell command and returns stdout bytes, mapping spawn failures to an empty string. +fn eval_shell_command_output(command: &str) -> Vec { + Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + .map(|output| output.stdout) + .unwrap_or_default() +} + +/// Echoes captured process output through the eval runtime value hooks. +fn eval_echo_process_output( + output: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if output.is_empty() { + return Ok(()); + } + let output = values.string_bytes_value(output)?; + values.echo(output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs new file mode 100644 index 0000000000..ca16c9ed39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `getenv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Unset variables return an empty string to match current eval semantics. + +use super::*; + +eval_builtin! { + name: "getenv", + area: NetworkEnv, + params: [name], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getenv($name)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +pub(in crate::interpreter) fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs new file mode 100644 index 0000000000..751d9f28d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostbyaddr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - libc resolver storage is copied before any subsequent resolver lookup can overwrite it. + +use super::*; + +eval_builtin! { + name: "gethostbyaddr", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +pub(in crate::interpreter) fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs new file mode 100644 index 0000000000..fbf0984dfd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Failed lookups return the original hostname input. + +use super::*; + +eval_builtin! { + name: "gethostbyname", + area: NetworkEnv, + params: [hostname], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +pub(in crate::interpreter) fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs new file mode 100644 index 0000000000..2b4a545e60 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The libc hostname buffer is stack-owned and copied into a PHP string. + +use super::*; + +eval_builtin! { + name: "gethostname", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostname()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +pub(in crate::interpreter) fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs new file mode 100644 index 0000000000..1a1982e1e9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Eval registry entry and implementation for `getprotobyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Lowercase C-string and protoent-name helpers are owned here for protocol lookups. + +use super::*; + +eval_builtin! { + name: "getprotobyname", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +pub(in crate::interpreter) fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +pub(in crate::interpreter) fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs new file mode 100644 index 0000000000..0b71fb38bc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `getprotobynumber`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Protocol-name extraction delegates to `getprotobyname`. + +use super::*; + +eval_builtin! { + name: "getprotobynumber", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +pub(in crate::interpreter) fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs new file mode 100644 index 0000000000..478e58b48d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Eval registry entry and implementation for `getservbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Service-name extraction is owned here and reused by `getservbyport`. + +use super::*; + +eval_builtin! { + name: "getservbyname", + area: NetworkEnv, + params: [service, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +pub(in crate::interpreter) fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + + +/// Copies a servent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs new file mode 100644 index 0000000000..b3adc46ae3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Eval registry entry and implementation for `getservbyport`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - C-string conversion and service-name extraction delegate to the owner builtin files. + +use super::*; + +eval_builtin! { + name: "getservbyport", + area: NetworkEnv, + params: [port, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +pub(in crate::interpreter) fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs new file mode 100644 index 0000000000..8c599d8b1a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `inet_ntop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - IPv4 formatting delegates to `long2ip` so byte rendering stays aligned. + +use super::*; + +eval_builtin! { + name: "inet_ntop", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +pub(in crate::interpreter) fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs new file mode 100644 index 0000000000..27b14c4a1d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `inet_pton`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - IPv4 parsing delegates to `ip2long` so malformed-address behavior stays aligned. + +use super::*; + +eval_builtin! { + name: "inet_pton", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +pub(in crate::interpreter) fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs new file mode 100644 index 0000000000..2fda828176 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Eval registry entry and implementation for `ip2long`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The dotted-quad parser is owned here and reused by `inet_pton`. + +use super::*; + +eval_builtin! { + name: "ip2long", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +pub(in crate::interpreter) fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + + +/// Parses exactly four decimal IPv4 octets separated by dots. +pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs new file mode 100644 index 0000000000..032145d3ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `long2ip`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The IPv4 formatter is owned here and reused by `inet_ntop`. + +use super::*; + +eval_builtin! { + name: "long2ip", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `long2ip($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +pub(in crate::interpreter) fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + + +/// Formats one packed IPv4 integer into dotted-quad text. +pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs new file mode 100644 index 0000000000..ccb3f26bb1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs @@ -0,0 +1,209 @@ +//! Purpose: +//! Orchestrates network lookup, IP conversion, environment, process, and +//! system-information eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Leaf builtin files own their registry declarations and builtin-specific wrappers. +//! - Shared helpers live in owner builtin files, such as `exec`, `ip2long`, +//! `long2ip`, `getprotobyname`, and `getservbyname`. + +use super::super::*; + +mod exec; +mod getenv; +mod gethostbyaddr; +mod gethostbyname; +mod gethostname; +mod getprotobyname; +mod getprotobynumber; +mod getservbyname; +mod getservbyport; +mod inet_ntop; +mod inet_pton; +mod ip2long; +mod long2ip; +mod passthru; +mod php_uname; +mod phpversion; +mod putenv; +mod shell_exec; +mod system; + +pub(in crate::interpreter) use exec::*; +pub(in crate::interpreter) use getenv::*; +pub(in crate::interpreter) use gethostbyaddr::*; +pub(in crate::interpreter) use gethostbyname::*; +pub(in crate::interpreter) use gethostname::*; +pub(in crate::interpreter) use getprotobyname::*; +pub(in crate::interpreter) use getprotobynumber::*; +pub(in crate::interpreter) use getservbyname::*; +pub(in crate::interpreter) use getservbyport::*; +pub(in crate::interpreter) use inet_ntop::*; +pub(in crate::interpreter) use inet_pton::*; +pub(in crate::interpreter) use ip2long::*; +pub(in crate::interpreter) use long2ip::*; +pub(in crate::interpreter) use passthru::*; +pub(in crate::interpreter) use php_uname::*; +pub(in crate::interpreter) use phpversion::*; +pub(in crate::interpreter) use putenv::*; +pub(in crate::interpreter) use shell_exec::*; +pub(in crate::interpreter) use system::*; + +/// Dispatches direct expression-level calls for network/env builtins. +pub(in crate::interpreter) fn eval_builtin_network_env_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "exec" => eval_builtin_exec(args, context, scope, values), + "shell_exec" => eval_builtin_shell_exec(args, context, scope, values), + "system" => eval_builtin_system(args, context, scope, values), + "passthru" => eval_builtin_passthru(args, context, scope, values), + "getenv" => eval_builtin_getenv(args, context, scope, values), + "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), + "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), + "gethostname" => eval_builtin_gethostname(args, values), + "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), + "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), + "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), + "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), + "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "long2ip" => eval_builtin_long2ip(args, context, scope, values), + "php_uname" => eval_builtin_php_uname(args, context, scope, values), + "phpversion" => eval_builtin_phpversion(args, values), + "putenv" => eval_builtin_putenv(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for network/env builtins. +pub(in crate::interpreter) fn eval_network_env_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values), + [mode] => eval_php_uname_result(Some(*mode), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values) + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values) + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values) + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values) + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values) + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values) + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values) + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values) + } + "exec" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_exec_result(*command, values) + } + "shell_exec" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_shell_exec_result(*command, values) + } + "system" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_system_result(*command, values) + } + "passthru" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_passthru_result(*command, values) + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values) + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values) + } + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values) + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) + } + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values) + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs new file mode 100644 index 0000000000..62be490593 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `passthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "passthru", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `passthru($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_passthru( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("passthru", args, context, scope, values) +} + +/// Evaluates already materialized `passthru()` command arguments. +pub(in crate::interpreter) fn eval_passthru_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("passthru", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs new file mode 100644 index 0000000000..27bc934606 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Eval registry entry and implementation for `php_uname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - libc uname fields are copied into PHP strings before formatting the requested mode. + +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "php_uname", + area: NetworkEnv, + params: [mode = EvalBuiltinDefaultValue::String("a")], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. +pub(in crate::interpreter) fn eval_builtin_php_uname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_php_uname_result(None, values), + [mode] => { + let mode = eval_expr(mode, context, scope, values)?; + eval_php_uname_result(Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads the local uname fields and formats the PHP `php_uname()` mode result. +pub(in crate::interpreter) fn eval_php_uname_result( + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => { + let bytes = values.string_bytes(mode)?; + let [mode] = bytes.as_slice() else { + return Err(EvalStatus::RuntimeFatal); + }; + *mode + } + None => b'a', + }; + + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes all uname fields into the stack-owned utsname buffer. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return values.string(""); + } + let utsname = unsafe { + // `uname` succeeded, so libc initialized the full `utsname` structure. + utsname.assume_init() + }; + let sysname = eval_uname_field_bytes(&utsname.sysname); + let nodename = eval_uname_field_bytes(&utsname.nodename); + let release = eval_uname_field_bytes(&utsname.release); + let version = eval_uname_field_bytes(&utsname.version); + let machine = eval_uname_field_bytes(&utsname.machine); + + match mode { + b'a' => { + let mut output = Vec::new(); + for field in [&sysname, &nodename, &release, &version, &machine] { + if !output.is_empty() { + output.push(b' '); + } + output.extend_from_slice(field); + } + values.string_bytes_value(&output) + } + b's' => values.string_bytes_value(&sysname), + b'n' => values.string_bytes_value(&nodename), + b'r' => values.string_bytes_value(&release), + b'v' => values.string_bytes_value(&version), + b'm' => values.string_bytes_value(&machine), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. +pub(in crate::interpreter) fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs new file mode 100644 index 0000000000..8233a9447d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Eval registry entry and implementation for `phpversion`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The compiler package version is read from the workspace manifest. + +use super::*; + +eval_builtin! { + name: "phpversion", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `phpversion()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +pub(in crate::interpreter) fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs new file mode 100644 index 0000000000..0ad3b601d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Eval registry entry and implementation for `putenv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Assignments mutate the host process environment for the current eval process. + +use super::*; + +eval_builtin! { + name: "putenv", + area: NetworkEnv, + params: [assignment], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `putenv($assignment)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_putenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [assignment] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let assignment = eval_expr(assignment, context, scope, values)?; + eval_putenv_result(assignment, values) +} + +/// Applies one `putenv()` assignment to the host environment. +pub(in crate::interpreter) fn eval_putenv_result( + assignment: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let assignment = values.string_bytes(assignment)?; + if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { + let name = String::from_utf8_lossy(&assignment[..separator]); + let value = String::from_utf8_lossy(&assignment[separator + 1..]); + std::env::set_var(name.as_ref(), value.as_ref()); + } else { + let name = String::from_utf8_lossy(&assignment); + std::env::remove_var(name.as_ref()); + } + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs new file mode 100644 index 0000000000..35e898706c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `shell_exec`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "shell_exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `shell_exec($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_shell_exec( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("shell_exec", args, context, scope, values) +} + +/// Evaluates already materialized `shell_exec()` command arguments. +pub(in crate::interpreter) fn eval_shell_exec_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("shell_exec", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs new file mode 100644 index 0000000000..aa85fbe25b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `system`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "system", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `system($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_system( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("system", args, context, scope, values) +} + +/// Evaluates already materialized `system()` command arguments. +pub(in crate::interpreter) fn eval_system_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("system", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/random.rs b/crates/elephc-magician/src/interpreter/builtins/random.rs new file mode 100644 index 0000000000..ef646be040 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/random.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Shared pseudo-random word source for eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::math` random builtins. +//! - `crate::interpreter::builtins::array` randomizing builtins. +//! +//! Key details: +//! - This is eval-local, process-local, and non-cryptographic; PHP-visible +//! builtin owners decide range and key semantics. + +use super::super::*; + +/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. +pub(in crate::interpreter) fn eval_random_u128() -> u128 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); + let pid = u128::from(std::process::id()); + let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs new file mode 100644 index 0000000000..cd27d98291 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_free`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so a local buffer variable can be nulled. + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_free", + area: RawMemory, + params: [buffer], + direct: BufferFree, + values: BufferFree, +} + +/// Evaluates PHP `buffer_free()` and nulls direct local variables when possible. +pub(in crate::interpreter) fn eval_builtin_buffer_free( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let [EvalExpr::LoadVar(variable)] = args { + return eval_buffer_free_direct_variable(variable, context, scope, values); + } + let [buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_buffer_free_result(buffer, values) +} + +/// Dispatches by-value `buffer_free()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_free_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_free_result(*buffer, values) +} + +/// Frees an AOT-shaped buffer header and returns PHP null. +fn eval_buffer_free_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_buffer_free_address(buffer, values)?; + values.null() +} + +/// Frees a local buffer variable and replaces the source variable with null. +fn eval_buffer_free_direct_variable( + variable: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(&EvalExpr::LoadVar(variable.to_string()), context, scope, values)?; + eval_buffer_free_address(value, values)?; + let null = values.null()?; + for replaced in scope.set_respecting_references( + variable.to_string(), + null, + ScopeCellOwnership::Owned, + ) { + values.release(replaced)?; + } + values.null() +} + +/// Frees the raw allocation addressed by an AOT-shaped buffer header pointer. +fn eval_buffer_free_address( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let address = super::ptr::eval_non_null_pointer(buffer, values)?; + unsafe { + libc::free(address.cast::()); + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs new file mode 100644 index 0000000000..285880ec4d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_len`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reads the logical element count from an AOT-shaped buffer header. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_len", + area: RawMemory, + params: [buffer], + direct: BufferLen, + values: BufferLen, +} + +/// Evaluates PHP `buffer_len()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_buffer_len( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_buffer_len_result(buffer, values) +} + +/// Dispatches by-value `buffer_len()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_len_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_len_result(*buffer, values) +} + +/// Reads the logical element count from an AOT-shaped buffer header. +fn eval_buffer_len_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let header = super::ptr::eval_non_null_pointer(buffer, values)?.cast::(); + let length = unsafe { ptr::read(header) }; + values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs new file mode 100644 index 0000000000..41efa687cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `buffer_new()` returns the same header pointer shape used by AOT buffers: +//! length word, stride word, then zeroed payload. + +use std::mem; +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_new", + area: RawMemory, + params: [length], + direct: BufferNew, + values: BufferNew, +} + +const BUFFER_HEADER_WORDS: usize = 2; +const BUFFER_DEFAULT_STRIDE: usize = 8; + +/// Evaluates PHP `buffer_new()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_buffer_new( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let length = eval_expr(length, context, scope, values)?; + eval_buffer_new_result(length, values) +} + +/// Dispatches by-value `buffer_new()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_new_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_new_result(*length, values) +} + +/// Allocates a zero-filled AOT-shaped buffer and returns its header address. +fn eval_buffer_new_result( + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let header_bytes = BUFFER_HEADER_WORDS + .checked_mul(mem::size_of::()) + .ok_or(EvalStatus::RuntimeFatal)?; + let payload_bytes = length + .checked_mul(BUFFER_DEFAULT_STRIDE) + .ok_or(EvalStatus::RuntimeFatal)?; + let total_bytes = header_bytes + .checked_add(payload_bytes) + .ok_or(EvalStatus::RuntimeFatal)?; + let allocation = unsafe { libc::calloc(total_bytes.max(1), 1) }; + if allocation.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { + let header = allocation.cast::(); + ptr::write(header, length); + ptr::write(header.add(1), BUFFER_DEFAULT_STRIDE); + } + super::ptr::eval_address_value(allocation as usize, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs new file mode 100644 index 0000000000..4a2401ad23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Groups eval raw pointer and buffer extension builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers and implementation code. +//! - Helper reuse stays between builtin files with `ptr` owning raw address +//! conversions, `ptr_get` owning read widths, and `ptr_set` owning write widths. + +mod buffer_free; +mod buffer_len; +mod buffer_new; +mod ptr; +mod ptr_get; +mod ptr_is_null; +mod ptr_null; +mod ptr_offset; +mod ptr_read16; +mod ptr_read32; +mod ptr_read8; +mod ptr_read_string; +mod ptr_set; +mod ptr_sizeof; +mod ptr_write16; +mod ptr_write32; +mod ptr_write8; +mod ptr_write_string; + +pub(in crate::interpreter) use buffer_free::*; +pub(in crate::interpreter) use buffer_len::*; +pub(in crate::interpreter) use buffer_new::*; +pub(in crate::interpreter) use ptr::*; +pub(in crate::interpreter) use ptr_get::*; +pub(in crate::interpreter) use ptr_is_null::*; +pub(in crate::interpreter) use ptr_null::*; +pub(in crate::interpreter) use ptr_offset::*; +pub(in crate::interpreter) use ptr_read16::*; +pub(in crate::interpreter) use ptr_read32::*; +pub(in crate::interpreter) use ptr_read8::*; +pub(in crate::interpreter) use ptr_read_string::*; +pub(in crate::interpreter) use ptr_set::*; +pub(in crate::interpreter) use ptr_sizeof::*; +pub(in crate::interpreter) use ptr_write16::*; +pub(in crate::interpreter) use ptr_write32::*; +pub(in crate::interpreter) use ptr_write8::*; +pub(in crate::interpreter) use ptr_write_string::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs new file mode 100644 index 0000000000..bf327c5ef0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and raw pointer conversion helpers for `ptr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` and sibling raw-memory builtins. +//! +//! Key details: +//! - Eval keeps `ptr(...)` unsupported because by-value cells do not expose raw +//! lvalue storage addresses safely. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr", + area: RawMemory, + params: [value], + direct: Ptr, + values: Ptr, +} + +/// Evaluates PHP `ptr()` and rejects unsupported eval lvalue-address extraction. +pub(in crate::interpreter) fn eval_builtin_ptr( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + _values: &mut impl RuntimeValueOps, +) -> Result { + let [_value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + Err(EvalStatus::UnsupportedConstruct) +} + +/// Dispatches by-value `ptr()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_values_result( + evaluated_args: &[RuntimeCellHandle], +) -> Result { + let [_value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + Err(EvalStatus::UnsupportedConstruct) +} + +/// Converts a runtime cell to a raw pointer address encoded as a PHP integer. +pub(super) fn eval_pointer_address( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_int_value(value, values)?; + usize::try_from(address).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts a runtime cell to a non-null raw pointer. +pub(super) fn eval_non_null_pointer( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<*mut u8, EvalStatus> { + let address = eval_pointer_address(value, values)?; + if address == 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(address as *mut u8) +} + +/// Boxes a raw pointer address as a PHP integer cell. +pub(super) fn eval_address_value( + address: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(i64::try_from(address).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs new file mode 100644 index 0000000000..18799f924f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Owns shared pointer read-width handling for read variants. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_get", + area: RawMemory, + params: [pointer], + direct: PtrGet, + values: PtrGet, +} + +/// Evaluates PHP `ptr_get()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_get( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_get_result(pointer, values) +} + +/// Dispatches by-value `ptr_get()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_get_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_get_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_get()`. +fn eval_ptr_get_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_pointer_read_result(pointer, PointerReadWidth::Word64, values) +} + +/// Reads one unsigned or machine-word value from raw memory. +pub(super) fn eval_pointer_read_result( + pointer: RuntimeCellHandle, + width: PointerReadWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let value = unsafe { + match width { + PointerReadWidth::Byte => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Half => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word32 => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word64 => { + let word = ptr::read_unaligned(address.cast::()); + i64::from_ne_bytes(word.to_ne_bytes()) + } + } + }; + values.int(value) +} + +/// Widths supported by pointer read helpers. +pub(super) enum PointerReadWidth { + Byte, + Half, + Word32, + Word64, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs new file mode 100644 index 0000000000..06ac1df27e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_is_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Checks the integer raw-address representation against zero. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_is_null", + area: RawMemory, + params: [pointer], + direct: PtrIsNull, + values: PtrIsNull, +} + +/// Evaluates PHP `ptr_is_null()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_is_null( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_is_null_result(pointer, values) +} + +/// Dispatches by-value `ptr_is_null()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_is_null_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_is_null_result(*pointer, values) +} + +/// Returns whether one raw pointer address is null. +fn eval_ptr_is_null_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_pointer_address(pointer, values)?; + values.bool_value(address == 0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs new file mode 100644 index 0000000000..960ee967c5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Null raw pointers are represented as the integer address zero. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_null", + area: RawMemory, + params: [], + direct: PtrNull, + values: PtrNull, +} + +/// Evaluates PHP `ptr_null()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_ptr_null( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_ptr_null_result(values) +} + +/// Dispatches by-value `ptr_null()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_null_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_ptr_null_result(values) +} + +/// Returns the raw null pointer address. +fn eval_ptr_null_result(values: &mut impl RuntimeValueOps) -> Result { + values.int(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs new file mode 100644 index 0000000000..6fbb9bc453 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_offset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Performs checked signed byte-offset arithmetic on raw pointer addresses. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_offset", + area: RawMemory, + params: [pointer, offset], + direct: PtrOffset, + values: PtrOffset, +} + +/// Evaluates PHP `ptr_offset()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_offset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, offset] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_ptr_offset_result(pointer, offset, values) +} + +/// Dispatches by-value `ptr_offset()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_offset_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, offset] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_offset_result(*pointer, *offset, values) +} + +/// Computes a derived raw pointer address by adding a signed byte offset. +fn eval_ptr_offset_result( + pointer: RuntimeCellHandle, + offset: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_pointer_address(pointer, values)?; + let offset = eval_int_value(offset, values)?; + let address = if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)? + } else { + let offset = usize::try_from(offset.unsigned_abs()).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_sub(offset).ok_or(EvalStatus::RuntimeFatal)? + }; + super::ptr::eval_address_value(address, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs new file mode 100644 index 0000000000..c643ec81b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for two-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read16", + area: RawMemory, + params: [pointer], + direct: PtrRead16, + values: PtrRead16, +} + +/// Evaluates PHP `ptr_read16()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read16( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read16_result(pointer, values) +} + +/// Dispatches by-value `ptr_read16()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read16_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read16_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read16()`. +fn eval_ptr_read16_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Half, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs new file mode 100644 index 0000000000..f9379835a8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for four-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read32", + area: RawMemory, + params: [pointer], + direct: PtrRead32, + values: PtrRead32, +} + +/// Evaluates PHP `ptr_read32()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read32_result(pointer, values) +} + +/// Dispatches by-value `ptr_read32()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read32_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read32_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read32()`. +fn eval_ptr_read32_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Word32, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs new file mode 100644 index 0000000000..05b9c508c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for one-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read8", + area: RawMemory, + params: [pointer], + direct: PtrRead8, + values: PtrRead8, +} + +/// Evaluates PHP `ptr_read8()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read8( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read8_result(pointer, values) +} + +/// Dispatches by-value `ptr_read8()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read8_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read8_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read8()`. +fn eval_ptr_read8_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Byte, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs new file mode 100644 index 0000000000..ef95f5c7a1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Copies raw memory bytes into a PHP byte string. + +use std::slice; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read_string", + area: RawMemory, + params: [pointer, length], + direct: PtrReadString, + values: PtrReadString, +} + +/// Evaluates PHP `ptr_read_string()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_read_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_ptr_read_string_result(pointer, length, values) +} + +/// Dispatches by-value `ptr_read_string()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read_string_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read_string_result(*pointer, *length, values) +} + +/// Copies raw memory bytes into a PHP byte string. +fn eval_ptr_read_string_result( + pointer: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = unsafe { slice::from_raw_parts(address.cast::(), length) }; + values.string_bytes_value(bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs new file mode 100644 index 0000000000..58abe2d814 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Owns shared pointer write-width handling for write variants. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_set", + area: RawMemory, + params: [pointer, value], + direct: PtrSet, + values: PtrSet, +} + +/// Evaluates PHP `ptr_set()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_set( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_set_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_set()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_set_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_set_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_set()`. +fn eval_ptr_set_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_pointer_write_result(pointer, value, PointerWriteWidth::Word64, values) +} + +/// Writes one integer payload to raw memory and returns PHP null. +pub(super) fn eval_pointer_write_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + width: PointerWriteWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let value = eval_int_value(value, values)?; + unsafe { + match width { + PointerWriteWidth::Byte => ptr::write_unaligned(address.cast::(), value as u8), + PointerWriteWidth::Half => ptr::write_unaligned(address.cast::(), value as u16), + PointerWriteWidth::Word32 => ptr::write_unaligned(address.cast::(), value as u32), + PointerWriteWidth::Word64 => { + ptr::write_unaligned( + address.cast::(), + u64::from_ne_bytes(value.to_ne_bytes()), + ) + } + } + } + values.null() +} + +/// Widths supported by pointer write helpers. +pub(super) enum PointerWriteWidth { + Byte, + Half, + Word32, + Word64, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs new file mode 100644 index 0000000000..e81077f482 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_sizeof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Computes the checked byte size for scalar pointer targets and boxed classes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_sizeof", + area: RawMemory, + params: [r#type], + direct: PtrSizeof, + values: PtrSizeof, +} + +/// Evaluates PHP `ptr_sizeof()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_sizeof( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [type_name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let type_name = eval_expr(type_name, context, scope, values)?; + eval_ptr_sizeof_result(type_name, context, values) +} + +/// Dispatches by-value `ptr_sizeof()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_sizeof_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_sizeof_result(*type_name, context, values) +} + +/// Computes the checked byte size for a low-level type name. +fn eval_ptr_sizeof_result( + type_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(type_name)?; + let type_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let size = eval_pointer_target_size(type_name.trim_start_matches('\\'), context) + .ok_or(EvalStatus::RuntimeFatal)?; + values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the eval-side byte size for one low-level pointer target name. +fn eval_pointer_target_size(type_name: &str, context: &ElephcEvalContext) -> Option { + match type_name.to_ascii_lowercase().as_str() { + "int" | "integer" => Some(8), + "float" | "double" | "real" => Some(8), + "bool" | "boolean" => Some(8), + "string" => Some(16), + "ptr" | "pointer" => Some(8), + _ => context.class(type_name).map(eval_boxed_class_size), + } +} + +/// Returns the boxed object storage size used by AOT class metadata. +fn eval_boxed_class_size(class: &EvalClass) -> usize { + let instance_properties = class + .properties() + .iter() + .filter(|property| !property.is_static()) + .count(); + 8 + instance_properties * 16 +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs new file mode 100644 index 0000000000..d3c90d50ab --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for two-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write16", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite16, + values: PtrWrite16, +} + +/// Evaluates PHP `ptr_write16()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write16( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write16_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write16()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write16_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write16_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write16()`. +fn eval_ptr_write16_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Half, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs new file mode 100644 index 0000000000..83d2ce68eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for four-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write32", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite32, + values: PtrWrite32, +} + +/// Evaluates PHP `ptr_write32()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write32_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write32()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write32_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write32_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write32()`. +fn eval_ptr_write32_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Word32, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs new file mode 100644 index 0000000000..b5092e175e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for one-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write8", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite8, + values: PtrWrite8, +} + +/// Evaluates PHP `ptr_write8()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write8( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write8_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write8()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write8_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write8_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write8()`. +fn eval_ptr_write8_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Byte, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs new file mode 100644 index 0000000000..caac4d7433 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Copies PHP string bytes into raw memory and returns the byte count written. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write_string", + area: RawMemory, + params: [pointer, string], + direct: PtrWriteString, + values: PtrWriteString, +} + +/// Evaluates PHP `ptr_write_string()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_ptr_write_string_result(pointer, string, values) +} + +/// Dispatches by-value `ptr_write_string()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write_string_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write_string_result(*pointer, *string, values) +} + +/// Copies PHP string bytes into raw memory and returns the byte count written. +fn eval_ptr_write_string_result( + pointer: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let bytes = values.string_bytes(string)?; + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), address.cast::(), bytes.len()); + } + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs b/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs new file mode 100644 index 0000000000..14c3e1fe99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Shared caller-lvalue writeback helpers for direct by-reference eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` modules that implement PHP builtins with +//! direct by-reference output parameters. +//! +//! Key details: +//! - Variable writes can request a specific scope-cell ownership, while object, +//! static-property, and array-element targets reuse method by-reference +//! writeback semantics. + +use super::super::*; + +/// Writes a direct by-reference builtin result back to the captured caller lvalue. +pub(in crate::interpreter) fn eval_write_direct_ref_target( + target: &EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + variable_ownership: Option, +) -> Result<(), EvalStatus> { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + let ownership = variable_ownership.unwrap_or_else(|| { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.flags().ownership) + .unwrap_or(ScopeCellOwnership::Owned) + }); + for replaced in set_scope_cell(context, scope, name.clone(), value, ownership)? { + values.release(replaced)?; + } + Ok(()) + } + _ => write_back_method_ref_target(target, value, context, values), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/captures.rs b/crates/elephc-magician/src/interpreter/builtins/regex/captures.rs new file mode 100644 index 0000000000..0cf6ffb672 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/captures.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Converts Rust regex captures into PHP-compatible eval arrays and values. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` match and replacement modules. +//! +//! Key details: +//! - Optional offset capture uses PHP's `[string, byte_offset]` representation and +//! unmatched captures follow `PREG_UNMATCHED_AS_NULL`. + +use super::super::super::*; + +/// Builds PHP's indexed `$matches` capture array for one regex result. +pub(in crate::interpreter) fn eval_preg_capture_array( + subject: &[u8], + captures: Option<&Captures<'_>>, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = captures.map_or(0, |captures| { + eval_preg_visible_capture_len(captures, unmatched_as_null) + }); + let mut result = values.array_new(len)?; + if let Some(captures) = captures { + for index in 0..len { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + captures, + index, + offset_capture, + unmatched_as_null, + values, + )?; + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Returns the capture count PHP should expose, dropping trailing unmatched groups. +pub(in crate::interpreter) fn eval_preg_visible_capture_len( + captures: &Captures<'_>, + unmatched_as_null: bool, +) -> usize { + if unmatched_as_null { + return captures.len(); + } + let mut len = captures.len(); + while len > 1 && captures.get(len - 1).is_none() { + len -= 1; + } + len +} + +/// Returns one captured byte range from the original subject. +pub(in crate::interpreter) fn eval_preg_capture_bytes<'a>( + subject: &'a [u8], + captures: &Captures<'_>, + index: usize, +) -> Option<&'a [u8]> { + captures + .get(index) + .map(|matched| &subject[matched.start()..matched.end()]) +} + +/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_capture_value( + subject: &[u8], + captures: &Captures<'_>, + index: usize, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let matched = captures.get(index); + let value = if matched.is_none() && unmatched_as_null { + values.null()? + } else { + let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { + &subject[matched.start()..matched.end()] + }); + values.string_bytes_value(bytes)? + }; + if !offset_capture { + return Ok(value); + } + + let offset = matched.map_or(Ok(-1_i64), |matched| { + i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) + })?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs b/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs new file mode 100644 index 0000000000..334c69d04b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs @@ -0,0 +1,254 @@ +//! Purpose: +//! PCRE2 POSIX-wrapper regex engine used by eval `preg_*` builtins. +//! Provides the small capture API the eval regex modules need while sharing +//! the same native regex family as the AOT runtime path. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::pattern`. +//! - `crate::interpreter::builtins::regex` match, replace, and split helpers. +//! +//! Key details: +//! - Subject and pattern bytes are passed through PCRE2's POSIX wrapper as C strings. +//! - Match offsets are byte offsets into the original subject, matching PHP capture arrays. + +use std::ffi::CString; +use std::marker::PhantomData; + +use libc::{c_char, c_int, c_void, size_t}; + +use super::super::super::EvalStatus; + +const REG_ICASE: c_int = 0x0001; +const REG_NEWLINE: c_int = 0x0002; +const REG_DOTALL: c_int = 0x0010; +const REG_STARTEND: c_int = 0x0080; +const REG_UNGREEDY: c_int = 0x0200; +const REG_UCP: c_int = 0x0400; +const REG_UTF: c_int = 0x0040; +const REG_NOMATCH: c_int = 17; + +/// PCRE2 POSIX `regex_t` layout for the supported PCRE2 wrapper ABI. +#[repr(C)] +struct Pcre2Regex { + re_pcre2_code: *mut c_void, + re_match_data: *mut c_void, + re_endp: *const c_char, + re_nsub: size_t, + re_erroffset: size_t, + re_cflags: c_int, +} + +/// PCRE2 POSIX `regmatch_t` capture offset pair. +#[repr(C)] +#[derive(Clone, Copy)] +struct Pcre2Regmatch { + rm_so: c_int, + rm_eo: c_int, +} + +unsafe extern "C" { + /// Compiles a PCRE2 pattern through the POSIX wrapper. + fn pcre2_regcomp(regex: *mut Pcre2Regex, pattern: *const c_char, flags: c_int) -> c_int; + + /// Executes a compiled PCRE2 regex and fills capture offsets. + fn pcre2_regexec( + regex: *const Pcre2Regex, + subject: *const c_char, + nmatch: size_t, + matches: *mut Pcre2Regmatch, + flags: c_int, + ) -> c_int; + + /// Releases resources owned by a compiled PCRE2 regex. + fn pcre2_regfree(regex: *mut Pcre2Regex); +} + +/// Supported PHP regex modifiers after delimiter stripping. +#[derive(Default)] +pub(in crate::interpreter) struct EvalPregModifiers { + pub(in crate::interpreter) case_insensitive: bool, + pub(in crate::interpreter) multi_line: bool, + pub(in crate::interpreter) dot_matches_new_line: bool, + pub(in crate::interpreter) swap_greed: bool, + pub(in crate::interpreter) unicode: bool, +} + +/// A compiled PCRE2 regex plus POSIX wrapper metadata. +pub(in crate::interpreter) struct Regex { + raw: Pcre2Regex, +} + +impl Regex { + /// Compiles a delimiter-stripped pattern with PHP regex modifiers. + pub(in crate::interpreter) fn compile( + body: &[u8], + modifiers: EvalPregModifiers, + ) -> Result { + let pattern = CString::new(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut raw = Pcre2Regex { + re_pcre2_code: std::ptr::null_mut(), + re_match_data: std::ptr::null_mut(), + re_endp: std::ptr::null(), + re_nsub: 0, + re_erroffset: 0, + re_cflags: 0, + }; + let status = unsafe { pcre2_regcomp(&mut raw, pattern.as_ptr(), modifiers.flags()) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Self { raw }) + } + + /// Returns the number of capture slots including the full match at index 0. + pub(in crate::interpreter) fn captures_len(&self) -> usize { + self.raw.re_nsub.saturating_add(1) + } + + /// Returns whether this regex matches the subject. + pub(in crate::interpreter) fn is_match(&self, subject: &[u8]) -> bool { + self.captures(subject).is_some() + } + + /// Returns the first capture set for this regex and subject. + pub(in crate::interpreter) fn captures<'a>(&self, subject: &'a [u8]) -> Option> { + self.exec_at(subject, 0) + } + + /// Returns every non-overlapping capture set for this regex and subject. + pub(in crate::interpreter) fn captures_iter<'a>( + &self, + subject: &'a [u8], + ) -> std::vec::IntoIter> { + let mut captures = Vec::new(); + let mut cursor = 0; + while cursor <= subject.len() { + let Some(next) = self.exec_at(subject, cursor) else { + break; + }; + let Some(full_match) = next.get(0) else { + break; + }; + let end = full_match.end(); + let start = full_match.start(); + captures.push(next); + if end > cursor { + cursor = end; + } else if start < subject.len() { + cursor = start + 1; + } else { + break; + } + } + captures.into_iter() + } + + /// Executes this regex from a byte offset, returning capture offsets on match. + fn exec_at<'a>(&self, subject: &'a [u8], start: usize) -> Option> { + let subject_c = CString::new(subject).ok()?; + let mut matches = vec![Pcre2Regmatch::unmatched(); self.captures_len().max(1)]; + matches[0].rm_so = c_int::try_from(start).ok()?; + matches[0].rm_eo = c_int::try_from(subject.len()).ok()?; + let status = unsafe { + pcre2_regexec( + &self.raw, + subject_c.as_ptr(), + matches.len(), + matches.as_mut_ptr(), + REG_STARTEND, + ) + }; + if status == REG_NOMATCH || status != 0 { + return None; + } + Some(Captures { + matches: matches + .into_iter() + .map(|matched| matched.to_offsets()) + .collect(), + _subject: PhantomData, + }) + } +} + +impl Drop for Regex { + /// Releases the compiled PCRE2 regex when the wrapper is dropped. + fn drop(&mut self) { + unsafe { pcre2_regfree(&mut self.raw) }; + } +} + +impl EvalPregModifiers { + /// Converts parsed PHP modifiers into PCRE2 POSIX compile flags. + fn flags(&self) -> c_int { + let mut flags = 0; + if self.case_insensitive { + flags |= REG_ICASE; + } + if self.multi_line { + flags |= REG_NEWLINE; + } + if self.dot_matches_new_line { + flags |= REG_DOTALL; + } + if self.swap_greed { + flags |= REG_UNGREEDY; + } + if self.unicode { + flags |= REG_UTF | REG_UCP; + } + flags + } +} + +impl Pcre2Regmatch { + /// Returns an unmatched capture sentinel. + fn unmatched() -> Self { + Self { rm_so: -1, rm_eo: -1 } + } + + /// Converts a PCRE2 offset pair to an optional Rust byte range. + fn to_offsets(self) -> Option<(usize, usize)> { + let start = usize::try_from(self.rm_so).ok()?; + let end = usize::try_from(self.rm_eo).ok()?; + Some((start, end)) + } +} + +/// One regex match span. +#[derive(Clone, Copy)] +pub(in crate::interpreter) struct Match { + start: usize, + end: usize, +} + +impl Match { + /// Returns the match start byte offset. + pub(in crate::interpreter) fn start(&self) -> usize { + self.start + } + + /// Returns the match end byte offset. + pub(in crate::interpreter) fn end(&self) -> usize { + self.end + } +} + +/// Capture offsets for one regex match. +pub(in crate::interpreter) struct Captures<'a> { + matches: Vec>, + _subject: PhantomData<&'a [u8]>, +} + +impl Captures<'_> { + /// Returns the number of capture slots including the full match. + pub(in crate::interpreter) fn len(&self) -> usize { + self.matches.len() + } + + /// Returns the match span for one capture slot. + pub(in crate::interpreter) fn get(&self, index: usize) -> Option { + let (start, end) = self.matches.get(index).copied().flatten()?; + Some(Match { start, end }) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs new file mode 100644 index 0000000000..6dff7dd1ef --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Eval registry entry and implementation for `mb_ereg_match`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` direct and by-value dispatch. +//! +//! Key details: +//! - `mb_ereg_match($pattern, $string, $options = null)` is a start-anchored match: +//! the pattern is a raw mbregex body (no preg delimiters), and a successful match +//! counts only when it begins at byte offset 0 — mirroring the AOT runtime helper, +//! which enforces `rm_so == 0` on PCRE2's leftmost match. +//! - The `$options` string maps `i` to case-insensitive compilation; other option +//! bytes are accepted without additional runtime effect, matching the AOT helper. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use super::*; + +eval_builtin! { + name: "mb_ereg_match", + area: Regex, + params: [pattern, subject, options = EvalBuiltinDefaultValue::Null], + direct: MbEregMatch, + values: MbEregMatch, +} + +/// Evaluates PHP `mb_ereg_match()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_mb_ereg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_mb_ereg_match_result(pattern, subject, None, values) + } + [pattern, subject, options] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let options = eval_expr(options, context, scope, values)?; + eval_mb_ereg_match_result(pattern, subject, Some(options), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `mb_ereg_match()` calls after argument binding. +pub(in crate::interpreter) fn eval_mb_ereg_match_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_mb_ereg_match_result(*pattern, *subject, None, values), + [pattern, subject, options] => { + eval_mb_ereg_match_result(*pattern, *subject, Some(*options), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether the raw mbregex pattern matches the subject start. +pub(in crate::interpreter) fn eval_mb_ereg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let modifiers = eval_mb_ereg_match_modifiers(options, values)?; + let pattern = values.string_bytes(pattern)?; + let regex = Regex::compile(&pattern, modifiers)?; + let subject = values.string_bytes(subject)?; + let matched = regex + .captures(&subject) + .and_then(|captures| captures.get(0)) + .is_some_and(|full_match| full_match.start() == 0); + values.bool_value(matched) +} + +/// Translates the optional `mb_ereg_match()` options string into compile modifiers. +fn eval_mb_ereg_match_modifiers( + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(options) = options else { + return Ok(EvalPregModifiers::default()); + }; + if values.is_null(options)? { + return Ok(EvalPregModifiers::default()); + } + let options = values.string_bytes(options)?; + Ok(EvalPregModifiers { + case_insensitive: options.contains(&b'i'), + ..EvalPregModifiers::default() + }) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs new file mode 100644 index 0000000000..5abd4fb98a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Groups PCRE-style preg eval builtins by entrypoint and shared helper domain. +//! Submodules keep `preg_match`, `preg_replace`, and `preg_split` behavior +//! separated while this module re-exports the interpreter-visible surface. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +mod captures; +mod engine; +mod mb_ereg_match; +mod preg_match; +mod preg_match_all; +mod pattern; +mod preg_replace; +mod preg_replace_callback; +mod replacement; +mod preg_split; +mod split_helpers; +mod targets; + +pub(in crate::interpreter) use captures::*; +pub(in crate::interpreter) use engine::*; +pub(in crate::interpreter) use mb_ereg_match::*; +pub(in crate::interpreter) use preg_match::*; +pub(in crate::interpreter) use preg_match_all::*; +pub(in crate::interpreter) use pattern::*; +pub(in crate::interpreter) use preg_replace::*; +pub(in crate::interpreter) use preg_replace_callback::*; +pub(in crate::interpreter) use replacement::*; +pub(in crate::interpreter) use preg_split::*; +pub(in crate::interpreter) use split_helpers::*; +pub(in crate::interpreter) use targets::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs b/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs new file mode 100644 index 0000000000..f50830e1e1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! Parses PHP delimited preg patterns and compiles them into PCRE2 regexes. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` entrypoint modules. +//! +//! Key details: +//! - Only eval-supported modifiers are accepted; unsupported delimiters or +//! malformed patterns produce runtime fatal status. + +use super::super::super::*; + +/// Compiles one eval PCRE-style delimited pattern into a PCRE2 regex. +pub(in crate::interpreter) fn eval_preg_regex( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; + Regex::compile(&body, modifiers) +} + +/// Splits a PHP delimited regex into body bytes and supported modifiers. +pub(in crate::interpreter) fn eval_preg_pattern_parts( + pattern: &[u8], +) -> Result<(Vec, EvalPregModifiers), EvalStatus> { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { + return Err(EvalStatus::RuntimeFatal); + } + let delimiter = pattern[0]; + if delimiter == b'\\' { + return Err(EvalStatus::RuntimeFatal); + } + let closing = eval_preg_closing_delimiter(delimiter); + let close_index = + eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; + let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); + let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; + Ok((body, modifiers)) +} + +/// Returns the closing regex delimiter for PHP's paired delimiter forms. +pub(in crate::interpreter) fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { + match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + _ => delimiter, + } +} + +/// Finds the first unescaped closing regex delimiter. +pub(in crate::interpreter) fn eval_preg_find_closing_delimiter( + pattern: &[u8], + closing: u8, +) -> Option { + let mut escaped = false; + for (index, byte) in pattern.iter().copied().enumerate().skip(1) { + if escaped { + escaped = false; + continue; + } + if byte == b'\\' { + escaped = true; + continue; + } + if byte == closing { + return Some(index); + } + } + None +} + +/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. +pub(in crate::interpreter) fn eval_preg_unescape_delimiter( + body: &[u8], + delimiter: u8, + closing: u8, +) -> Vec { + let mut result = Vec::with_capacity(body.len()); + let mut index = 0; + while index < body.len() { + if body[index] == b'\\' + && index + 1 < body.len() + && matches!(body[index + 1], byte if byte == delimiter || byte == closing) + { + result.push(body[index + 1]); + index += 2; + } else { + result.push(body[index]); + index += 1; + } + } + result +} + +/// Parses eval-supported PHP regex modifiers. +pub(in crate::interpreter) fn eval_preg_modifiers( + modifiers: &[u8], +) -> Result { + let mut parsed = EvalPregModifiers::default(); + for modifier in modifiers { + match *modifier { + b'i' => parsed.case_insensitive = true, + b'm' => parsed.multi_line = true, + b's' => parsed.dot_matches_new_line = true, + b'U' => parsed.swap_greed = true, + b'u' => parsed.unicode = true, + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(parsed) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs new file mode 100644 index 0000000000..95372e8e47 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs @@ -0,0 +1,178 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_match`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` and special by-ref call handling. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - `$matches` writeback for `preg_match()`. +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use super::super::*; +use super::*; + +eval_builtin! { + name: "preg_match", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: PregMatch, + values: PregMatch, +} + + +/// Evaluates PHP `preg_match()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, None, values)?; + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `preg_match()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_preg_match_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + &evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return eval_preg_match_result(pattern.value, subject.value, values); + }; + let target = matches + .ref_target + .clone() + .ok_or(EvalStatus::RuntimeFatal)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(&target, matches_array, context, values)?; + Ok(result) +} + +/// Returns whether one regex matches the subject string. +pub(in crate::interpreter) fn eval_preg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + values.int(i64::from(regex.is_match(&subject))) +} + +/// Returns the match flag plus PHP `$matches` capture array for one regex search. +pub(in crate::interpreter) fn eval_preg_match_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let flags = eval_preg_match_flags(flags, values)?; + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + if let Some(captures) = regex.captures(&subject) { + let matches = eval_preg_capture_array( + &subject, + Some(&captures), + offset_capture, + unmatched_as_null, + values, + )?; + let matched = values.int(1)?; + return Ok((matched, matches)); + } + let matches = + eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; + let matched = values.int(0)?; + Ok((matched, matches)) +} + +/// Returns supported `preg_match()` flags. +pub(in crate::interpreter) fn eval_preg_match_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Dispatches by-value `preg_match()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_match_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(matched) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(matched) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs new file mode 100644 index 0000000000..1e544ba482 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs @@ -0,0 +1,239 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_match_all`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` and special by-ref call handling. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - capture-matrix assembly for `preg_match_all()`. +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use super::super::*; +use super::*; + +eval_builtin! { + name: "preg_match_all", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: PregMatchAll, + values: PregMatchAll, +} + + +/// Evaluates PHP `preg_match_all()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match_all( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, None, values)?; + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `preg_match_all()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_preg_match_all_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + &evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return eval_preg_match_all_result(pattern.value, subject.value, values); + }; + let target = matches + .ref_target + .clone() + .ok_or(EvalStatus::RuntimeFatal)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(&target, matches_array, context, values)?; + Ok(result) +} + +/// Counts all non-overlapping regex matches in one subject string. +pub(in crate::interpreter) fn eval_preg_match_all_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let count = regex.captures_iter(&subject).count(); + values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. +pub(in crate::interpreter) fn eval_preg_match_all_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let capture_count = regex.captures_len(); + let subject = values.string_bytes(subject)?; + let captures: Vec> = regex.captures_iter(&subject).collect(); + let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let flags = eval_preg_match_all_flags(flags, values)?; + let matches = if flags & EVAL_PREG_SET_ORDER != 0 { + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? + } else { + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? + }; + Ok((count, matches)) +} + +/// Returns supported `preg_match_all()` flags. +pub(in crate::interpreter) fn eval_preg_match_all_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(EVAL_PREG_PATTERN_ORDER); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_PATTERN_ORDER + | EVAL_PREG_SET_ORDER + | EVAL_PREG_OFFSET_CAPTURE + | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_pattern_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let mut row = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let key = + values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_set_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let mut row = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Dispatches by-value `preg_match_all()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_match_all_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(count) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(count) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs new file mode 100644 index 0000000000..0bd14bd899 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! backreference replacement expansion for `preg_replace()`. + +use super::super::super::*; +use super::*; + +eval_builtin! { + name: "preg_replace", + area: Regex, + params: [pattern, replacement, subject], + direct: PregReplace, + values: PregReplace, +} + +/// Evaluates PHP `preg_replace()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +pub(in crate::interpreter) fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + + +/// Dispatches by-value `preg_replace()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_replace_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_result(*pattern, *replacement, *subject, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs new file mode 100644 index 0000000000..9f37d770f8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_replace_callback`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! callback invocation for `preg_replace_callback()`. + +use super::super::super::*; +use super::super::*; +use super::*; + +eval_builtin! { + name: "preg_replace_callback", + area: Regex, + params: [pattern, callback, subject], + direct: PregReplaceCallback, + values: PregReplaceCallback, +} + +/// Evaluates PHP `preg_replace_callback()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace_callback( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_callback_result_from_scope(pattern, callback, subject, Some(scope), context, values) +} + +/// Replaces every regex match by invoking an eval-supported callback with `$matches`. +pub(in crate::interpreter) fn eval_preg_replace_callback_result( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_preg_replace_callback_result_from_scope(pattern, callback, subject, None, context, values) +} + +/// Replaces regex matches with optional lexical scope for callback names. +fn eval_preg_replace_callback_result_from_scope( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; + let callback_result = + eval_evaluated_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = values.cast_string(callback_result)?; + let callback_bytes = values.string_bytes(callback_result)?; + result.extend_from_slice(&callback_bytes); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + + +/// Dispatches by-value `preg_replace_callback()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_replace_callback_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs new file mode 100644 index 0000000000..e4e837cfd6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs @@ -0,0 +1,120 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_split`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - result assembly for `preg_split()`. +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use super::*; + +eval_builtin! { + name: "preg_split", + area: Regex, + params: [ + pattern, + subject, + limit = EvalBuiltinDefaultValue::Int(-1), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: PregSplit, + values: PregSplit, +} + + +/// Evaluates PHP `preg_split()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_split_result(pattern, subject, None, None, values) + } + [pattern, subject, limit] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), None, values) + } + [pattern, subject, limit, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits a subject string with eval-supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + limit: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let limit = eval_preg_split_limit(limit, values)?; + let flags = eval_preg_split_flags(flags, values)?; + let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; + let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; + let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; + let mut pieces = Vec::::new(); + let mut cursor = 0; + + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + if eval_preg_split_reached_limit(&pieces, limit) { + break; + } + eval_preg_split_push_piece( + &mut pieces, + &subject[cursor..matched.start()], + cursor, + no_empty, + ); + if capture_delimiters { + eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); + } + cursor = matched.end(); + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); + + let mut result = values.array_new(pieces.len())?; + for (index, piece) in pieces.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_split_piece_value(piece, offset_capture, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Dispatches by-value `preg_split()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_split_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values), + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values) + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs new file mode 100644 index 0000000000..3e901f145c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Expands PHP preg replacement backreferences against one regex capture set. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::replace`. +//! +//! Key details: +//! - Supports `$n`, `${n}`, and `\n` capture references without allocating +//! intermediate runtime cells. + +use super::*; + +/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. +pub(in crate::interpreter) fn eval_preg_expand_replacement( + replacement: &[u8], + subject: &[u8], + captures: &Captures<'_>, + result: &mut Vec, +) { + let mut index = 0; + while index < replacement.len() { + match replacement[index] { + b'$' => { + if let Some((capture_index, next_index)) = + eval_preg_replacement_capture_index(replacement, index + 1) + { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } else { + result.push(replacement[index]); + index += 1; + } + } + b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { + let (capture_index, next_index) = + eval_preg_decimal_capture_index(replacement, index + 1); + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } + byte => { + result.push(byte); + index += 1; + } + } + } +} + +/// Parses a dollar-style replacement capture reference. +pub(in crate::interpreter) fn eval_preg_replacement_capture_index( + bytes: &[u8], + index: usize, +) -> Option<(usize, usize)> { + if bytes.get(index).copied() == Some(b'{') { + let mut cursor = index + 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == start || bytes.get(cursor).copied() != Some(b'}') { + return None; + } + let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; + return Some((capture, cursor + 1)); + } + if bytes.get(index).is_some_and(u8::is_ascii_digit) { + let (capture, next) = eval_preg_decimal_capture_index(bytes, index); + return Some((capture, next)); + } + None +} + +/// Parses a one- or two-digit replacement capture reference. +pub(in crate::interpreter) fn eval_preg_decimal_capture_index( + bytes: &[u8], + index: usize, +) -> (usize, usize) { + let mut cursor = index; + let end = usize::min(bytes.len(), index + 2); + while cursor < end && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + ( + eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), + cursor, + ) +} + +/// Converts ASCII decimal bytes into a `usize` capture index. +pub(in crate::interpreter) fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { + let mut value = 0usize; + for byte in bytes { + value = value.checked_mul(10)?; + value = value.checked_add(usize::from(byte - b'0'))?; + } + Some(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/split_helpers.rs b/crates/elephc-magician/src/interpreter/builtins/regex/split_helpers.rs new file mode 100644 index 0000000000..80d0fa2556 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/split_helpers.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Owns `preg_split()` flag parsing and split-piece conversion helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::split`. +//! +//! Key details: +//! - Split pieces retain byte offsets so `PREG_SPLIT_OFFSET_CAPTURE` can be applied +//! after piece collection. + +use super::super::super::*; +use super::super::*; + +/// One `preg_split()` output segment plus its byte offset in the subject. +pub(in crate::interpreter) struct EvalPregSplitPiece { + bytes: Vec, + offset: usize, +} + +/// Returns the PHP `preg_split()` limit, treating zero as unlimited. +pub(in crate::interpreter) fn eval_preg_split_limit( + limit: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(limit) = limit else { + return Ok(None); + }; + let limit = eval_int_value(limit, values)?; + if limit <= 0 { + return Ok(None); + } + usize::try_from(limit) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = + EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. +pub(in crate::interpreter) fn eval_preg_split_reached_limit( + pieces: &[EvalPregSplitPiece], + limit: Option, +) -> bool { + matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) +} + +/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. +pub(in crate::interpreter) fn eval_preg_split_push_piece( + pieces: &mut Vec, + piece: &[u8], + offset: usize, + no_empty: bool, +) { + if no_empty && piece.is_empty() { + return; + } + pieces.push(EvalPregSplitPiece { + bytes: piece.to_vec(), + offset, + }); +} + +/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. +pub(in crate::interpreter) fn eval_preg_split_push_captures( + pieces: &mut Vec, + subject: &[u8], + captures: &Captures<'_>, + no_empty: bool, +) { + for index in 1..captures.len() { + if let Some(matched) = captures.get(index) { + eval_preg_split_push_piece( + pieces, + &subject[matched.start()..matched.end()], + matched.start(), + no_empty, + ); + } + } +} + +/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_split_piece_value( + piece: &EvalPregSplitPiece, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.string_bytes_value(&piece.bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs b/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs new file mode 100644 index 0000000000..9102e12602 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Shared by-reference `$matches` target helpers for eval preg builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::match_one` +//! - `crate::interpreter::builtins::regex::match_all` +//! +//! Key details: +//! - Direct preg calls capture writable caller lvalues in source order, then +//! write the materialized matches array back after regex execution. + +use super::super::super::*; +use super::super::*; + +/// Captures a writable `$matches` argument target from a direct preg call. +pub(in crate::interpreter) fn eval_preg_matches_target( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, target) = eval_call_arg_value(expr, context, scope, values)?; + target.ok_or(EvalStatus::RuntimeFatal) +} + +/// Writes a preg `$matches` result back to the captured caller lvalue. +pub(in crate::interpreter) fn eval_write_preg_matches_target( + target: &EvalReferenceTarget, + matches_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_write_direct_ref_target( + target, + matches_array, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs new file mode 100644 index 0000000000..4b0ffee3ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Named and spread argument binding for builtin calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. + +use super::*; + +/// Evaluates a direct PHP-visible builtin call with named or spread arguments. +pub(in crate::interpreter) fn eval_builtin_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + Ok(result) +} + +/// Binds evaluated builtin arguments to PHP parameter order when names are used. +pub(in crate::interpreter) fn bind_evaluated_builtin_args( + name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); + } + + let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + collect_bound_builtin_args(name, bound_args, values) +} + +/// Binds one named builtin-call value to the matching PHP parameter slot. +pub(in crate::interpreter) fn bind_builtin_named_arg( + params: &[&str], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + +/// Collects ordered builtin arguments, applying PHP defaults for named-call gaps. +pub(in crate::interpreter) fn collect_bound_builtin_args( + name: &str, + bound_args: Vec>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !bound_args.iter().any(Option::is_some) { + return Ok(Vec::new()); + } + + let shape = eval_builtin_signature_shape(name).ok_or(EvalStatus::RuntimeFatal)?; + let last_index = bound_args + .iter() + .rposition(Option::is_some) + .expect("non-empty bound args has a last supplied arg"); + let mut args = Vec::with_capacity(last_index + 1); + + for (index, arg) in bound_args.into_iter().take(last_index + 1).enumerate() { + if let Some(value) = arg { + args.push(value); + } else if index >= shape.required_param_count { + args.push(eval_builtin_default_arg(name, index, values)?); + } else { + return Err(EvalStatus::RuntimeFatal); + } + } + + Ok(args) +} + +/// Materializes one builtin default argument as a runtime cell. +fn eval_builtin_default_arg( + name: &str, + index: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_builtin_default_value(name, index).ok_or(EvalStatus::RuntimeFatal)? { + EvalBuiltinDefaultValue::Null => values.null(), + EvalBuiltinDefaultValue::Bool(value) => values.bool_value(value), + EvalBuiltinDefaultValue::Int(value) => values.int(value), + EvalBuiltinDefaultValue::Float(value) => values.float(value), + EvalBuiltinDefaultValue::String(value) => values.string(value), + EvalBuiltinDefaultValue::Bytes(value) => values.string_bytes_value(value), + EvalBuiltinDefaultValue::EmptyArray => values.array_new(0), + } +} + +/// Returns PHP parameter names for builtin calls implemented by eval. +pub(in crate::interpreter) fn eval_builtin_param_names( + name: &str, +) -> Option<&'static [&'static str]> { + if let Some(params) = eval_declared_builtin_param_names(name) { + return Some(params); + } + + None +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs new file mode 100644 index 0000000000..956c61dd68 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -0,0 +1,434 @@ +//! Purpose: +//! Resolves PHP callbacks into normalized callable targets. +//! Invocation strategies live in focused child modules. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. +//! - Callback normalization remains centralized while dispatch is grouped by +//! evaluated, object, static, and call-array entry surfaces. + +mod array_dispatch; +mod execution; +mod object_dispatch; +mod static_dispatch; + +use super::*; + +pub(in crate::interpreter) use array_dispatch::*; +pub(in crate::interpreter) use execution::*; +use object_dispatch::*; +use static_dispatch::*; + +/// Distinguishes PHP's invokable-object callback form from an explicit method callback. +#[derive(Clone, Copy, PartialEq, Eq)] +enum EvalObjectCallbackKind { + /// A bare object callback whose `__invoke` magic method is callable regardless of visibility. + InvokableObject, + /// An explicit object-method callback that must pass normal visibility validation. + Method, +} + +/// Dispatches `call_user_func_array` with optional lexical scope for special class receivers. +pub(in crate::interpreter) fn eval_call_user_func_array_with_values_from_scope( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_call_user_func_callback( + callback, + "call_user_func_array", + lexical_scope, + context, + values, + )?; + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = eval_array_call_arg_values(arg_array, context, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Dispatches `call_user_func` with optional lexical scope for special class receivers. +pub(in crate::interpreter) fn eval_call_user_func_with_values_from_scope( + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, callback_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = + eval_call_user_func_callback(*callback, "call_user_func", lexical_scope, context, values)?; + eval_evaluated_callable_with_call_user_func_values( + &callback, + callback_args.to_vec(), + context, + values, + ) +} + +/// Normalizes a `call_user_func*` callback and maps non-invokable objects to PHP's TypeError. +fn eval_call_user_func_callback( + callback: RuntimeCellHandle, + function_name: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_callable_with_optional_scope(callback, context, lexical_scope, values) { + Ok(callback) => { + eval_validate_call_user_func_callback(&callback, function_name, context, values)?; + Ok(callback) + } + Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { + eval_call_user_func_type_error( + function_name, + "no array or string given", + context, + values, + ) + } + Err(status) => Err(status), + } +} + +/// Normalizes one PHP callback value for eval dynamic callable dispatch. +pub(in crate::interpreter) fn eval_callable( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_callable_with_optional_scope(callback, context, None, values) +} + +/// Normalizes one PHP callback while retaining the current method scope when available. +pub(in crate::interpreter) fn eval_callable_from_scope( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_callable_with_optional_scope(callback, context, Some(scope), values) +} + +/// Normalizes one PHP callback with optional scope-sensitive special class receivers. +pub(in crate::interpreter) fn eval_callable_with_optional_scope( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(callback)? == EVAL_TAG_OBJECT { + return eval_object_callable(callback, context, values); + } + if values.is_array_like(callback)? { + return eval_array_callable(callback, context, lexical_scope, values); + } + eval_string_callable(callback, context, lexical_scope, values) +} + +/// Normalizes one invokable eval object for dynamic callable dispatch. +pub(in crate::interpreter) fn eval_object_callable( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(callback)?; + if let Some(target) = context.closure_object_target(identity) { + return Ok(eval_closure_object_target_callable(target)); + } + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(callback, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if is_static || is_abstract { + return Err(EvalStatus::UnsupportedConstruct); + } + return Ok(EvaluatedCallable::InvokableObject { object: callback }); + }; + let Some((_, method)) = context.class_method(class.name(), "__invoke") else { + if eval_dynamic_class_native_invokable_method_class(class.name(), context, values)? + .is_some() + { + return Ok(EvaluatedCallable::InvokableObject { object: callback }); + } + return Err(EvalStatus::UnsupportedConstruct); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::UnsupportedConstruct); + } + Ok(EvaluatedCallable::InvokableObject { object: callback }) +} + +/// Converts a PHP-visible eval `Closure` object target into the shared callback enum. +fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> EvaluatedCallable { + match target { + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { + name: name.clone(), + display_name: name.clone(), + }, + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => EvaluatedCallable::BoundClosure { + name: name.clone(), + bound_this: *bound_this, + bound_scope: bound_scope.clone(), + }, + EvalClosureObjectTarget::InvokableObject { object } => EvaluatedCallable::InvokableObject { + object: *object, + }, + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::ObjectMethod { + object: *object, + method: method.clone(), + called_class: called_class.clone(), + native_class: native_class.clone(), + bridge_scope: bridge_scope.clone(), + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::StaticMethod { + class_name: class_name.clone(), + method: method.clone(), + called_class: called_class.clone(), + native_class: native_class.clone(), + bridge_scope: bridge_scope.clone(), + }, + } +} + +/// Normalizes one two-element object-method or static-method callable array. +pub(in crate::interpreter) fn eval_array_callable( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.array_len(callback)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = match values.int(1) { + Ok(one) => one, + Err(status) => { + values.release(zero)?; + return Err(status); + } + }; + let receiver = match values.array_get(callback, zero) { + Ok(receiver) => receiver, + Err(status) => { + values.release(zero)?; + values.release(one)?; + return Err(status); + } + }; + let method = match values.array_get(callback, one) { + Ok(method) => method, + Err(status) => { + values.release(zero)?; + values.release(one)?; + return Err(status); + } + }; + values.release(zero)?; + values.release(one)?; + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => { + let native_dispatch = context + .eval_object_callable_native_dispatch(callback, receiver, &method) + .map(|(native_class, bridge_scope, called_class)| { + ( + native_class.to_string(), + bridge_scope.to_string(), + called_class.to_string(), + ) + }); + let (native_class, bridge_scope, called_class) = native_dispatch + .map(|(native_class, bridge_scope, called_class)| { + (Some(native_class), Some(bridge_scope), Some(called_class)) + }) + .unwrap_or((None, None, None)); + Ok(EvaluatedCallable::ObjectMethod { + object: receiver, + method, + called_class, + native_class, + bridge_scope, + }) + } + EVAL_TAG_STRING => { + let class_name = String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?; + if let Some(callable) = eval_special_class_array_callable( + &class_name, + &method, + lexical_scope, + context, + values, + )? { + return Ok(callable); + } + let called_class = context + .eval_static_callable_called_class(callback, &class_name, &method) + .map(str::to_string); + let native_dispatch = context + .eval_static_callable_native_dispatch(callback, &class_name, &method) + .map(|(native_class, bridge_scope)| { + (native_class.to_string(), bridge_scope.to_string()) + }); + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) + .unwrap_or((None, None)); + Ok(EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Resolves deprecated `self`/`static`/`parent` callable arrays inside method scope. +fn eval_special_class_array_callable( + class_name: &str, + method: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_callable_array_receiver_is_special_class_name(class_name) { + return Ok(None); + } + let Some(scope) = lexical_scope else { + return Ok(None); + }; + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let use_instance_receiver = + !eval_special_class_array_static_method_exists(&receiver.dispatch_class, method, context, values)?; + if use_instance_receiver { + if let Some(object) = + eval_static_syntax_instance_receiver(&receiver.dispatch_class, Some(scope), context, values)? + { + return Ok(Some(EvaluatedCallable::ObjectMethod { + object, + method: method.to_string(), + called_class: Some(receiver.called_class), + native_class: None, + bridge_scope: None, + })); + } + } + Ok(Some(EvaluatedCallable::StaticMethod { + class_name: receiver.dispatch_class, + method: method.to_string(), + called_class: Some(receiver.called_class), + native_class: None, + bridge_scope: None, + })) +} + +/// Returns whether a callable-array receiver is PHP's deprecated special class string. +fn eval_callable_array_receiver_is_special_class_name(class_name: &str) -> bool { + matches!( + class_name.trim_start_matches('\\').to_ascii_lowercase().as_str(), + "self" | "static" | "parent" + ) +} + +/// Returns whether a special class callable array names a real static method. +fn eval_special_class_array_static_method_exists( + class_name: &str, + method: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some((_, method)) = eval_dynamic_static_method_for_call(class_name, method, context) { + return Ok(method.is_static()); + } + let native_class = if context.has_class(class_name) { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + parent + } else if context.has_interface(class_name) + || context.has_trait(class_name) + || context.has_enum(class_name) + { + return Ok(false); + } else { + class_name.to_string() + }; + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method, context, values)? + .is_some_and(|(_, _, is_static, _)| is_static)) +} + +/// Normalizes one string callback name for eval dynamic callable dispatch. +/// Uses method lexical scope only for PHP APIs that resolve deprecated `self::`, +/// `static::`, and `parent::` string callbacks through the current method. +pub(in crate::interpreter) fn eval_string_callable( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + if let Some((class_name, method)) = callback.split_once("::") { + if class_name.is_empty() || method.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(callable) = eval_special_class_array_callable( + class_name, + method, + lexical_scope, + context, + values, + )? { + return Ok(callable); + } + return Ok(EvaluatedCallable::StaticMethod { + class_name: class_name.trim_start_matches('\\').to_string(), + method: method.to_string(), + called_class: None, + native_class: None, + bridge_scope: None, + }); + } + let display_name = callback.trim_start_matches('\\').to_string(); + Ok(EvaluatedCallable::Named { + name: display_name.to_ascii_lowercase(), + display_name, + }) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs new file mode 100644 index 0000000000..9b6fcdd4ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! Executes normalized callbacks from evaluated call-array arguments or values. +//! +//! Called from: +//! - `call_user_func_array` and internal callable-with-values helpers. +//! +//! Key details: +//! - Named metadata and call-array ordering are preserved before target dispatch. + +use super::*; + +/// Invokes an already normalized callback with optional named-argument metadata. +pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_callable_with_call_array_args(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_call_array_args(name, evaluated_args, context, values); + }; + eval_bound_closure_with_call_args_ref_mode( + &closure, + *bound_this, + bound_scope.clone(), + closure.function().parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_args(*object, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ), + None => eval_object_method_with_call_user_func_args( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => match called_class { + Some(called_class) => eval_static_method_with_call_user_func_args( + class_name, + method, + Some(called_class), + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_with_call_user_func_args( + class_name, + method, + None, + evaluated_args, + context, + values, + ), + }, + }, + } +} + +/// Invokes a PHP-visible callable name with source-order positional values. +pub(in crate::interpreter) fn eval_callable_with_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = + bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a callable with arguments that may carry `call_user_func_array` names. +pub(in crate::interpreter) fn eval_callable_with_call_array_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } + if let Some(result) = + eval_mutating_builtin_with_call_array_args(name, &evaluated_args, context, values)? + { + return Ok(result); + } + if eval_php_visible_builtin_exists(name) { + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_evaluated_args_and_ref_mode( + &function, + function.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs new file mode 100644 index 0000000000..aedacc4159 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Executes normalized callables from already evaluated positional values. +//! +//! Called from: +//! - `call_user_func`, direct callable invocation, and callback builtins. +//! +//! Key details: +//! - Closure binding and by-value reference warnings preserve PHP call semantics. + +use super::*; + +/// Invokes an already normalized callback with source-order positional values. +pub(in crate::interpreter) fn eval_evaluated_callable_with_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } + eval_callable_with_values(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_values(name, evaluated_args, context, values); + }; + eval_bound_closure_with_call_args( + &closure, + *bound_this, + bound_scope.clone(), + positional_args(evaluated_args), + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_call_result( + *object, + positional_args(evaluated_args), + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ), + None => eval_method_call_result(*object, method, evaluated_args, context, values), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + positional_args(evaluated_args), + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method, + positional_args(evaluated_args), + context, + values, + ), + }, + }, + } +} + +/// Invokes a bound eval closure with either `$this` or only an explicit class scope. +pub(super) fn eval_bound_closure_with_call_args( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope( + closure, + this_object, + bound_scope, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope( + closure, + bound_scope, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a bound eval closure with caller-selected by-reference binding flags. +pub(super) fn eval_bound_closure_with_call_args_ref_flags( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a bound eval closure with caller-selected by-reference binding mode. +pub(super) fn eval_bound_closure_with_call_args_ref_mode( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + } +} + +/// Invokes a normalized callback through `call_user_func()` by-value argument semantics. +pub(super) fn eval_evaluated_callable_with_call_user_func_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_values( + name, + evaluated_args, + context, + values, + ); + }; + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( + &closure, + *bound_this, + bound_scope.clone(), + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_values( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_values( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_values( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, + } +} + +/// Invokes a normalized callback with by-value semantics while preserving named args. +pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_clear_evaluated_arg_ref_targets(evaluated_args); + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_named_callable_with_call_user_func_args(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_args( + name, + evaluated_args, + context, + values, + ); + }; + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( + &closure, + *bound_this, + bound_scope.clone(), + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_args( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_args( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_args( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, + } +} + +/// Removes caller writeback targets before a by-value callable API dispatch. +pub(super) fn eval_clear_evaluated_arg_ref_targets( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs new file mode 100644 index 0000000000..c1ecd92752 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs @@ -0,0 +1,376 @@ +//! Purpose: +//! Dispatches named, invokable-object, and object-method callbacks. +//! +//! Called from: +//! - Callable execution after callback normalization. +//! +//! Key details: +//! - Native and eval object methods share call_user_func by-value handling. + +use super::*; + +/// Invokes a named callable through `call_user_func()` and warns for by-ref parameters. +pub(super) fn eval_named_callable_with_call_user_func_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + &closure, + None, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a named callable through by-value callable semantics with named args. +pub(super) fn eval_named_callable_with_call_user_func_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args + .iter() + .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) + { + let evaluated_values = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_named_callable_with_call_user_func_values( + name, + evaluated_values, + context, + values, + ); + } + if let Some(closure) = context.closure(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + &closure, + None, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes an invokable object through `call_user_func()` by-value argument semantics. +pub(super) fn eval_invokable_object_with_call_user_func_values( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_values( + object, + "__invoke", + EvalObjectCallbackKind::InvokableObject, + evaluated_args, + context, + values, + ) +} + +/// Invokes an invokable object through by-value callable semantics with named args. +pub(super) fn eval_invokable_object_with_call_user_func_args( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_args( + object, + "__invoke", + EvalObjectCallbackKind::InvokableObject, + evaluated_args, + context, + values, + ) +} + +/// Invokes an object-method callable through `call_user_func()` by-value semantics. +pub(super) fn eval_object_method_with_call_user_func_values( + object: RuntimeCellHandle, + method: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + callback_kind, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + +/// Invokes an object-method callable through by-value callable semantics with named args. +pub(super) fn eval_object_method_with_call_user_func_args( + object: RuntimeCellHandle, + method: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + callback_kind, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated object methods. +pub(super) fn eval_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + if callback_kind == EvalObjectCallbackKind::Method + && validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + { + return eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_method_with_values_and_ref_mode( + &declaring_class, + &called_class_name, + &method, + object, + method.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) + .map(Some); + } + let Some(parent) = context.class_native_parent_name(&called_class_name) else { + return Ok(None); + }; + eval_native_object_method_call_user_func_result_for_class( + object, + &parent, + method_name, + Some(&called_class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts call-user-func by-value dispatch for a generated/AOT object method. +pub(super) fn eval_native_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = runtime_object_class_name(object, values)?; + eval_native_object_method_call_user_func_result_for_class( + object, + &class_name, + method_name, + Some(&class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts generated/AOT object-method dispatch for one resolved receiver class. +pub(super) fn eval_native_object_method_call_user_func_result_for_class( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + called_class_scope: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on the + // receiver's class (PHP private-method shadowing); dispatch with the + // scope string so the native shadow slot selects it directly. + return eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ) + .map(Some); + } + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract) + { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs new file mode 100644 index 0000000000..1969a3c7eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs @@ -0,0 +1,243 @@ +//! Purpose: +//! Dispatches normalized static-method callbacks through call_user_func semantics. +//! +//! Called from: +//! - Callable execution for class-string and special-scope static targets. +//! +//! Key details: +//! - Called-class propagation and by-reference warning flags stay paired. + +use super::*; + +/// Invokes a static-method callable through `call_user_func()` by-value semantics. +pub(super) fn eval_static_method_with_call_user_func_values( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a static-method callable through by-value callable semantics with named args. +pub(super) fn eval_static_method_with_call_user_func_args( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated static methods. +pub(super) fn eval_static_method_call_user_func_result( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = called_class.unwrap_or(&dispatch_class).to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, method_name, context) + { + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + return eval_magic_static_method_call( + &dispatch_class, + &called_class, + method_name, + evaluated_args, + context, + values, + ); + } + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_static_method_with_values_and_ref_mode( + &declaring_class, + &called_class, + &method, + method.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) + .map(Some); + } + let native_class = if context.has_class(&dispatch_class) { + let Some(parent) = context.class_native_parent_name(&dispatch_class) else { + return Ok(None); + }; + parent + } else if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + return Ok(None); + } else { + dispatch_class.clone() + }; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + method_name, + context, + values, + )? + else { + if context + .native_static_method_signature(&native_class, method_name) + .is_some() + { + return eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + None, + Some(&called_class), + context, + values, + ) + .map(Some); + } + return Ok(None); + }; + if !is_static || is_abstract { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract) + { + return eval_native_magic_static_method_call( + &native_class, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + Some(&declaring_class), + Some(&called_class), + context, + values, + ) + .map(Some) +} + +/// Builds by-value binding flags for `call_user_func()` and emits PHP by-ref warnings. +pub(super) fn eval_call_user_func_by_value_ref_flags( + callable_name: &str, + params: &[String], + parameter_is_by_ref: &[bool], + parameter_is_variadic: &[bool], + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); + for arg_index in 0..supplied_count { + let param_index = if variadic_index.is_some_and(|index| arg_index >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + arg_index + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + continue; + } + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + arg_index + 1 + ))?; + } + Ok(vec![false; params.len()]) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs new file mode 100644 index 0000000000..da80aa6505 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -0,0 +1,622 @@ +//! Purpose: +//! Validates normalized call_user_func callback targets before dispatch. +//! Keeps PHP's invalid-callback TypeError messages out of generic callable normalization. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::callable`. +//! +//! Key details: +//! - Direct callable invocation still uses normal method-call errors; these helpers +//! are scoped to `call_user_func` and `call_user_func_array`. +//! - This file is a deliberate >500 LoC single-scope validation matrix for PHP's +//! callback TypeErrors; splitting it would duplicate method visibility and AOT +//! bridge checks across call_user_func surfaces. + +use super::super::super::*; + +#[derive(Clone, Copy)] +enum EvalCallableValidationError<'a> { + CallUserFunc(&'a str), + ClosureFromCallable, +} + +/// Validates callback targets whose PHP errors depend on method metadata. +pub(in crate::interpreter) fn eval_validate_call_user_func_callback( + callback: &EvaluatedCallable, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_validate_callback( + callback, + EvalCallableValidationError::CallUserFunc(function_name), + context, + values, + ) +} + +/// Validates `Closure::fromCallable()` callback targets before materializing a closure. +pub(in crate::interpreter) fn eval_validate_closure_from_callable_callback( + callback: &EvaluatedCallable, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_validate_callback( + callback, + EvalCallableValidationError::ClosureFromCallable, + context, + values, + ) +} + +/// Throws the PHP TypeError used when `Closure::fromCallable()` cannot normalize a value. +pub(in crate::interpreter) fn eval_closure_from_callable_type_error( + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + EvalCallableValidationError::ClosureFromCallable, + reason, + context, + values, + ) +} + +/// Validates one normalized callback for a PHP API with invalid-callback TypeErrors. +fn eval_validate_callback( + callback: &EvaluatedCallable, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match callback { + EvaluatedCallable::Named { name, display_name } => { + eval_validate_named_callable(name, display_name, error, context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => match native_class { + Some(_) => Ok(()), + None => eval_validate_call_user_func_object_method( + *object, + method, + error, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + native_class, + .. + } => match native_class { + Some(_) => Ok(()), + None => eval_validate_call_user_func_static_method( + class_name, + method, + error, + context, + values, + ), + }, + EvaluatedCallable::BoundClosure { .. } | EvaluatedCallable::InvokableObject { .. } => { + Ok(()) + } + } +} + +/// Validates string function callables before APIs materialize or dispatch them. +fn eval_validate_named_callable( + name: &str, + display_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if context.has_closure(name) + || context.has_function(name) + || eval_function_probe_exists(context, name) + { + return Ok(()); + } + eval_invalid_callback_type_error( + error, + &format!("function \"{display_name}\" not found or invalid function name"), + context, + values, + ) +} + +/// Validates `[$object, "method"]` callbacks before call_user_func dispatch. +fn eval_validate_call_user_func_object_method( + object: RuntimeCellHandle, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(()); + } + let Some(class) = context.dynamic_object_class(identity) else { + return eval_validate_call_user_func_native_object_method( + object, + method_name, + error, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + else { + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + let has_native_metadata = eval_dynamic_class_native_method_metadata( + &called_class_name, + method_name, + context, + values, + )? + .is_some(); + let has_native_magic = + eval_call_user_func_native_instance_magic_callable(&parent, context, values)?; + let has_native_signature = context + .native_method_signature(&parent, method_name) + .is_some(); + let missing_native_class = !values.class_exists(&parent)?; + let has_native_target = has_native_metadata + || has_native_magic + || has_native_signature + || missing_native_class; + if has_native_target { + return eval_validate_call_user_func_native_object_method_for_class( + &parent, + &called_class_name, + method_name, + error, + context, + values, + ); + } + } + if eval_call_user_func_instance_magic_callable(&called_class_name, context) { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + error, + &called_class_name, + method_name, + context, + values, + ); + }; + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_call_user_func_instance_magic_callable(&called_class_name, context) + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + error, + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ) +} + +/// Validates generated/AOT object-method callbacks when method metadata is available. +fn eval_validate_call_user_func_native_object_method( + object: RuntimeCellHandle, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = runtime_object_class_name(object, values)?; + eval_validate_call_user_func_native_object_method_for_class( + &class_name, + &class_name, + method_name, + error, + context, + values, + ) +} + +/// Validates generated/AOT object-method callbacks by class metadata. +fn eval_validate_call_user_func_native_object_method_for_class( + class_name: &str, + error_class_name: &str, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + if eval_call_user_func_native_instance_magic_callable(&class_name, context, values)? + || context + .native_method_signature(&class_name, method_name) + .is_some() + || !values.class_exists(class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + error, + error_class_name, + method_name, + context, + values, + ); + }; + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_instance_magic_callable(&class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + error, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + +/// Validates `["Class", "method"]` callbacks before call_user_func dispatch. +fn eval_validate_call_user_func_static_method( + class_name: &str, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(()); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() { + return eval_call_user_func_non_static_method_type_error( + error, + &declaring_class, + method.name(), + context, + values, + ); + } + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_call_user_func_static_magic_callable(&class_name, context) + { + return Ok(()); + } + return eval_call_user_func_method_access_type_error( + error, + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if eval_call_user_func_static_magic_callable(&class_name, context) { + return Ok(()); + } + if let Some(parent) = context.class_native_parent_name(&class_name) { + return eval_validate_call_user_func_native_static_method_for_class( + &parent, + &class_name, + method_name, + error, + context, + values, + ); + } + return eval_call_user_func_missing_method_type_error( + error, + &class_name, + method_name, + context, + values, + ); + } + eval_validate_call_user_func_native_static_method( + &class_name, + method_name, + error, + context, + values, + ) +} + +/// Validates generated/AOT static-method callbacks when method metadata is available. +fn eval_validate_call_user_func_native_static_method( + class_name: &str, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_call_user_func_native_static_magic_callable(class_name, context, values)? + || context + .native_static_method_signature(class_name, method_name) + .is_some() + || !values.class_exists(class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + error, + class_name, + method_name, + context, + values, + ); + }; + if !is_static { + return eval_call_user_func_non_static_method_type_error( + error, + &declaring_class, + method_name, + context, + values, + ); + } + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_static_magic_callable(class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + error, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + +/// Validates generated/AOT static-method callbacks while preserving eval-class error names. +fn eval_validate_call_user_func_native_static_method_for_class( + class_name: &str, + error_class_name: &str, + method_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_call_user_func_native_static_magic_callable(class_name, context, values)? + || context + .native_static_method_signature(class_name, method_name) + .is_some() + || !values.class_exists(class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + error, + error_class_name, + method_name, + context, + values, + ); + }; + if !is_static { + return eval_call_user_func_non_static_method_type_error( + error, + &declaring_class, + method_name, + context, + values, + ); + } + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_static_magic_callable(class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + error, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + +/// Returns whether an eval class has an instance magic-call fallback. +fn eval_call_user_func_instance_magic_callable( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a static magic-call fallback. +fn eval_call_user_func_static_magic_callable( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} + +/// Returns whether an AOT class has an instance magic-call fallback. +fn eval_call_user_func_native_instance_magic_callable( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether an AOT class has a static magic-call fallback. +fn eval_call_user_func_native_static_magic_callable( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Throws the API-specific TypeError for a missing object or static method callback. +fn eval_call_user_func_missing_method_type_error( + error: EvalCallableValidationError<'_>, + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + error, + &format!( + "class {} does not have a method \"{}\"", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws the API-specific TypeError for an inaccessible method callback. +fn eval_call_user_func_method_access_type_error( + error: EvalCallableValidationError<'_>, + class_name: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + error, + &format!( + "cannot access {} method {}::{}()", + eval_call_user_func_visibility_label(visibility), + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws the API-specific TypeError for non-static static-method callbacks. +fn eval_call_user_func_non_static_method_type_error( + error: EvalCallableValidationError<'_>, + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + error, + &format!( + "non-static method {}::{}() cannot be called statically", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws a call_user_func or call_user_func_array invalid-callback TypeError. +pub(in crate::interpreter) fn eval_call_user_func_type_error( + function_name: &str, + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + EvalCallableValidationError::CallUserFunc(function_name), + reason, + context, + values, + ) +} + +/// Throws the invalid-callback TypeError for the PHP API currently validating a callback. +fn eval_invalid_callback_type_error( + error: EvalCallableValidationError<'_>, + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let message = match error { + EvalCallableValidationError::CallUserFunc(function_name) => { + format!( + "{}(): Argument #1 ($callback) must be a valid callback, {}", + function_name, reason + ) + } + EvalCallableValidationError::ClosureFromCallable => { + format!("Failed to create closure from callable: {reason}") + } + }; + eval_throw_type_error(&message, context, values) +} + +/// Returns PHP's lowercase visibility label used in callback TypeError messages. +fn eval_call_user_func_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs new file mode 100644 index 0000000000..640e44cf5e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Routes by-value dynamic builtin dispatch through declarative registry lookup +//! and eval-only runtime alias fallbacks. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Migrated builtins dispatch through `eval_declared_builtin_values_call`. +//! - Procedural date/time aliases remain a runtime fallback because eval cannot +//! run the static name-resolver rewrite before dispatch. + +use super::eval_declared_builtin_values_call; +use super::super::super::*; + +/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. +pub(in crate::interpreter) fn eval_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_declared_builtin_values_call(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + + if let Some(result) = + eval_date_procedural_alias_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } + Ok(None) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs new file mode 100644 index 0000000000..94dde9d5cc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -0,0 +1,530 @@ +//! Purpose: +//! Dispatches dynamic callable invocations of builtin mutators while preserving +//! caller-side by-reference targets captured during argument evaluation. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::callable`. +//! +//! Key details: +//! - This module only handles builtin calls whose direct PHP semantics can write +//! to caller storage. Other builtins continue through the by-value dispatcher. +//! - This file is a deliberate >500 LoC single-scope by-reference dispatcher: +//! the shared concern is preserving captured writeback targets, not builtin +//! area ownership, so splitting by area would duplicate binding semantics. + +use super::super::super::*; +use super::super::*; + +/// Dispatches dynamic builtin calls that must preserve by-reference caller targets. +pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "settype" => eval_dynamic_settype_call(evaluated_args, context, values)?, + "array_walk" => eval_dynamic_array_walk_call(evaluated_args, context, values)?, + "array_pop" | "array_shift" => { + eval_dynamic_array_pop_shift_call(name, evaluated_args, context, values)? + } + "array_push" | "array_unshift" => { + eval_dynamic_array_push_unshift_call(name, evaluated_args, context, values)? + } + "array_splice" => eval_dynamic_array_splice_call(evaluated_args, context, values)?, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + eval_dynamic_array_sort_call(name, evaluated_args, context, values)? + } + "uasort" | "uksort" | "usort" => { + eval_dynamic_user_sort_call(name, evaluated_args, context, values)? + } + "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, + "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, + "is_callable" => { + Some(eval_is_callable_call_with_evaluated_args( + evaluated_args, + context, + values, + )?) + } + "flock" => eval_dynamic_flock_call(evaluated_args, context, values)?, + "fsockopen" | "pfsockopen" => { + eval_dynamic_fsockopen_call(evaluated_args, context, values)? + } + "stream_select" => eval_dynamic_stream_select_call(evaluated_args, context, values)?, + "stream_socket_accept" => { + eval_dynamic_stream_socket_accept_call(evaluated_args, context, values)? + } + "stream_socket_recvfrom" => { + eval_dynamic_stream_socket_recvfrom_call(evaluated_args, context, values)? + } + _ => return Ok(None), + }; + Ok(result) +} + +/// Evaluates a dynamic `settype()` call when the first argument is writable. +fn eval_dynamic_settype_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["var", "type"], evaluated_args, false)?; + let var = required_evaluated_ref_arg(&bound, 0)?; + let type_name = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = var.ref_target.as_ref() else { + return Ok(None); + }; + let Some(converted) = eval_settype_cast_value(var.value, type_name.value, values)? else { + return values.bool_value(false).map(Some); + }; + eval_write_direct_ref_target( + target, + converted, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(true).map(Some) +} + +/// Evaluates a dynamic `array_walk()` call when the array argument is writable. +fn eval_dynamic_array_walk_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = + bind_evaluated_ref_builtin_args(&["array", "callback"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let callback = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.clone() else { + return Ok(None); + }; + eval_array_walk_ref_result(array.value, target, callback.value, context, values).map(Some) +} + +/// Evaluates a dynamic `array_pop()` or `array_shift()` call against a writable array. +fn eval_dynamic_array_pop_shift_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["array"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let (result, replacement) = eval_array_pop_shift_replacement(name, array.value, values)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates dynamic `array_push()` or `array_unshift()` calls against a writable array. +fn eval_dynamic_array_push_unshift_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, inserted) = + bind_evaluated_ref_builtin_args(&["array", "values"], evaluated_args, true)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + if inserted.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let inserted_values = inserted.iter().map(|arg| arg.value).collect::>(); + let replacement = + eval_array_push_unshift_replacement(name, array.value, &inserted_values, values)?; + let result = eval_array_push_unshift_count_result(array.value, inserted_values.len(), values)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `array_splice()` call against a writable array. +fn eval_dynamic_array_splice_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["array", "offset", "length", "replacement"], + evaluated_args, + false, + )?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let offset = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let length = optional_evaluated_ref_arg(&bound, 2).map(|arg| arg.value); + let replacement_arg = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (removed, replacement) = eval_array_splice_removed_and_replacement( + array.value, + offset.value, + length, + replacement_arg, + values, + )?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(removed)) +} + +/// Evaluates a dynamic standard array sort call against a writable array. +fn eval_dynamic_array_sort_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["array"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let replacement = eval_array_sort_replacement(name, array.value, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic user-comparator sort call against a writable array. +fn eval_dynamic_user_sort_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = + bind_evaluated_ref_builtin_args(&["array", "callback"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let callback = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let replacement = + eval_user_sort_replacement(name, array.value, callback.value, context, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `preg_match()` call when `$matches` is a writable lvalue. +fn eval_dynamic_preg_match_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = matches.ref_target.as_ref() else { + return Ok(None); + }; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (result, matches_array) = + eval_preg_match_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(target, matches_array, context, values)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `preg_match_all()` call when `$matches` is a writable lvalue. +fn eval_dynamic_preg_match_all_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = matches.ref_target.as_ref() else { + return Ok(None); + }; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(target, matches_array, context, values)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `flock()` call when `$would_block` is a writable lvalue. +fn eval_dynamic_flock_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["stream", "operation", "would_block"], + evaluated_args, + false, + )?; + let stream = required_evaluated_ref_arg(&bound, 0)?; + let operation = required_evaluated_ref_arg(&bound, 1)?; + let Some(would_block) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = would_block.ref_target.as_ref() else { + return Ok(None); + }; + let (success, would_block) = + eval_flock_result(stream.value, operation.value, context, values)?; + let would_block = values.bool_value(would_block)?; + eval_write_direct_ref_target( + target, + would_block, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(success).map(Some) +} + +/// Evaluates a dynamic `fsockopen()`/`pfsockopen()` call when error outputs are writable. +fn eval_dynamic_fsockopen_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code = optional_evaluated_ref_arg(&bound, 2); + let error_message = optional_evaluated_ref_arg(&bound, 3); + if error_code.is_none() && error_message.is_none() { + return Ok(None); + } + let error_code_target = optional_dynamic_ref_target(error_code)?; + let error_message_target = optional_dynamic_ref_target(error_message)?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + if let Some(target) = error_code_target { + let error_code = values.int(error_code)?; + eval_write_direct_ref_target( + target, + error_code, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + if let Some(target) = error_message_target { + let error_message = values.string(&error_message)?; + eval_write_direct_ref_target( + target, + error_message, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Evaluates a dynamic `stream_select()` call and writes conservative empty arrays. +fn eval_dynamic_stream_select_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = [ + read.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Evaluates a dynamic `stream_socket_accept()` call when `$peer_name` is writable. +fn eval_dynamic_stream_socket_accept_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "timeout", "peer_name"], + evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let Some(peer_name) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = peer_name.ref_target.as_ref() else { + return Ok(None); + }; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + if let Some(peer_name) = peer_name { + let peer_name = values.string(&peer_name)?; + eval_write_direct_ref_target( + target, + peer_name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Evaluates a dynamic `stream_socket_recvfrom()` call when `$address` is writable. +fn eval_dynamic_stream_socket_recvfrom_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let Some(address) = optional_evaluated_ref_arg(&bound, 3) else { + return Ok(None); + }; + let Some(target) = address.ref_target.as_ref() else { + return Ok(None); + }; + let (result, address) = + eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; + if let Some(address) = address { + let address = values.string(&address)?; + eval_write_direct_ref_target( + target, + address, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Returns a writable target for an optional dynamic by-reference argument. +fn optional_dynamic_ref_target( + arg: Option<&EvaluatedCallArg>, +) -> Result, EvalStatus> { + match arg { + Some(arg) => arg.ref_target.as_ref().map(Some).ok_or(EvalStatus::RuntimeFatal), + None => Ok(None), + } +} + +/// Binds already evaluated arguments while preserving by-reference target metadata. +pub(in crate::interpreter) fn bind_evaluated_ref_builtin_args( + params: &[&str], + evaluated_args: &[EvaluatedCallArg], + variadic_last: bool, +) -> Result<(Vec>, Vec), EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut variadic_args = Vec::new(); + let mut next_positional = 0; + let mut saw_named = false; + + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + saw_named = true; + let Some(index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[index] = Some(arg.clone()); + continue; + } + + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + if variadic_last && next_positional >= params.len().saturating_sub(1) { + variadic_args.push(arg.clone()); + next_positional += 1; + continue; + } + if next_positional >= params.len() { + return Err(EvalStatus::RuntimeFatal); + } + if bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.clone()); + next_positional += 1; + } + + if variadic_last { + let variadic_index = params.len().saturating_sub(1); + if let Some(named_variadic) = bound_args[variadic_index].take() { + variadic_args.insert(0, named_variadic); + } + } + + Ok((bound_args, variadic_args)) +} + +/// Returns a required already evaluated argument by bound parameter index. +pub(in crate::interpreter) fn required_evaluated_ref_arg( + bound_args: &[Option], + index: usize, +) -> Result<&EvaluatedCallArg, EvalStatus> { + bound_args + .get(index) + .and_then(Option::as_ref) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns an optional already evaluated argument by bound parameter index. +pub(in crate::interpreter) fn optional_evaluated_ref_arg( + bound_args: &[Option], + index: usize, +) -> Option<&EvaluatedCallArg> { + bound_args.get(index).and_then(Option::as_ref) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs new file mode 100644 index 0000000000..e7979d4444 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -0,0 +1,191 @@ +//! Purpose: +//! Groups builtin registry lookup, argument binding, callable dispatch, and +//! evaluated-argument builtin dispatch. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core eval call paths. +//! +//! Key details: +//! - The large by-value dispatch match is isolated from argument planning and +//! callable normalization. + +use std::collections::HashMap; +use std::sync::OnceLock; + +use super::super::*; +use super::spec::EvalBuiltinSpec; + +mod binding; +mod callable; +mod callable_validation; +mod dispatch; +mod dynamic_mutation; +mod names; +mod signature; + +pub(in crate::interpreter) use binding::*; +pub(in crate::interpreter) use callable::*; +pub(in crate::interpreter) use callable_validation::*; +pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use dynamic_mutation::*; +pub(in crate::interpreter) use names::*; +pub(in crate::interpreter) use signature::*; + +/// Lazy registry of builtins migrated to declarative eval specs. +struct DeclaredBuiltinRegistry { + /// Case-insensitive lookup keyed by canonical lowercase PHP builtin name. + by_name: HashMap, + /// Stable ordered list of registered canonical names. + names: Vec<&'static str>, +} + +/// Global eval builtin registry built from inventory submissions. +static DECLARED_BUILTIN_REGISTRY: OnceLock = OnceLock::new(); + +/// Builds the declarative registry and rejects duplicate builtin names. +fn build_declared_builtin_registry() -> DeclaredBuiltinRegistry { + let mut by_name = HashMap::new(); + let mut names = Vec::new(); + + for spec in inventory::iter:: { + validate_declared_builtin_spec(spec); + let key = spec.name.to_ascii_lowercase(); + if by_name.insert(key, spec).is_some() { + panic!( + "duplicate eval builtin name registered in inventory: \"{}\"", + spec.name + ); + } + names.push(spec.name); + } + + names.sort_unstable(); + DeclaredBuiltinRegistry { by_name, names } +} + +/// Validates static spec invariants before the registry is exposed. +fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { + let expected_param_names = spec.params.len() + usize::from(spec.variadic.is_some()); + assert_eq!( + expected_param_names, + spec.param_names.len(), + "eval builtin {} has mismatched params and param_names", + spec.name + ); + for (param, name) in spec.params.iter().zip(spec.param_names.iter()) { + assert_eq!( + param.name, *name, + "eval builtin {} has a param_names entry out of sync", + spec.name + ); + if param.by_ref { + assert!( + spec.by_ref_params.contains(¶m.name), + "eval builtin {} marks {} by-ref without listing it", + spec.name, + param.name + ); + } + } + for by_ref_name in spec.by_ref_params { + assert!( + spec.params + .iter() + .any(|param| param.name == *by_ref_name && param.by_ref), + "eval builtin {} lists {} as by-ref without marking the parameter", + spec.name, + by_ref_name + ); + } + if let Some(variadic) = spec.variadic { + assert_eq!( + spec.param_names.last().copied(), + Some(variadic), + "eval builtin {} has a variadic name out of sync", + spec.name + ); + } + if let Some(required_param_count) = spec.required_param_count { + assert!( + required_param_count <= spec.params.len(), + "eval builtin {} has a required parameter count larger than its parameter list", + spec.name + ); + } + let _ = spec.area(); +} + +/// Returns the declarative registry, initializing it on first access. +fn declared_builtin_registry() -> &'static DeclaredBuiltinRegistry { + DECLARED_BUILTIN_REGISTRY.get_or_init(build_declared_builtin_registry) +} + +/// Looks up a declaratively migrated eval builtin with PHP case-insensitive matching. +pub(in crate::interpreter) fn eval_declared_builtin_spec( + name: &str, +) -> Option<&'static EvalBuiltinSpec> { + let key = name.trim_start_matches('\\').to_ascii_lowercase(); + declared_builtin_registry().by_name.get(&key).copied() +} + +/// Returns whether a PHP-visible builtin has migrated into the declarative registry. +pub(in crate::interpreter) fn eval_declared_builtin_exists(name: &str) -> bool { + eval_declared_builtin_spec(name).is_some() +} + +/// Returns stable canonical names for builtins in the declarative registry. +pub(in crate::interpreter) fn eval_declared_builtin_function_names() -> &'static [&'static str] { + declared_builtin_registry().names.as_slice() +} + +/// Returns PHP parameter names for a declaratively migrated builtin. +pub(in crate::interpreter) fn eval_declared_builtin_param_names( + name: &str, +) -> Option<&'static [&'static str]> { + eval_declared_builtin_spec(name).map(|spec| spec.param_names) +} + +/// Returns a default value from a declaratively migrated builtin spec. +pub(in crate::interpreter) fn eval_declared_builtin_default_value( + name: &str, + param_index: usize, +) -> Option { + eval_declared_builtin_spec(name).and_then(|spec| spec.default_value(param_index)) +} + +/// Dispatches a declaratively migrated builtin from unevaluated positional expressions. +pub(in crate::interpreter) fn eval_declared_builtin_direct_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(spec) = eval_declared_builtin_spec(name) else { + return Ok(None); + }; + let Some(hook) = spec.direct else { + return Ok(None); + }; + hook.call(spec.name, args, context, scope, values).map(Some) +} + +/// Dispatches a declaratively migrated builtin from already evaluated argument cells. +pub(in crate::interpreter) fn eval_declared_builtin_values_call( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(spec) = eval_declared_builtin_spec(name) else { + return Ok(None); + }; + let Some(hook) = spec.values else { + return Ok(None); + }; + hook.call(spec.name, evaluated_args, context, values) + .map(Some) +} + +#[cfg(test)] +mod tests; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs new file mode 100644 index 0000000000..50bff7f4ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Builtin existence helpers used by eval function probes. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Declarative specs are the source of truth for PHP-visible eval builtin names. +//! - Lookup callers pass canonical lowercase PHP symbol names. + +use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; + +/// Returns the eval interpreter's PHP-visible builtin names. +pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'static [&'static str] { + eval_declared_builtin_function_names() +} + +/// Returns true for PHP-visible builtin names implemented by the eval interpreter. +pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { + eval_declared_builtin_exists(name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs new file mode 100644 index 0000000000..8de9ac53fb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Signature-shape metadata derived from PHP-visible eval builtin declarations. +//! +//! Called from: +//! - `crate::interpreter::builtin_metadata` +//! - builtin registry tests and argument binding audits. +//! +//! Key details: +//! - Declarative specs are the only signature source after builtin migration. +//! - Default values let named calls skip optional parameters without changing +//! positional semantics. + +use super::{eval_declared_builtin_default_value, eval_declared_builtin_spec}; + +/// Comparison-friendly shape for one eval builtin signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(in crate::interpreter) struct EvalBuiltinSignatureShape { + /// Number of leading parameters that must be supplied. + pub(in crate::interpreter) required_param_count: usize, + /// Number of parameters that have defaults. + pub(in crate::interpreter) default_param_count: usize, + /// Variadic parameter name when this builtin accepts extra arguments. + pub(in crate::interpreter) variadic: Option<&'static str>, + /// Parameter names that are passed by reference. + pub(in crate::interpreter) by_ref_params: &'static [&'static str], +} + +/// Runtime-materializable default value for one eval builtin parameter. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(in crate::interpreter) enum EvalBuiltinDefaultValue { + /// PHP null default. + Null, + /// PHP boolean default. + Bool(bool), + /// PHP integer default. + Int(i64), + /// PHP float default. + Float(f64), + /// PHP string default represented as UTF-8 text. + String(&'static str), + /// PHP string default represented as raw bytes. + Bytes(&'static [u8]), + /// PHP empty indexed array default. + EmptyArray, +} + +/// Returns signature-shape metadata for one PHP-visible eval builtin. +pub(in crate::interpreter) fn eval_builtin_signature_shape( + name: &str, +) -> Option { + eval_declared_builtin_spec(name).map(|spec| { + EvalBuiltinSignatureShape { + required_param_count: spec.required_param_count(), + default_param_count: spec.default_param_count(), + variadic: spec.variadic, + by_ref_params: spec.by_ref_param_names(), + } + }) +} + +/// Returns the concrete default value for one optional builtin parameter. +pub(in crate::interpreter) fn eval_builtin_default_value( + name: &str, + param_index: usize, +) -> Option { + eval_declared_builtin_default_value(name, param_index) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs new file mode 100644 index 0000000000..832db3c675 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs @@ -0,0 +1,20 @@ +//! Purpose: +//! Test module wiring for eval builtin registry discovery and metadata checks. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Focused child modules keep large registry assertions near their area while +//! still sharing access to private registry helpers. + +mod direct_hooks; +mod exposure; +mod metadata_core; +mod metadata_filesystem; +mod metadata_misc; +mod metadata_regex; +mod metadata_streams; +mod metadata_time_and_env; + +use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs new file mode 100644 index 0000000000..fe28c54bc5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Registry direct-hook invariant tests for source-sensitive builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies direct-call fallback is needed only for source-sensitive pre-dispatched builtins. + #[test] + fn declared_builtin_registry_marks_only_pre_dispatched_builtins_without_direct_hooks() { + let mut without_direct: Vec<&str> = eval_declared_builtin_function_names() + .iter() + .copied() + .filter(|name| { + eval_declared_builtin_spec(name) + .is_some_and(|spec| spec.direct.is_none()) + }) + .collect(); + without_direct.sort_unstable(); + + assert_eq!( + without_direct, + [ + "array_pop", + "array_push", + "array_shift", + "array_splice", + "array_unshift", + "array_walk", + "arsort", + "asort", + "flock", + "fsockopen", + "krsort", + "ksort", + "natcasesort", + "natsort", + "pfsockopen", + "rsort", + "settype", + "shuffle", + "sort", + "stream_select", + "stream_socket_accept", + "stream_socket_recvfrom", + "uasort", + "uksort", + "usort", + ] + ); + } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs new file mode 100644 index 0000000000..bdb1b9d621 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs @@ -0,0 +1,295 @@ +//! Purpose: +//! Registry exposure tests for representative migrated eval builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies representative migrated builtins are present in the declarative registry. + #[test] + fn declared_builtin_registry_exposes_representative_migrated_builtins() { + for name in [ + "abs", + "acos", + "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", + "array_key_exists", + "array_keys", + "array_intersect", + "array_intersect_key", + "array_map", + "array_merge", + "array_pop", + "array_push", + "array_reduce", + "array_reverse", + "array_shift", + "array_splice", + "array_sum", + "array_unshift", + "array_walk", + "arsort", + "asort", + "basename", + "boolval", + "base64_encode", + "bin2hex", + "buffer_new", + "call_user_func", + "call_user_func_array", + "checkdate", + "chdir", + "chgrp", + "chmod", + "chown", + "class_alias", + "class_exists", + "closedir", + "clearstatcache", + "copy", + "count", + "ctype_alpha", + "date", + "date_default_timezone_get", + "date_default_timezone_set", + "define", + "defined", + "dirname", + "disk_free_space", + "disk_total_space", + "die", + "exec", + "exit", + "function_exists", + "explode", + "file", + "file_exists", + "file_get_contents", + "file_put_contents", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", + "fclose", + "fdatasync", + "feof", + "fflush", + "fgetc", + "fgets", + "floatval", + "fnmatch", + "fpassthru", + "fread", + "fseek", + "fstat", + "ftruncate", + "fsync", + "ftell", + "fwrite", + "getdate", + "get_class", + "getcwd", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", + "gettype", + "gmdate", + "gmmktime", + "glob", + "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", + "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", + "header", + "hex2bin", + "htmlspecialchars", + "hrtime", + "http_response_code", + "implode", + "inet_ntop", + "inet_pton", + "intval", + "interface_exists", + "iterator_apply", + "iterator_count", + "iterator_to_array", + "is_array", + "is_bool", + "is_callable", + "is_dir", + "is_double", + "is_executable", + "is_file", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_link", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_readable", + "is_real", + "is_resource", + "is_scalar", + "is_string", + "is_subclass_of", + "is_writable", + "is_writeable", + "ip2long", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", + "krsort", + "ksort", + "lchgrp", + "lchown", + "link", + "linkinfo", + "localtime", + "log", + "long2ip", + "lstat", + "microtime", + "md5", + "min", + "mkdir", + "mktime", + "mt_rand", + "natcasesort", + "natsort", + "nl2br", + "number_format", + "opendir", + "pathinfo", + "pclose", + "passthru", + "php_uname", + "phpversion", + "popen", + "print_r", + "printf", + "ptr_null", + "ptr_read16", + "ptr_write_string", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", + "putenv", + "rand", + "random_int", + "range", + "rawurlencode", + "readdir", + "readfile", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", + "rename", + "rewind", + "rewinddir", + "rmdir", + "scandir", + "sha1", + "shell_exec", + "settype", + "shuffle", + "sleep", + "sort", + "sprintf", + "spl_autoload", + "spl_object_id", + "sscanf", + "stat", + "stream_copy_to_stream", + "stream_get_contents", + "stream_get_filters", + "stream_get_line", + "stream_get_meta_data", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", + "stream_isatty", + "stream_resolve_include_path", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", + "stream_supports_lock", + "str_contains", + "str_pad", + "str_replace", + "strlen", + "str_repeat", + "strrev", + "strtotime", + "substr", + "system", + "symlink", + "sys_get_temp_dir", + "tempnam", + "time", + "tmpfile", + "touch", + "trait_exists", + "trim", + "strval", + "uasort", + "uksort", + "umask", + "unlink", + "unset", + "usleep", + "usort", + "var_dump", + "vprintf", + "vsprintf", + "wordwrap", + ] { + assert!( + eval_declared_builtin_exists(name), + "{name} should be registered declaratively" + ); + } + } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs new file mode 100644 index 0000000000..3b2da14758 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs @@ -0,0 +1,172 @@ +//! Purpose: +//! Registry metadata tests for core, arrays, class, callable, and raw-memory builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_core_metadata() { + assert_eq!( + eval_declared_builtin_param_names("count"), + Some(["value", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("count", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("strlen"), + Some(["string"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_finite"), + Some(["num"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_object"), + Some(["value"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("log"), + Some(["num", "base"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("log", 1), + Some(EvalBuiltinDefaultValue::Float(std::f64::consts::E)) + ); + assert_eq!( + eval_declared_builtin_param_names("max"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("array_map"), + Some(["callback", "array", "arrays"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_map").map(|shape| shape.variadic), + Some(Some("arrays")) + ); + assert_eq!( + eval_declared_builtin_param_names("array_filter"), + Some(["array", "callback", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("iterator_to_array"), + Some(["iterator", "preserve_keys"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("iterator_to_array", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_spec("array_pop").map(EvalBuiltinSpec::by_ref_param_names), + Some(["array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("array_push"), + Some(["array", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_push").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 3), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); + assert_eq!( + eval_declared_builtin_param_names("settype"), + Some(["var", "type"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("settype").map(EvalBuiltinSpec::by_ref_param_names), + Some(["var"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("print_r"), + Some(["value", "return"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("print_r", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("var_dump"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("var_dump").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_param_names("class_exists"), + Some(["class", "autoload"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("class_exists", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_param_names("get_class"), + Some(["object"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("get_class", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("is_callable"), + Some(["value", "syntax_only", "callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("is_callable").map(EvalBuiltinSpec::by_ref_param_names), + Some(["callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("is_callable", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_builtin_signature_shape("isset").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("spl_autoload_register"), + Some(["callback", "throw", "prepend"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("spl_autoload_register", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("buffer_new"), + Some(["length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_read_string"), + Some(["pointer", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_sizeof"), + Some(["type"].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs new file mode 100644 index 0000000000..0d5073d556 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Registry metadata tests for filesystem path, file, and directory builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_filesystem_metadata() { assert_eq!( + eval_declared_builtin_param_names("basename"), + Some(["path", "suffix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("basename", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_default_value("dirname", 1), + Some(EvalBuiltinDefaultValue::Int(1)) + ); + assert_eq!( + eval_declared_builtin_default_value("fnmatch", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("pathinfo", 1), + Some(EvalBuiltinDefaultValue::Int(15)) + ); + assert_eq!( + eval_declared_builtin_param_names("disk_free_space"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getcwd"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("glob"), + Some(["pattern"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("linkinfo"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_resolve_include_path"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath_cache_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("sys_get_temp_dir"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_exists"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_get_contents"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_put_contents"), + Some(["filename", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readfile"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filemtime"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filesize"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_writable"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stat"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chdir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chmod"), + Some(["filename", "permissions"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chown"), + Some(["filename", "user"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chgrp"), + Some(["filename", "group"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("link"), + Some(["target", "link"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rename"), + Some(["from", "to"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("scandir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tempnam"), + Some(["directory", "prefix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("popen"), + Some(["command", "mode"].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs new file mode 100644 index 0000000000..ebd6c9dd0e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs @@ -0,0 +1,143 @@ +//! Purpose: +//! Registry metadata tests for string formatting, compression, hash, and stream-setting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_misc_metadata() { assert_eq!( + eval_declared_builtin_param_names("explode"), + Some(["separator", "string", "limit"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("explode", 2), + Some(EvalBuiltinDefaultValue::Int(i64::MAX)) + ); + assert_eq!( + eval_declared_builtin_param_names("implode"), + Some(["separator", "array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("implode", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_builtin_signature_shape("implode").map(|shape| shape.required_param_count), + Some(1) + ); + assert_eq!( + eval_declared_builtin_param_names("sprintf"), + Some(["format", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sprintf").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_param_names("sscanf"), + Some(["string", "format", "vars"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sscanf").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("vsprintf"), + Some(["format", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("gzcompress"), + Some(["data", "level"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("gzcompress", 1), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_default_value("gzinflate", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash"), + Some(["algo", "data", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_algos"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_init"), + Some(["algo", "flags", "key"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("md5"), + Some(["string", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("sha1", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_is_local"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_supports_lock"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_isatty"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_blocking"), + Some(["stream", "enable"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_chunk_size"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_read_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_write_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_timeout"), + Some(["stream", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_set_timeout", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("touch", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("umask", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs new file mode 100644 index 0000000000..6ded1e0b56 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Registry metadata tests for regex builtin signatures and defaults. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_regex_metadata() { assert_eq!( + eval_declared_builtin_param_names("preg_match"), + Some(["pattern", "subject", "matches", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match", 2), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match_all", 3), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_builtin_signature_shape("preg_match").map(|shape| shape.by_ref_params), + Some(["matches"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("preg_replace_callback"), + Some(["pattern", "callback", "subject"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_split", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs new file mode 100644 index 0000000000..d930852ecb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs @@ -0,0 +1,181 @@ +//! Purpose: +//! Registry metadata tests for stream and stream-socket builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_stream_metadata() { assert_eq!( + eval_declared_builtin_param_names("pclose"), + Some(["handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("opendir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("closedir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readdir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rewinddir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tmpfile"), + Some([].as_slice()) + ); + for name in [ + "fclose", + "fgetc", + "fgets", + "feof", + "fflush", + "fpassthru", + "fsync", + "fdatasync", + "ftell", + "rewind", + "fstat", + "stream_get_meta_data", + ] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["stream"].as_slice()), + "{name} should declare one stream parameter" + ); + } + assert_eq!( + eval_declared_builtin_param_names("fread"), + Some(["stream", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fgetcsv"), + Some(["stream", "length", "separator"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fgetcsv", 2), + Some(EvalBuiltinDefaultValue::String(",")) + ); + assert_eq!( + eval_declared_builtin_param_names("flock"), + Some(["stream", "operation", "would_block"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("flock").map(|shape| shape.by_ref_params), + Some(["would_block"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fsockopen"), + Some(["hostname", "port", "error_code", "error_message", "timeout"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("fsockopen").map(|shape| shape.by_ref_params), + Some(["error_code", "error_message"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fwrite"), + Some(["stream", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fseek"), + Some(["stream", "offset", "whence"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fseek", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("ftruncate"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_copy_to_stream"), + Some(["from", "to", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 3), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_contents"), + Some(["stream", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_context_set_option"), + Some(["context", "wrapper_or_options", "option_name", "value"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_context_set_option") + .map(|shape| shape.required_param_count), + Some(2) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_line"), + Some(["stream", "length", "ending"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_line", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_select"), + Some(["read", "write", "except", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_select").map(|shape| shape.by_ref_params), + Some(["read", "write", "except"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_socket_recvfrom"), + Some(["socket", "length", "flags", "address"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_socket_recvfrom") + .map(|shape| shape.by_ref_params), + Some(["address"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_wrapper_register"), + Some(["protocol", "class", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("vfprintf"), + Some(["stream", "format", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_wrappers"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_transports"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_filters"), + Some([].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs new file mode 100644 index 0000000000..1d4ed6c9d1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs @@ -0,0 +1,159 @@ +//! Purpose: +//! Registry metadata tests for random, scalar formatting, JSON, environment, and time builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_time_and_env_metadata() { for name in ["rand", "mt_rand", "random_int"] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["min", "max"].as_slice()), + "{name} should declare min/max parameters" + ); + } + assert_eq!( + eval_declared_builtin_param_names("number_format"), + Some( + [ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ] + .as_slice() + ) + ); + assert_eq!( + eval_declared_builtin_param_names("ctype_alpha"), + Some(["text"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("str_repeat"), + Some(["string", "times"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("wordwrap"), + Some(["string", "width", "break", "cut_long_words"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("wordwrap", 2), + Some(EvalBuiltinDefaultValue::String("\n")) + ); + assert_eq!( + eval_declared_builtin_param_names("json_decode"), + Some(["json", "associative", "depth", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("json_decode", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("json_encode", 2), + Some(EvalBuiltinDefaultValue::Int(512)) + ); + assert_eq!( + eval_declared_builtin_param_names("json_last_error"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("date"), + Some(["format", "timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("date", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("date_default_timezone_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("header"), + Some(["header", "replace", "response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("http_response_code"), + Some(["response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("http_response_code", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("php_uname"), + Some(["mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("php_uname", 0), + Some(EvalBuiltinDefaultValue::String("a")) + ); + assert_eq!( + eval_declared_builtin_param_names("phpversion"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getenv"), + Some(["name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getservbyname"), + Some(["service", "protocol"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("call_user_func"), + Some(["callback", "args"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("exit"), + Some(["status"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("exit", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("getdate"), + Some(["timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hrtime"), + Some(["as_number"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hrtime", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("microtime"), + Some(["as_float"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("strtotime"), + Some(["datetime", "baseTimestamp"].as_slice()) + ); + assert_eq!(eval_declared_builtin_param_names("time"), Some([].as_slice())); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/common.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/common.rs new file mode 100644 index 0000000000..1080d4b525 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/common.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Common scalar conversion and checksum helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; + +/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. +pub(in crate::interpreter) fn eval_crc32_bytes(bytes: &[u8]) -> u32 { + let mut crc = 0xffff_ffff_u32; + for byte in bytes { + crc ^= u32::from(*byte); + for _ in 0..8 { + let mask = 0_u32.wrapping_sub(crc & 1); + crc = (crc >> 1) ^ (0xedb8_8320 & mask); + } + } + !crc +} + +/// Casts one eval value to PHP int and returns the scalar payload. +pub(in crate::interpreter) fn eval_int_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_int(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs new file mode 100644 index 0000000000..643f8a6483 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Shared numeric helper algorithms used by per-builtin math home files. +//! +//! Called from: +//! - `crate::interpreter::builtins::math::{min,max}`. +//! +//! Key details: +//! - Runtime cells remain opaque and ordering stays delegated to +//! `RuntimeValueOps::compare`. + +use super::super::super::*; + +/// Selects the smallest or largest evaluated cell using runtime comparison hooks. +pub(in crate::interpreter) fn eval_min_max_selected( + evaluated_args: &[RuntimeCellHandle], + op: EvalBinOp, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((&first, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut selected = first; + for candidate in rest { + let better = values.compare(op, *candidate, selected)?; + if values.truthy(better)? { + selected = *candidate; + } + } + Ok(selected) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs new file mode 100644 index 0000000000..faa83bd849 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Groups scalar helper functions that are still shared by eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - PHP-visible string builtin implementations live in `builtins::string` leaf +//! files; this module keeps cross-domain scalar helpers only. + +mod common; +mod math; +mod types; + +pub(in crate::interpreter) use common::*; +pub(in crate::interpreter) use math::*; +pub(in crate::interpreter) use types::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs new file mode 100644 index 0000000000..b787cdff2d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Shared scalar type helpers used by eval builtins and dynamic conversions. +//! +//! Called from: +//! - `crate::interpreter::builtins::types` and dynamic value conversion paths. +//! +//! Key details: +//! - PHP-visible symbol/introspection builtins live in their focused symbol +//! builtin files; this module keeps cross-domain scalar predicates only. + +use super::super::super::*; + +/// Returns the PHP-visible type name for a concrete eval runtime tag. +pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "integer", + EVAL_TAG_FLOAT => "double", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "boolean", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_OBJECT => "object", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "NULL", + _ => "NULL", + } +} + +/// Matches the static backend's legacy ASCII numeric-string scan. +pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let mut index = 0; + let mut consumed_digits = 0; + if bytes[index] == b'-' { + index += 1; + if index >= bytes.len() { + return false; + } + } + + while index < bytes.len() { + if bytes[index] == b'.' { + index += 1; + break; + } + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + while index < bytes.len() { + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + consumed_digits > 0 +} diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs new file mode 100644 index 0000000000..4ddd8c2a15 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -0,0 +1,152 @@ +//! Purpose: +//! Declarative builtin specifications for the eval interpreter. +//! Each spec owns PHP-visible metadata plus optional direct and evaluated-arg +//! dispatch hooks for one builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` lookup and metadata helpers. +//! - `eval_builtin!` submissions in per-builtin home files. +//! +//! Key details: +//! - Specs are collected with `inventory` to let builtin files register +//! themselves without growing a central match. +//! - Hook enums keep calls monomorphized over `RuntimeValueOps`. + +pub(in crate::interpreter) use super::hooks::{EvalDirectHook, EvalValuesHook}; +pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; + +/// Broad domain used to group eval builtin home files. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::interpreter) enum EvalArea { + /// Array and collection builtins. + Array, + /// Core callable, constant, process-control, and debug-output builtins. + Core, + /// Filesystem, path, and stream builtins. + Filesystem, + /// Formatting and display-oriented numeric builtins. + Formatting, + /// JSON encoding, decoding, validation, and error-state builtins. + Json, + /// Numeric and mathematical builtins. + Math, + /// Network, host, environment, and process builtins. + NetworkEnv, + /// PCRE-style regex builtins. + Regex, + /// Raw pointer and buffer extension builtins. + RawMemory, + /// String-processing builtins. + String, + /// Symbol, class metadata, SPL, and language-construct probes. + Symbols, + /// Date, time, and sleep builtins. + Time, + /// Scalar conversion and type-related builtins. + Types, +} + +impl EvalArea { + /// Returns the stable lowercase spelling used by documentation metadata. + pub(in crate::interpreter) fn name(self) -> &'static str { + match self { + EvalArea::Array => "array", + EvalArea::Core => "core", + EvalArea::Filesystem => "filesystem", + EvalArea::Formatting => "formatting", + EvalArea::Json => "json", + EvalArea::Math => "math", + EvalArea::NetworkEnv => "network_env", + EvalArea::Regex => "regex", + EvalArea::RawMemory => "raw_memory", + EvalArea::String => "string", + EvalArea::Symbols => "symbols", + EvalArea::Time => "time", + EvalArea::Types => "types", + } + } +} + +/// Parameter metadata for one eval builtin argument. +#[derive(Clone, Copy)] +pub(in crate::interpreter) struct EvalParamSpec { + /// PHP-visible parameter name. + pub(in crate::interpreter) name: &'static str, + /// Optional PHP default value. + pub(in crate::interpreter) default: Option, + /// Whether this parameter must bind to caller storage. + pub(in crate::interpreter) by_ref: bool, +} + +/// Static declaration for one PHP-visible eval builtin. +pub(in crate::interpreter) struct EvalBuiltinSpec { + /// Canonical lowercase PHP builtin name. + pub(in crate::interpreter) name: &'static str, + /// Builtin family used by the file layout. + pub(in crate::interpreter) area: EvalArea, + /// Parameter names in PHP call order. + pub(in crate::interpreter) param_names: &'static [&'static str], + /// Parameter metadata in PHP call order. + pub(in crate::interpreter) params: &'static [EvalParamSpec], + /// Variadic parameter name, when supported. + pub(in crate::interpreter) variadic: Option<&'static str>, + /// Parameter names that must bind by reference. + pub(in crate::interpreter) by_ref_params: &'static [&'static str], + /// Explicit required parameter count for non-trailing default shapes. + pub(in crate::interpreter) required_param_count: Option, + /// Direct expression-level dispatch hook. + pub(in crate::interpreter) direct: Option, + /// Evaluated-argument dispatch hook. + pub(in crate::interpreter) values: Option, + /// Workspace-relative path of the home file that declared this builtin + /// (captured via `file!()` at the `eval_builtin!` invocation site). + pub(in crate::interpreter) home_file: &'static str, +} + +impl EvalBuiltinSpec { + /// Returns this builtin's file-layout area. + pub(in crate::interpreter) fn area(&self) -> EvalArea { + self.area + } + + /// Returns the number of required leading parameters. + pub(in crate::interpreter) fn required_param_count(&self) -> usize { + if let Some(required_param_count) = self.required_param_count { + return required_param_count; + } + self.params + .iter() + .take_while(|param| param.default.is_none()) + .count() + } + + /// Returns the number of parameters that define defaults. + pub(in crate::interpreter) fn default_param_count(&self) -> usize { + let fixed_defaults = self + .params + .iter() + .filter(|param| param.default.is_some()) + .count(); + fixed_defaults + usize::from(self.variadic.is_some()) + } + + /// Returns by-reference parameter names, checking they agree with param flags in debug builds. + pub(in crate::interpreter) fn by_ref_param_names(&self) -> &'static [&'static str] { + debug_assert!(self + .params + .iter() + .filter(|param| param.by_ref) + .all(|param| self.by_ref_params.contains(¶m.name))); + self.by_ref_params + } + + /// Returns the default value for one PHP parameter slot. + pub(in crate::interpreter) fn default_value( + &self, + param_index: usize, + ) -> Option { + self.params.get(param_index).and_then(|param| param.default) + } +} + +inventory::collect!(EvalBuiltinSpec); diff --git a/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs new file mode 100644 index 0000000000..e28e6bd608 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Declarative eval registry entry for `addslashes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing slash escaping hook. + +eval_builtin! { + name: "addslashes", + area: String, + params: [string], + direct: Slashes, + values: Slashes, +} + +use super::super::super::*; + +/// Evaluates PHP `addslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_addslashes( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_addslashes_result(value, values) +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +pub(in crate::interpreter) fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs new file mode 100644 index 0000000000..d2326aba45 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs @@ -0,0 +1,96 @@ +//! Purpose: +//! Declarative eval registry entry for `base64_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing Base64 decode hook. + +eval_builtin! { + name: "base64_decode", + area: String, + params: [string], + direct: Base64Decode, + values: Base64Decode, +} + +use super::super::super::*; + +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +pub(in crate::interpreter) fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( + quartet: &[Option], + output: &mut Vec, +) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs new file mode 100644 index 0000000000..150203d42c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Declarative eval registry entry for `base64_encode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing Base64 encode hook. + +eval_builtin! { + name: "base64_encode", + area: String, + params: [string], + direct: Base64Encode, + values: Base64Encode, +} + +use super::super::super::*; + +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +pub(in crate::interpreter) fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs new file mode 100644 index 0000000000..d3e5955524 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Declarative eval registry entry for `bin2hex`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing hex encode hook. + +eval_builtin! { + name: "bin2hex", + area: String, + params: [string], + direct: Bin2Hex, + values: Bin2Hex, +} + +use super::super::super::*; + +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +pub(in crate::interpreter) fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chop.rs b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs new file mode 100644 index 0000000000..e3479d2026 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `chop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "chop", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} + +use super::super::super::*; + +/// Evaluates PHP `chop(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_chop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("chop", args, context, scope, values) +} + +/// Applies PHP `chop(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_chop_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("chop", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chr.rs b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs new file mode 100644 index 0000000000..f261b4e568 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Declarative eval registry entry for `chr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing byte-string hook. + +eval_builtin! { + name: "chr", + area: String, + params: [codepoint], + direct: Chr, + values: Chr, +} + +use super::super::super::*; + +/// Evaluates PHP's `chr(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +pub(in crate::interpreter) fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs new file mode 100644 index 0000000000..b7a5912f9f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Declarative eval registry entry for `crc32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing checksum hook. + +eval_builtin! { + name: "crc32", + area: String, + params: [string], + direct: Crc32, + values: Crc32, +} + +use super::super::super::*; +use super::super::eval_crc32_bytes; + +/// Evaluates PHP `crc32(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +pub(in crate::interpreter) fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs new file mode 100644 index 0000000000..96e9f48458 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_alnum`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_alnum", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} + +use super::super::super::*; + +/// Evaluates PHP `ctype_alnum(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_alnum( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_alnum", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_alnum(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_alnum_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_alnum", value, values) +} + +/// Evaluates a named PHP `ctype_*` predicate over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_named_result(name, value, values) +} + +/// Returns the PHP boolean result for one named ASCII `ctype_*` byte-string check. +pub(in crate::interpreter) fn eval_ctype_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +pub(in crate::interpreter) fn eval_ctype_byte_matches( + name: &str, + byte: u8, +) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs new file mode 100644 index 0000000000..e2bdeecdaa --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_alpha`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_alpha", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} + +use super::super::super::*; + +/// Evaluates PHP `ctype_alpha(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_alpha( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_alpha", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_alpha(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_alpha_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_alpha", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs new file mode 100644 index 0000000000..2b7946f1e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_digit`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_digit", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} + +use super::super::super::*; + +/// Evaluates PHP `ctype_digit(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_digit( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_digit", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_digit(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_digit_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_digit", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs new file mode 100644 index 0000000000..c82531743e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_space", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} + +use super::super::super::*; + +/// Evaluates PHP `ctype_space(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_space( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_space", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_space(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_space_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_space", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs new file mode 100644 index 0000000000..beac8a3f52 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Declarative eval registry entry for `explode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The current eval implementation supports the two-argument runtime form. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "explode", + area: String, + params: [separator, string, limit = EvalBuiltinDefaultValue::Int(i64::MAX)], + direct: StringSplitJoin, + values: StringSplitJoin, +} + +use super::super::super::*; + +/// Evaluates PHP `explode()` over separator and string expressions. +pub(in crate::interpreter) fn eval_builtin_explode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_explode_result(separator, string, values) +} + +/// Splits one PHP byte string into an indexed array using a non-empty separator. +pub(in crate::interpreter) fn eval_explode_result( + separator: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let separator = values.string_bytes(separator)?; + if separator.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let string = values.string_bytes(string)?; + let mut result = values.array_new(0)?; + let mut start = 0; + let mut index = 0_i64; + while let Some(found) = super::strstr::eval_find_subslice(&string, &separator, start) { + result = eval_push_explode_segment(result, index, &string[start..found], values)?; + start = found + separator.len(); + index += 1; + } + eval_push_explode_segment(result, index, &string[start..], values) +} + +/// Dispatches evaluated `explode()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_explode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values) +} + +/// Appends one split segment to an indexed `explode()` result array. +pub(in crate::interpreter) fn eval_push_explode_segment( + array: RuntimeCellHandle, + index: i64, + segment: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.string_bytes_value(segment)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs new file mode 100644 index 0000000000..c4ab645795 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Declarative eval registry entry for `grapheme_strrev`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the grapheme string reverse hook. + +eval_builtin! { + name: "grapheme_strrev", + area: String, + params: [string], + direct: GraphemeStrrev, + values: GraphemeStrrev, +} + +use super::super::super::*; +use unicode_segmentation::UnicodeSegmentation; + +/// Evaluates PHP `grapheme_strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_grapheme_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_grapheme_strrev_result(value, values) +} + +/// Reverses a materialized PHP string by grapheme cluster or returns false for invalid UTF-8. +pub(in crate::interpreter) fn eval_grapheme_strrev_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let Ok(source) = std::str::from_utf8(&bytes) else { + return values.bool_value(false); + }; + let reversed = source.graphemes(true).rev().collect::(); + values.string(&reversed) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs new file mode 100644 index 0000000000..1b52868baa --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs @@ -0,0 +1,148 @@ +//! Purpose: +//! Declarative eval registry entry for `gzcompress`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzcompress", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; +use flate2::read::{DeflateDecoder, ZlibDecoder}; +use flate2::write::{DeflateEncoder, ZlibEncoder}; +use flate2::Compression; +use std::io::{Read, Write}; + +/// Evaluates PHP `gzcompress(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzcompress( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzcompress", args, context, scope, values) +} + +/// Applies PHP `gzcompress(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzcompress_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzcompress", evaluated_args, values) +} + +/// Evaluates a named gzip/zlib builtin over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzip_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_gzip_named_result(name, &evaluated_args, values) +} + +/// Dispatches one materialized gzip/zlib builtin call. +pub(in crate::interpreter) fn eval_gzip_named_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let (data, option) = match evaluated_args { + [data] => (*data, None), + [data, option] => (*data, Some(*option)), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + match name { + "gzcompress" => eval_gz_encode(data, option, true, values), + "gzdeflate" => eval_gz_encode(data, option, false, values), + "gzuncompress" => eval_gz_decode(data, true, values), + "gzinflate" => eval_gz_decode(data, false, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes data as zlib-wrapped or raw-DEFLATE bytes. +fn eval_gz_encode( + data: Vec, + level: Option, + zlib_wrapped: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let compression = eval_gz_compression(level, values)?; + let compressed = if zlib_wrapped { + let mut encoder = ZlibEncoder::new(Vec::new(), compression); + eval_gz_write_all(&mut encoder, &data)?; + encoder.finish().map_err(|_| EvalStatus::RuntimeFatal)? + } else { + let mut encoder = DeflateEncoder::new(Vec::new(), compression); + eval_gz_write_all(&mut encoder, &data)?; + encoder.finish().map_err(|_| EvalStatus::RuntimeFatal)? + }; + values.string_bytes_value(&compressed) +} + +/// Decodes zlib-wrapped or raw-DEFLATE bytes and returns false on inflate errors. +fn eval_gz_decode( + data: Vec, + zlib_wrapped: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let decoded = if zlib_wrapped { + eval_gz_read(ZlibDecoder::new(data.as_slice())) + } else { + eval_gz_read(DeflateDecoder::new(data.as_slice())) + }; + match decoded { + Ok(decoded) => values.string_bytes_value(&decoded), + Err(_) => values.bool_value(false), + } +} + +/// Converts PHP's optional compression level to a flate2 compression value. +fn eval_gz_compression( + level: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(level) = level else { + return Ok(Compression::default()); + }; + let level = eval_int_value(level, values)?; + if level < 0 { + return Ok(Compression::default()); + } + let level = u32::try_from(level).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(Compression::new(level.min(9))) +} + +/// Writes all source bytes into a compression stream. +fn eval_gz_write_all( + encoder: &mut W, + data: &[u8], +) -> Result<(), EvalStatus> { + encoder + .write_all(data) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Reads all bytes from a decompression stream. +fn eval_gz_read(mut decoder: R) -> std::io::Result> { + let mut decoded = Vec::new(); + decoder.read_to_end(&mut decoded)?; + Ok(decoded) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs new file mode 100644 index 0000000000..726b7d7076 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzdeflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzdeflate", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzdeflate(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzdeflate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzdeflate", args, context, scope, values) +} + +/// Applies PHP `gzdeflate(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzdeflate_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzdeflate", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs new file mode 100644 index 0000000000..90a580e0dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzinflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzinflate", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzinflate(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzinflate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzinflate", args, context, scope, values) +} + +/// Applies PHP `gzinflate(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzinflate_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzinflate", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs new file mode 100644 index 0000000000..4b9b8b0aa1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzuncompress`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzuncompress", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzuncompress(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzuncompress( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzuncompress", args, context, scope, values) +} + +/// Applies PHP `gzuncompress(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzuncompress_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzuncompress", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash.rs new file mode 100644 index 0000000000..d89a56cb7f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash.rs @@ -0,0 +1,155 @@ +//! Purpose: +//! Declarative eval registry entry for `hash`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash", + area: String, + params: [algo, data, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `hash(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("hash", args, context, scope, values) +} + +/// Applies PHP `hash(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("hash", evaluated_args, values) +} + +/// Evaluates a named one-shot PHP hash builtin over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_one_shot_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_hash_one_shot_named_result(name, &evaluated_args, values) +} + +/// Computes the result for one-shot PHP hash digest builtins from evaluated args. +pub(in crate::interpreter) fn eval_hash_one_shot_named_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "md5" | "sha1" => { + let (data, binary) = match evaluated_args { + [data] => (*data, false), + [data, binary] => (*data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + eval_hash_digest_result(name.as_bytes(), &data, binary, values) + } + "hash" => { + let (algo, data, binary) = match evaluated_args { + [algo, data] => (*algo, *data, false), + [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + eval_hash_digest_result(&algo, &data, binary, values) + } + "hash_file" => { + let (algo, filename, binary) = match evaluated_args { + [algo, filename] => (*algo, *filename, false), + [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + super::hash_file::eval_hash_file_digest_result(algo, filename, binary, values) + } + "hash_hmac" => { + let (algo, data, key, binary) = match evaluated_args { + [algo, data, key] => (*algo, *data, *key, false), + [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + let key = values.string_bytes(key)?; + super::hash_hmac::eval_hash_hmac_digest_result(&algo, &data, &key, binary, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_digest_result( + algo: &[u8], + data: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hash(algo, data)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hash( + algo: &[u8], + data: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hash( + algo.as_ptr(), + algo.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Converts a crypto ABI digest length into an owned digest byte vector. +pub(in crate::interpreter) fn eval_crypto_digest_bytes( + len: isize, + output: &[u8; 64], +) -> Result, EvalStatus> { + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + if len > output.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(output[..len].to_vec()) +} + +/// Formats a raw digest using PHP's `$binary` flag convention. +pub(in crate::interpreter) fn eval_format_digest_result( + raw: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if binary { + return values.string_bytes_value(raw); + } + values.string(&super::bin2hex::eval_lower_hex_bytes(raw)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs new file mode 100644 index 0000000000..b37f874f33 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_algos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The static string-array helper is shared by list-returning string builtins. + +eval_builtin! { + name: "hash_algos", + area: String, + params: [], + direct: HashAlgos, + values: HashAlgos, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_algos()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +pub(in crate::interpreter) fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Dispatches evaluated `hash_algos()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_algos_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds one indexed PHP array from a static string slice. +pub(in crate::interpreter) fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs new file mode 100644 index 0000000000..d2167151d8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Hash context resources are owned by the eval context stream table. + +eval_builtin! { + name: "hash_copy", + area: String, + params: [context], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_copy($context)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_copy( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + eval_hash_copy_result(hash_context, context, values) +} + +/// Clones a materialized incremental hash context into a new resource. +pub(in crate::interpreter) fn eval_hash_copy_result( + hash_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + match context.stream_resources_mut().copy_hash_context(id) { + Some(copy_id) => values.resource(copy_id), + None => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated `hash_copy()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_copy_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_copy_result(*hash_context, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs new file mode 100644 index 0000000000..9edde5e003 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_equals`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the constant-time byte compare hook. + +eval_builtin! { + name: "hash_equals", + area: String, + params: [known_string, user_string], + direct: HashEquals, + values: HashEquals, +} + +use super::super::super::*; + +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +pub(in crate::interpreter) fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs new file mode 100644 index 0000000000..1ca2a15280 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_file", + area: String, + params: [algo, filename, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `hash_file(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("hash_file", args, context, scope, values) +} + +/// Applies PHP `hash_file(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_file_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("hash_file", evaluated_args, values) +} + +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +pub(in crate::interpreter) fn eval_hash_file_digest_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => super::hash::eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs new file mode 100644 index 0000000000..8da01d7beb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_final`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Finalizing consumes the hash context resource from the eval stream table. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_final", + area: String, + params: [context, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_final($context, $binary = false)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_final( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let hash_context = eval_expr(&args[0], context, scope, values)?; + let binary = match args.get(1) { + Some(binary) => { + let binary = eval_expr(binary, context, scope, values)?; + values.truthy(binary)? + } + None => false, + }; + eval_hash_final_result(hash_context, binary, context, values) +} + +/// Finalizes a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_final_result( + hash_context: RuntimeCellHandle, + binary: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + let raw = context + .stream_resources_mut() + .finalize_hash_context(id) + .ok_or(EvalStatus::RuntimeFatal)?; + super::hash::eval_format_digest_result(&raw, binary, values) +} + +/// Dispatches evaluated `hash_final()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_final_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [hash_context] => eval_hash_final_result(*hash_context, false, context, values), + [hash_context, binary] => { + let binary = values.truthy(*binary)?; + eval_hash_final_result(*hash_context, binary, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs new file mode 100644 index 0000000000..6702542560 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_hmac`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_hmac", + area: String, + params: [algo, data, key, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_hmac(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_hmac( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("hash_hmac", args, context, scope, values) +} + +/// Applies PHP `hash_hmac(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_hmac_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("hash_hmac", evaluated_args, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_hmac_digest_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + super::hash::eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hmac( + algo: &[u8], + data: &[u8], + key: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + super::hash::eval_crypto_digest_bytes(len, &output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs new file mode 100644 index 0000000000..03b999f55d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_init`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Runtime resources are stored in the eval context stream table. +//! - Optional HMAC parameters remain metadata-only for current eval behavior. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_init", + area: String, + params: [ + algo, + flags = EvalBuiltinDefaultValue::Int(0), + key = EvalBuiltinDefaultValue::String(""), + ], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_init($algo)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_init( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [algo] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let algo = eval_expr(algo, context, scope, values)?; + eval_hash_init_result(algo, context, values) +} + +/// Opens an incremental hash context resource. +pub(in crate::interpreter) fn eval_hash_init_result( + algo: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + match context.stream_resources_mut().open_hash_context(&algo) { + Some(id) => values.resource(id), + None => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated `hash_init()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_init_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [algo] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_init_result(*algo, context, values) +} + +/// Converts a runtime resource cell into eval's zero-based hash context id. +pub(in crate::interpreter) fn eval_hash_context_resource_id( + hash_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(hash_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(hash_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs new file mode 100644 index 0000000000..51166702cf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_update`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Hash context resources are owned by the eval context stream table. + +eval_builtin! { + name: "hash_update", + area: String, + params: [context, data], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_update($context, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_update( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_hash_update_result(hash_context, data, context, values) +} + +/// Feeds data into a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_update_result( + hash_context: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + let data = values.string_bytes(data)?; + values.bool_value(context.stream_resources_mut().update_hash_context(id, &data)) +} + +/// Dispatches evaluated `hash_update()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_update_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_update_result(*hash_context, *data, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs new file mode 100644 index 0000000000..14dccc0fb0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Declarative eval registry entry for `hex2bin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing hex decode hook. + +eval_builtin! { + name: "hex2bin", + area: String, + params: [string], + direct: Hex2Bin, + values: Hex2Bin, +} + +use super::super::super::*; + +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +pub(in crate::interpreter) fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs new file mode 100644 index 0000000000..e49b76c3fc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `html_entity_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. + +eval_builtin! { + name: "html_entity_decode", + area: String, + params: [string], + direct: HtmlEntity, + values: HtmlEntity, +} + +use super::super::super::*; + +/// Evaluates PHP `html_entity_decode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_html_entity_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("html_entity_decode", args, context, scope, values) +} + +/// Applies PHP `html_entity_decode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_html_entity_decode_value_result(value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs new file mode 100644 index 0000000000..b5b7f79620 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Declarative eval registry entry for `htmlentities`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. + +eval_builtin! { + name: "htmlentities", + area: String, + params: [ + string, + flags = EvalBuiltinDefaultValue::Int(11), + encoding = EvalBuiltinDefaultValue::String("UTF-8"), + ], + direct: HtmlEntity, + values: HtmlEntity, +} + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +/// Evaluates PHP `htmlentities(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_htmlentities( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("htmlentities", args, context, scope, values) +} + +/// Applies PHP `htmlentities(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_htmlentities_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_htmlspecialchars_result(value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs new file mode 100644 index 0000000000..2efbbadded --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs @@ -0,0 +1,128 @@ +//! Purpose: +//! Declarative eval registry entry for `htmlspecialchars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. + +eval_builtin! { + name: "htmlspecialchars", + area: String, + params: [ + string, + flags = EvalBuiltinDefaultValue::Int(11), + encoding = EvalBuiltinDefaultValue::String("UTF-8"), + ], + direct: HtmlEntity, + values: HtmlEntity, +} + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +/// Evaluates PHP `htmlspecialchars(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_htmlspecialchars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("htmlspecialchars", args, context, scope, values) +} + +/// Evaluates a named HTML entity encode/decode builtin over one string expression. +/// The encoders accept optional flags/encoding arguments; like the static +/// runtime they are evaluated but have no effect (ENT_QUOTES behaviour). +pub(in crate::interpreter) fn eval_builtin_html_entity_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let accepts_options = matches!(name, "htmlspecialchars" | "htmlentities"); + let value = match args { + [value] => value, + [value, _] | [value, _, _] if accepts_options => value, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let value = eval_expr(value, context, scope, values)?; + for extra in &args[1..] { + eval_expr(extra, context, scope, values)?; + } + eval_html_entity_named_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +pub(in crate::interpreter) fn eval_html_entity_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_value_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +pub(in crate::interpreter) fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +pub(in crate::interpreter) fn eval_html_entity_decode_value_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/implode.rs b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs new file mode 100644 index 0000000000..94b37983e8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Declarative eval registry entry for `implode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The current eval implementation supports the two-argument runtime form. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "implode", + area: String, + params: [separator = EvalBuiltinDefaultValue::Null, array], + required: 1, + direct: StringSplitJoin, + values: StringSplitJoin, +} + +use super::super::super::*; + +/// Evaluates PHP `implode()` over separator and array expressions. +pub(in crate::interpreter) fn eval_builtin_implode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_implode_result(separator, array, values) +} + +/// Joins array values in eval iteration order using PHP string conversion. +pub(in crate::interpreter) fn eval_implode_result( + separator: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let separator = values.string_bytes(separator)?; + let len = values.array_len(array)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.extend_from_slice(&separator); + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let value = values.string_bytes(value)?; + output.extend_from_slice(&value); + } + values.string_bytes_value(&output) +} + +/// Dispatches evaluated `implode()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_implode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs new file mode 100644 index 0000000000..bd3b6c0789 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `lcfirst`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-case hook. + +eval_builtin! { + name: "lcfirst", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} + +use super::super::super::*; + +/// Evaluates PHP `lcfirst(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_lcfirst( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("lcfirst", args, context, scope, values) +} + +/// Applies PHP `lcfirst(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_lcfirst_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("lcfirst", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs new file mode 100644 index 0000000000..e41668063b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `ltrim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "ltrim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} + +use super::super::super::*; + +/// Evaluates PHP `ltrim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_ltrim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("ltrim", args, context, scope, values) +} + +/// Applies PHP `ltrim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_ltrim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("ltrim", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/md5.rs b/crates/elephc-magician/src/interpreter/builtins/string/md5.rs new file mode 100644 index 0000000000..6814eef643 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/md5.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `md5`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "md5", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `md5(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_md5( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("md5", args, context, scope, values) +} + +/// Applies PHP `md5(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_md5_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("md5", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs new file mode 100644 index 0000000000..944eb30326 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -0,0 +1,154 @@ +//! Purpose: +//! Per-builtin string, hash, encoding, compression, and stream-introspection eval +//! builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading and re-exports. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own the matching +//! eval implementation or delegate to the closest builtin owner for shared code. + +mod addslashes; +mod base64_decode; +mod base64_encode; +mod bin2hex; +mod chop; +mod chr; +mod crc32; +mod ctype_alnum; +mod ctype_alpha; +mod ctype_digit; +mod ctype_space; +mod explode; +mod grapheme_strrev; +mod gzcompress; +mod gzdeflate; +mod gzinflate; +mod gzuncompress; +mod hash; +mod hash_algos; +mod hash_copy; +mod hash_equals; +mod hash_file; +mod hash_final; +mod hash_hmac; +mod hash_init; +mod hash_update; +mod hex2bin; +mod html_entity_decode; +mod htmlentities; +mod htmlspecialchars; +mod implode; +mod lcfirst; +mod ltrim; +mod md5; +mod nl2br; +mod ord; +mod rawurldecode; +mod rawurlencode; +mod rtrim; +mod sha1; +mod str_contains; +mod str_ends_with; +mod str_ireplace; +mod str_pad; +mod str_repeat; +mod str_replace; +mod str_split; +mod str_starts_with; +mod strcasecmp; +mod strcmp; +mod stream_get_filters; +mod stream_get_transports; +mod stream_get_wrappers; +mod stream_is_local; +mod stream_supports_lock; +mod stripslashes; +mod strlen; +mod strpos; +mod strrev; +mod strrpos; +mod strstr; +mod strtolower; +mod strtoupper; +mod substr; +mod substr_replace; +mod trim; +mod ucfirst; +mod ucwords; +mod urldecode; +mod urlencode; +mod wordwrap; + +pub(in crate::interpreter) use addslashes::*; +pub(in crate::interpreter) use base64_decode::*; +pub(in crate::interpreter) use base64_encode::*; +pub(in crate::interpreter) use bin2hex::*; +pub(in crate::interpreter) use chop::*; +pub(in crate::interpreter) use chr::*; +pub(in crate::interpreter) use crc32::*; +pub(in crate::interpreter) use ctype_alnum::*; +pub(in crate::interpreter) use ctype_alpha::*; +pub(in crate::interpreter) use ctype_digit::*; +pub(in crate::interpreter) use ctype_space::*; +pub(in crate::interpreter) use explode::*; +pub(in crate::interpreter) use grapheme_strrev::*; +pub(in crate::interpreter) use gzcompress::*; +pub(in crate::interpreter) use gzdeflate::*; +pub(in crate::interpreter) use gzinflate::*; +pub(in crate::interpreter) use gzuncompress::*; +pub(in crate::interpreter) use hash::*; +pub(in crate::interpreter) use hash_algos::*; +pub(in crate::interpreter) use hash_copy::*; +pub(in crate::interpreter) use hash_equals::*; +pub(in crate::interpreter) use hash_file::*; +pub(in crate::interpreter) use hash_final::*; +pub(in crate::interpreter) use hash_hmac::*; +pub(in crate::interpreter) use hash_init::*; +pub(in crate::interpreter) use hash_update::*; +pub(in crate::interpreter) use hex2bin::*; +pub(in crate::interpreter) use html_entity_decode::*; +pub(in crate::interpreter) use htmlentities::*; +pub(in crate::interpreter) use htmlspecialchars::*; +pub(in crate::interpreter) use implode::*; +pub(in crate::interpreter) use lcfirst::*; +pub(in crate::interpreter) use ltrim::*; +pub(in crate::interpreter) use md5::*; +pub(in crate::interpreter) use nl2br::*; +pub(in crate::interpreter) use ord::*; +pub(in crate::interpreter) use rawurldecode::*; +pub(in crate::interpreter) use rawurlencode::*; +pub(in crate::interpreter) use rtrim::*; +pub(in crate::interpreter) use sha1::*; +pub(in crate::interpreter) use str_contains::*; +pub(in crate::interpreter) use str_ends_with::*; +pub(in crate::interpreter) use str_ireplace::*; +pub(in crate::interpreter) use str_pad::*; +pub(in crate::interpreter) use str_repeat::*; +pub(in crate::interpreter) use str_replace::*; +pub(in crate::interpreter) use str_split::*; +pub(in crate::interpreter) use str_starts_with::*; +pub(in crate::interpreter) use strcasecmp::*; +pub(in crate::interpreter) use strcmp::*; +pub(in crate::interpreter) use stream_get_filters::*; +pub(in crate::interpreter) use stream_get_transports::*; +pub(in crate::interpreter) use stream_get_wrappers::*; +pub(in crate::interpreter) use stream_is_local::*; +pub(in crate::interpreter) use stream_supports_lock::*; +pub(in crate::interpreter) use stripslashes::*; +pub(in crate::interpreter) use strlen::*; +pub(in crate::interpreter) use strpos::*; +pub(in crate::interpreter) use strrev::*; +pub(in crate::interpreter) use strrpos::*; +pub(in crate::interpreter) use strstr::*; +pub(in crate::interpreter) use strtolower::*; +pub(in crate::interpreter) use strtoupper::*; +pub(in crate::interpreter) use substr::*; +pub(in crate::interpreter) use substr_replace::*; +pub(in crate::interpreter) use trim::*; +pub(in crate::interpreter) use ucfirst::*; +pub(in crate::interpreter) use ucwords::*; +pub(in crate::interpreter) use urldecode::*; +pub(in crate::interpreter) use urlencode::*; +pub(in crate::interpreter) use wordwrap::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs new file mode 100644 index 0000000000..44e39078e8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Declarative eval registry entry for `nl2br`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the newline-to-break hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "nl2br", + area: String, + params: [string, use_xhtml = EvalBuiltinDefaultValue::Bool(true)], + direct: Nl2br, + values: Nl2br, +} + +use super::super::super::*; + +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +pub(in crate::interpreter) fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +pub(in crate::interpreter) fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ord.rs b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs new file mode 100644 index 0000000000..beff5b6db9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Declarative eval registry entry for `ord`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing byte introspection hook. + +eval_builtin! { + name: "ord", + area: String, + params: [character], + direct: Ord, + values: Ord, +} + +use super::super::super::*; + +/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. +pub(in crate::interpreter) fn eval_builtin_ord( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ord_result(value, values) +} + +/// Returns the first byte of one converted string, or zero for an empty string. +pub(in crate::interpreter) fn eval_ord_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(bytes.first().copied().unwrap_or(0))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs new file mode 100644 index 0000000000..9b98a99c38 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `rawurldecode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing URL decode hook. + +eval_builtin! { + name: "rawurldecode", + area: String, + params: [string], + direct: UrlDecode, + values: UrlDecode, +} + +use super::super::super::*; + +/// Evaluates PHP `rawurldecode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_rawurldecode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_builtin_url_decode_named("rawurldecode", args, context, scope, values) +} + +/// Applies PHP `rawurldecode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_rawurldecode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_url_decode_named_result("rawurldecode", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs new file mode 100644 index 0000000000..6f568304dc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `rawurlencode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing URL encode hook. + +eval_builtin! { + name: "rawurlencode", + area: String, + params: [string], + direct: UrlEncode, + values: UrlEncode, +} + +use super::super::super::*; + +/// Evaluates PHP `rawurlencode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_rawurlencode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_builtin_url_encode_named("rawurlencode", args, context, scope, values) +} + +/// Applies PHP `rawurlencode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_rawurlencode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_url_encode_named_result("rawurlencode", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs new file mode 100644 index 0000000000..1e39989021 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `rtrim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "rtrim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} + +use super::super::super::*; + +/// Evaluates PHP `rtrim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_rtrim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("rtrim", args, context, scope, values) +} + +/// Applies PHP `rtrim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_rtrim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("rtrim", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs b/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs new file mode 100644 index 0000000000..d996b8428a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `sha1`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "sha1", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `sha1(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_sha1( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("sha1", args, context, scope, values) +} + +/// Applies PHP `sha1(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_sha1_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("sha1", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs new file mode 100644 index 0000000000..070a33531e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Declarative eval registry entry for `str_contains`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. + +eval_builtin! { + name: "str_contains", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} + +use super::super::super::*; + +/// Evaluates PHP `str_contains(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_contains( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_contains", args, context, scope, values) +} + +/// Applies PHP `str_contains(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_contains_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_contains", haystack, needle, values) +} + +/// Evaluates one named PHP byte-string search predicate. +pub(in crate::interpreter) fn eval_builtin_string_search_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_search_named_result(name, haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +pub(in crate::interpreter) fn eval_string_search_named_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs new file mode 100644 index 0000000000..f2d24097bb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `str_ends_with`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. + +eval_builtin! { + name: "str_ends_with", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} + +use super::super::super::*; + +/// Evaluates PHP `str_ends_with(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_ends_with( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_ends_with", args, context, scope, values) +} + +/// Applies PHP `str_ends_with(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_ends_with_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_ends_with", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs new file mode 100644 index 0000000000..1ce2aceb62 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `str_ireplace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_ireplace", + area: String, + params: [search, replace, subject, count = EvalBuiltinDefaultValue::Null], + direct: StrReplace, + values: StrReplace, +} + +use super::super::super::*; + +/// Evaluates PHP `str_ireplace(...)` over search, replacement, and subject expressions. +pub(in crate::interpreter) fn eval_builtin_str_ireplace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_replace::eval_builtin_str_replace("str_ireplace", args, context, scope, values) +} + +/// Applies PHP `str_ireplace(...)` to already evaluated search, replacement, and subject values. +pub(in crate::interpreter) fn eval_str_ireplace_result( + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_replace::eval_str_replace_result("str_ireplace", search, replace, subject, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs new file mode 100644 index 0000000000..db584e9ac5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs @@ -0,0 +1,120 @@ +//! Purpose: +//! Declarative eval registry entry for `str_pad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-pad hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_pad", + area: String, + params: [ + string, + length, + pad_string = EvalBuiltinDefaultValue::String(" "), + pad_type = EvalBuiltinDefaultValue::Int(1), + ], + direct: StrPad, + values: StrPad, +} + +use super::super::super::*; + +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +pub(in crate::interpreter) fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +pub(in crate::interpreter) fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +pub(in crate::interpreter) fn eval_str_pad_sides( + pad_budget: usize, + pad_type: i64, +) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +pub(in crate::interpreter) fn eval_append_repeated_pad( + output: &mut Vec, + pad_string: &[u8], + count: usize, +) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs new file mode 100644 index 0000000000..7f2470f644 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Declarative eval registry entry for `str_repeat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing repeat hook. + +eval_builtin! { + name: "str_repeat", + area: String, + params: [string, times], + direct: StrRepeat, + values: StrRepeat, +} + +use super::super::super::*; + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +pub(in crate::interpreter) fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +pub(in crate::interpreter) fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs new file mode 100644 index 0000000000..87d62643ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Declarative eval registry entry for `str_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_replace", + area: String, + params: [search, replace, subject, count = EvalBuiltinDefaultValue::Null], + direct: StrReplace, + values: StrReplace, +} + +use super::super::super::*; + +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +pub(in crate::interpreter) fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +pub(in crate::interpreter) fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(super::strstr::eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs new file mode 100644 index 0000000000..edb9716de3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Declarative eval registry entry for `str_split`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-split hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_split", + area: String, + params: [string, length = EvalBuiltinDefaultValue::Int(1)], + direct: StrSplit, + values: StrSplit, +} + +use super::super::super::*; + +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +pub(in crate::interpreter) fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +pub(in crate::interpreter) fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs new file mode 100644 index 0000000000..cdbf36c901 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `str_starts_with`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. + +eval_builtin! { + name: "str_starts_with", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} + +use super::super::super::*; + +/// Evaluates PHP `str_starts_with(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_starts_with( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_starts_with", args, context, scope, values) +} + +/// Applies PHP `str_starts_with(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_starts_with_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_starts_with", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs new file mode 100644 index 0000000000..d46f5db679 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `strcasecmp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-compare hook. + +eval_builtin! { + name: "strcasecmp", + area: String, + params: [string1, string2], + direct: StringCompare, + values: StringCompare, +} + +use super::super::super::*; + +/// Evaluates PHP `strcasecmp(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_strcasecmp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_builtin_string_compare_named("strcasecmp", args, context, scope, values) +} + +/// Applies PHP `strcasecmp(...)` to two evaluated string values. +pub(in crate::interpreter) fn eval_strcasecmp_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_string_compare_named_result("strcasecmp", left, right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs new file mode 100644 index 0000000000..c337f5d9d9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Declarative eval registry entry for `strcmp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-compare hook. + +eval_builtin! { + name: "strcmp", + area: String, + params: [string1, string2], + direct: StringCompare, + values: StringCompare, +} + +use super::super::super::*; + +/// Evaluates PHP `strcmp(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_strcmp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_builtin_string_compare_named("strcmp", args, context, scope, values) +} + +/// Applies PHP `strcmp(...)` to two evaluated string values. +pub(in crate::interpreter) fn eval_strcmp_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_string_compare_named_result("strcmp", left, right, values) +} + +/// Evaluates one named PHP string comparison builtin. +pub(in crate::interpreter) fn eval_builtin_string_compare_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_named_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +pub(in crate::interpreter) fn eval_string_compare_named_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs new file mode 100644 index 0000000000..324951f5ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_filters`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the static stream-filter list helper. + +eval_builtin! { + name: "stream_get_filters", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_filters()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_filters( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_filters", args, context, values) +} + +/// Builds the result for PHP `stream_get_filters()`. +pub(in crate::interpreter) fn eval_stream_get_filters_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_filters", context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs new file mode 100644 index 0000000000..e91c9b6c53 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_transports`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the static stream-transport list helper. + +eval_builtin! { + name: "stream_get_transports", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_transports()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_transports( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_transports", args, context, values) +} + +/// Builds the result for PHP `stream_get_transports()`. +pub(in crate::interpreter) fn eval_stream_get_transports_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_transports", context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs new file mode 100644 index 0000000000..ee20b2328e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_wrappers`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the eval stream-wrapper registry helper. + +eval_builtin! { + name: "stream_get_wrappers", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_wrappers()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_wrappers( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_wrappers", args, context, values) +} + +/// Builds the result for PHP `stream_get_wrappers()`. +pub(in crate::interpreter) fn eval_stream_get_wrappers_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_wrappers", context, values) +} + +/// Evaluates a named stream introspection builtin with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_introspection_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_named_result(name, context, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +pub(in crate::interpreter) fn eval_stream_introspection_named_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => return eval_static_string_array_result(EVAL_STREAM_FILTERS, values), + "stream_get_transports" => { + return eval_static_string_array_result(EVAL_STREAM_TRANSPORTS, values); + } + "stream_get_wrappers" => context.stream_resources().stream_wrappers(EVAL_STREAM_WRAPPERS), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_owned_string_array_result(&items, values) +} + +/// Builds one indexed PHP array from an owned string slice. +pub(in crate::interpreter) fn eval_owned_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs new file mode 100644 index 0000000000..60a855bc06 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_is_local`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the stream boolean predicate helper. + +eval_builtin! { + name: "stream_is_local", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_is_local(...)` over one stream expression. +pub(in crate::interpreter) fn eval_builtin_stream_is_local( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_builtin_stream_bool_predicate_named("stream_is_local", args, context, scope, values) +} + +/// Builds the result for PHP `stream_is_local(...)` from one evaluated stream value. +pub(in crate::interpreter) fn eval_stream_is_local_result( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_stream_bool_predicate_named_result("stream_is_local", stream, values) +} + +/// Evaluates a named stream boolean predicate over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_bool_predicate_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_bool_predicate_named_result(name, stream, values) +} + +/// Returns elephc's fixed stream-locality and lock-support predicate values. +pub(in crate::interpreter) fn eval_stream_bool_predicate_named_result( + name: &str, + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "stream_is_local" => values.bool_value(true), + "stream_supports_lock" => { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs new file mode 100644 index 0000000000..f303ec3da8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_supports_lock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the stream boolean predicate helper. + +eval_builtin! { + name: "stream_supports_lock", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_supports_lock(...)` over one stream expression. +pub(in crate::interpreter) fn eval_builtin_stream_supports_lock( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_builtin_stream_bool_predicate_named("stream_supports_lock", args, context, scope, values) +} + +/// Builds the result for PHP `stream_supports_lock(...)` from one evaluated stream value. +pub(in crate::interpreter) fn eval_stream_supports_lock_result( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_stream_bool_predicate_named_result("stream_supports_lock", stream, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs new file mode 100644 index 0000000000..0304dfa981 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Declarative eval registry entry for `stripslashes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing slash unescaping hook. + +eval_builtin! { + name: "stripslashes", + area: String, + params: [string], + direct: Slashes, + values: Slashes, +} + +use super::super::super::*; + +/// Evaluates PHP `stripslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stripslashes( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_stripslashes_result(value, values) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +pub(in crate::interpreter) fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs new file mode 100644 index 0000000000..2cd65327a0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Declarative eval registry entry for `strlen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing string-length hook. + +eval_builtin! { + name: "strlen", + area: String, + params: [string], + direct: Strlen, + values: Strlen, +} + +use super::super::super::*; + +/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. +pub(in crate::interpreter) fn eval_builtin_strlen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let bytes = values.string_bytes(value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs new file mode 100644 index 0000000000..ab68534750 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs @@ -0,0 +1,84 @@ +//! Purpose: +//! Declarative eval registry entry for `strpos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-position hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strpos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} + +use super::super::super::*; + +/// Evaluates PHP `strpos(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_strpos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("strpos", args, context, scope, values) +} + +/// Applies PHP `strpos(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_strpos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("strpos", haystack, needle, values) +} + +/// Evaluates one named PHP byte-string position builtin. +pub(in crate::interpreter) fn eval_builtin_string_position_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_named_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +pub(in crate::interpreter) fn eval_string_position_named_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs new file mode 100644 index 0000000000..dfc5602517 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `strrev`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing string-reversal hook. + +eval_builtin! { + name: "strrev", + area: String, + params: [string], + direct: Strrev, + values: Strrev, +} + +use super::super::super::*; + +/// Evaluates PHP's `strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + +/// Reverses one converted eval string using the runtime string helper. +pub(in crate::interpreter) fn eval_strrev_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.strrev(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs new file mode 100644 index 0000000000..93b806a4db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `strrpos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-position hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strrpos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} + +use super::super::super::*; + +/// Evaluates PHP `strrpos(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_strrpos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("strrpos", args, context, scope, values) +} + +/// Applies PHP `strrpos(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_strrpos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("strrpos", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs new file mode 100644 index 0000000000..b5d88b1e33 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Declarative eval registry entry for `strstr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the strstr hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strstr", + area: String, + params: [haystack, needle, before_needle = EvalBuiltinDefaultValue::Bool(false)], + direct: Strstr, + values: Strstr, +} + +use super::super::super::*; + +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +pub(in crate::interpreter) fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +pub(in crate::interpreter) fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} + +/// Finds `needle` inside `haystack` starting from one byte offset. +pub(in crate::interpreter) fn eval_find_subslice( + haystack: &[u8], + needle: &[u8], + start: usize, +) -> Option { + haystack + .get(start..)? + .windows(needle.len()) + .position(|window| window == needle) + .map(|position| position + start) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs new file mode 100644 index 0000000000..7b4918239d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Declarative eval registry entry for `strtolower`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-case hook. + +eval_builtin! { + name: "strtolower", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} + +use super::super::super::*; + +/// Evaluates PHP `strtolower(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strtolower( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("strtolower", args, context, scope, values) +} + +/// Applies PHP `strtolower(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_strtolower_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("strtolower", value, values) +} + +/// Evaluates one named ASCII case-conversion string builtin. +pub(in crate::interpreter) fn eval_builtin_string_case_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_named_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +pub(in crate::interpreter) fn eval_string_case_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs new file mode 100644 index 0000000000..c231c6a6a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `strtoupper`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-case hook. + +eval_builtin! { + name: "strtoupper", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} + +use super::super::super::*; + +/// Evaluates PHP `strtoupper(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strtoupper( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("strtoupper", args, context, scope, values) +} + +/// Applies PHP `strtoupper(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_strtoupper_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("strtoupper", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs new file mode 100644 index 0000000000..565814fb11 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Declarative eval registry entry for `substr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the substring hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "substr", + area: String, + params: [string, offset, length = EvalBuiltinDefaultValue::Null], + direct: Substr, + values: Substr, +} + +use super::super::super::*; + +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +pub(in crate::interpreter) fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +pub(in crate::interpreter) fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs new file mode 100644 index 0000000000..b2e635feec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Declarative eval registry entry for `substr_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the substring-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "substr_replace", + area: String, + params: [string, replace, offset, length = EvalBuiltinDefaultValue::Null], + direct: SubstrReplace, + values: SubstrReplace, +} + +use super::super::super::*; + +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +pub(in crate::interpreter) fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +pub(in crate::interpreter) fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/trim.rs b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs new file mode 100644 index 0000000000..b3983c23ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Declarative eval registry entry for `trim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "trim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} + +use super::super::super::*; + +/// Evaluates PHP `trim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_trim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("trim", args, context, scope, values) +} + +/// Applies PHP `trim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_trim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("trim", value, mask, values) +} + +pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates one named PHP trim-family builtin. +pub(in crate::interpreter) fn eval_builtin_trim_like_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_named_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_named_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +pub(in crate::interpreter) fn eval_trim_like_named_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs new file mode 100644 index 0000000000..49d934b5b2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `ucfirst`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string-case hook. + +eval_builtin! { + name: "ucfirst", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} + +use super::super::super::*; + +/// Evaluates PHP `ucfirst(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ucfirst( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("ucfirst", args, context, scope, values) +} + +/// Applies PHP `ucfirst(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_ucfirst_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("ucfirst", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs new file mode 100644 index 0000000000..f73c3a6f2d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Declarative eval registry entry for `ucwords`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the ucwords hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "ucwords", + area: String, + params: [string, separators = EvalBuiltinDefaultValue::Bytes(b" \t\r\n\x0c\x0b")], + direct: Ucwords, + values: Ucwords, +} + +use super::super::super::*; + +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +pub(in crate::interpreter) fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +pub(in crate::interpreter) fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs new file mode 100644 index 0000000000..60e6688b3d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Declarative eval registry entry for `urldecode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing URL decode hook. + +eval_builtin! { + name: "urldecode", + area: String, + params: [string], + direct: UrlDecode, + values: UrlDecode, +} + +use super::super::super::*; + +/// Evaluates PHP `urldecode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_urldecode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_builtin_url_decode_named("urldecode", args, context, scope, values) +} + +/// Applies PHP `urldecode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_urldecode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_url_decode_named_result("urldecode", value, values) +} + +/// Evaluates a named PHP URL decoder over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_decode_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_named_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +pub(in crate::interpreter) fn eval_url_decode_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + super::hex2bin::eval_hex_nibble(bytes[index + 1]), + super::hex2bin::eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs new file mode 100644 index 0000000000..04b80e2070 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs @@ -0,0 +1,87 @@ +//! Purpose: +//! Declarative eval registry entry for `urlencode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the existing URL encode hook. + +eval_builtin! { + name: "urlencode", + area: String, + params: [string], + direct: UrlEncode, + values: UrlEncode, +} + +use super::super::super::*; + +/// Evaluates PHP `urlencode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_urlencode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_builtin_url_encode_named("urlencode", args, context, scope, values) +} + +/// Applies PHP `urlencode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_urlencode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_url_encode_named_result("urlencode", value, values) +} + +/// Evaluates a named PHP URL encoder over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_encode_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_named_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +pub(in crate::interpreter) fn eval_url_encode_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +pub(in crate::interpreter) fn eval_url_encode_keeps_byte( + name: &str, + byte: u8, +) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs new file mode 100644 index 0000000000..1bb799f86b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs @@ -0,0 +1,157 @@ +//! Purpose: +//! Declarative eval registry entry for `wordwrap`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the wordwrap hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "wordwrap", + area: String, + params: [ + string, + width = EvalBuiltinDefaultValue::Int(75), + r#break = EvalBuiltinDefaultValue::String("\n"), + cut_long_words = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Wordwrap, + values: Wordwrap, +} + +use super::super::super::*; + +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +pub(in crate::interpreter) fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +pub(in crate::interpreter) fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +pub(in crate::interpreter) fn eval_wordwrap_bytes( + bytes: &[u8], + width: i64, + break_string: &[u8], + cut: bool, +) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs new file mode 100644 index 0000000000..91bbb2e983 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Orchestrates symbol, constant, class, and language-construct eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Concrete builtin behavior lives in focused `symbols/` modules so each +//! source file stays cohesive and below the ordinary 500 LoC guideline. + +use super::super::*; + +mod class_alias; +mod class_attribute_args; +mod class_attribute_names; +mod class_exists; +mod class_get_attributes; +mod class_implements; +mod class_parents; +mod class_uses; +mod dispatch; +mod empty; +mod enum_exists; +mod function_exists; +mod get_called_class; +mod get_class; +mod get_class_methods; +mod get_class_vars; +mod get_declared_classes; +mod get_declared_interfaces; +mod get_declared_traits; +mod get_object_vars; +mod get_parent_class; +mod get_resource_id; +mod get_resource_type; +mod interface_exists; +mod is_a; +mod is_callable; +mod is_subclass_of; +mod isset; +mod method_exists; +mod property_exists; +mod spl_autoload; +mod spl_autoload_call; +mod spl_autoload_extensions; +mod spl_autoload_functions; +mod spl_autoload_register; +mod spl_autoload_unregister; +mod spl_classes; +mod spl_object_hash; +mod spl_object_id; +mod trait_exists; +mod unset; + +pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; +pub(in crate::interpreter) use class_attribute_names::{ + eval_class_attribute_args_result, eval_reflection_attribute_array_result, +}; +pub(in crate::interpreter) use class_implements::eval_class_relation_result; +pub(in crate::interpreter) use function_exists::{ + eval_date_procedural_alias_names, eval_function_probe_exists, +}; +pub(in crate::interpreter) use get_class::eval_get_class_result; +pub(in crate::interpreter) use get_parent_class::eval_get_parent_class_result; +pub(in crate::interpreter) use is_a::dynamic_object_is_a; +pub(in crate::interpreter) use is_callable::{ + eval_builtin_is_callable_call, eval_is_callable_call_with_evaluated_args, + eval_is_callable_value, +}; +pub(in crate::interpreter) use method_exists::eval_member_exists_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs new file mode 100644 index 0000000000..0b8577403e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs @@ -0,0 +1,111 @@ +//! Purpose: +//! Eval registry entry and implementation for `class_alias`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime aliases are stored in the eval context for eval declarations and +//! generated/AOT class-like metadata. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_alias", + area: Symbols, + params: [r#class, alias, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `class_alias(class, alias, autoload?)` calls. +pub(in crate::interpreter) fn eval_class_alias_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_class_alias(args, context, scope, values) +} + +/// Evaluates materialized `class_alias(...)` arguments. +pub(in crate::interpreter) fn eval_class_alias_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_alias_result(evaluated_args, context, values) +} + +/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. +pub(in crate::interpreter) fn eval_builtin_class_alias( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match args { + [class, alias] => ( + eval_expr(class, context, scope, values)?, + eval_expr(alias, context, scope, values)?, + ), + [class, alias, autoload] => { + let class = eval_expr(class, context, scope, values)?; + let alias = eval_expr(alias, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + (class, alias) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_alias_result(&[class, alias], context, values) +} + +/// Evaluates `class_alias(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_alias_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match evaluated_args { + [class, alias] => (*class, *alias), + [class, alias, _autoload] => (*class, *alias), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let class = eval_class_alias_name(class, values)?; + let alias = eval_class_alias_name(alias, values)?; + if alias.is_empty() + || context.resolve_class_like_name(&alias).is_some() + || values.class_exists(&alias)? + || eval_runtime_interface_exists(&alias, values)? + || values.trait_exists(&alias)? + || values.enum_exists(&alias)? + { + return values.bool_value(false); + } + let aliased = if context.resolve_class_like_name(&class).is_some() { + context.define_class_alias(&class, &alias) + } else if values.enum_exists(&class)? { + context.define_external_enum_alias(&class, &alias) + } else if values.class_exists(&class)? { + context.define_external_class_alias(&class, &alias) + } else if eval_runtime_interface_exists(&class, values)? { + context.define_external_interface_alias(&class, &alias) + } else if values.trait_exists(&class)? { + context.define_external_trait_alias(&class, &alias) + } else { + false + }; + values.bool_value(aliased) +} + +/// Reads and normalizes one `class_alias()` class-name argument. +fn eval_class_alias_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs new file mode 100644 index 0000000000..ebd2efc1cd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Declarative eval registry entry for `class_attribute_args`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-attribute metadata logic lives in `class_attribute_names`. + +eval_builtin! { + name: "class_attribute_args", + area: Symbols, + params: [class_name, attribute_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_attribute_args` symbol builtin. +pub(in crate::interpreter) fn eval_class_attribute_args_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_attribute_names::eval_builtin_class_attribute_metadata( + "class_attribute_args", + args, + context, + scope, + values, + ) +} + +/// Dispatches evaluated-argument calls for the `class_attribute_args` symbol builtin. +pub(in crate::interpreter) fn eval_class_attribute_args_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_attribute_names::eval_class_attribute_metadata_result( + "class_attribute_args", + evaluated_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs new file mode 100644 index 0000000000..7d3b596ef3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs @@ -0,0 +1,248 @@ +//! Purpose: +//! Eval registry entry and implementation for `class_attribute_names`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-attribute metadata logic for `class_attribute_args()` and +//! `class_get_attributes()` lives here. + +eval_builtin! { + name: "class_attribute_names", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::eval_class_metadata_name; + +/// Dispatches direct eval calls for the `class_attribute_names` symbol builtin. +pub(in crate::interpreter) fn eval_class_attribute_names_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_class_attribute_metadata("class_attribute_names", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_attribute_names` symbol builtin. +pub(in crate::interpreter) fn eval_class_attribute_names_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_attribute_metadata_result("class_attribute_names", evaluated_args, context, values) +} + +/// Evaluates class attribute metadata helpers. +pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match (name, args) { + ("class_attribute_names" | "class_get_attributes", [class_name]) => { + vec![eval_expr(class_name, context, scope, values)?] + } + ("class_attribute_args", [class_name, attribute_name]) => vec![ + eval_expr(class_name, context, scope, values)?, + eval_expr(attribute_name, context, scope, values)?, + ], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_attribute_metadata_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized class attribute metadata arguments. +pub(in crate::interpreter) fn eval_class_attribute_metadata_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, evaluated_args) { + ("class_attribute_names", [class_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + eval_class_attribute_names_result(&attributes, values) + } + ("class_get_attributes", [class_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + eval_reflection_attribute_array_result( + &attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, + context, + values, + ) + } + ("class_attribute_args", [class_name, attribute_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attribute_name = eval_class_metadata_name(*attribute_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + let Some(attribute) = attributes + .iter() + .find(|attribute| eval_attribute_name_matches(attribute.name(), &attribute_name)) + else { + return values.array_new(0); + }; + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_class_attribute_args_result(args, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns class-like attributes for a dynamic eval class, interface, trait, or enum. +fn eval_class_like_attributes<'a>( + context: &'a ElephcEvalContext, + name: &str, +) -> Option<&'a [EvalAttribute]> { + if let Some(class) = context.class(name) { + return Some(class.attributes()); + } + if let Some(interface) = context.interface(name) { + return Some(interface.attributes()); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(trait_decl.attributes()); + } + context.enum_decl(name).map(EvalEnum::attributes) +} + +/// Returns class-like attributes for eval declarations or generated AOT metadata. +fn eval_class_like_attribute_metadata( + context: &ElephcEvalContext, + name: &str, +) -> Vec { + if let Some(attributes) = eval_class_like_attributes(context, name) { + return attributes.to_vec(); + } + context.native_class_attributes(name) +} + +/// Builds the indexed string array returned by `class_attribute_names()`. +fn eval_class_attribute_names_result( + attributes: &[EvalAttribute], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(attribute.name())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed `ReflectionAttribute` array from eval-retained attribute metadata. +pub(in crate::interpreter) fn eval_reflection_attribute_array_result( + attributes: &[EvalAttribute], + target: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = values.int(index as i64)?; + let args = eval_class_attribute_args_result(args, values)?; + let repeated = eval_attribute_is_repeated(attributes, attribute.name()); + let reflection_attribute = + values.reflection_attribute_new(attribute.name(), args, target, repeated)?; + let identity = values.object_identity(reflection_attribute)?; + context.register_eval_reflection_attribute(identity, attribute.clone(), target, repeated); + values.release(args)?; + result = values.array_set(result, key, reflection_attribute)?; + } + Ok(result) +} + +/// Returns true when an attribute name appears more than once on the same owner. +fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool { + attributes + .iter() + .filter(|attribute| eval_attribute_name_matches(attribute.name(), name)) + .nth(1) + .is_some() +} + +/// Builds the mixed PHP array returned by `class_attribute_args()`. +pub(in crate::interpreter) fn eval_class_attribute_args_result( + args: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = if args.iter().any(|arg| arg.name().is_some()) { + values.assoc_new(args.len())? + } else { + values.array_new(args.len())? + }; + for (index, arg) in args.iter().enumerate() { + let key = match arg.name() { + Some(name) => values.string(name)?, + None => values.int(index as i64)?, + }; + let value = eval_class_attribute_arg_value(arg.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Materializes one retained eval attribute argument as a runtime cell. +fn eval_class_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => eval_class_attribute_array_arg_value(elements, values), + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_class_attribute_arg_value(value, values) + } + } +} + +/// Materializes one retained attribute array literal as a runtime array cell. +fn eval_class_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + for (index, element) in elements.iter().enumerate() { + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, + }; + let value = eval_class_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether a query names the same PHP attribute class case-insensitively. +fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { + attribute_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(query.trim_start_matches('\\')) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs new file mode 100644 index 0000000000..c154105d22 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Eval registry entry and implementation for `class_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Lookup checks eval declarations before generated/AOT runtime metadata. +//! - `Closure` is treated as a built-in class-like symbol. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_exists", + area: Symbols, + params: [r#class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `class_exists(...)` calls against dynamic and generated class-name tables. +pub(in crate::interpreter) fn eval_class_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_class_exists(args, context, scope, values) +} + +/// Evaluates materialized `class_exists(...)` arguments. +pub(in crate::interpreter) fn eval_class_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_exists_result(evaluated_args, context, values) +} + +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. +pub(in crate::interpreter) fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. +pub(in crate::interpreter) fn eval_class_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + if name.eq_ignore_ascii_case("Closure") { + return Ok(true); + } + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs new file mode 100644 index 0000000000..e9f084e690 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Declarative eval registry entry for `class_get_attributes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-attribute metadata logic lives in `class_attribute_names`. + +eval_builtin! { + name: "class_get_attributes", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_get_attributes` symbol builtin. +pub(in crate::interpreter) fn eval_class_get_attributes_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_attribute_names::eval_builtin_class_attribute_metadata( + "class_get_attributes", + args, + context, + scope, + values, + ) +} + +/// Dispatches evaluated-argument calls for the `class_get_attributes` symbol builtin. +pub(in crate::interpreter) fn eval_class_get_attributes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_attribute_names::eval_class_attribute_metadata_result( + "class_get_attributes", + evaluated_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs new file mode 100644 index 0000000000..da6e5bbd29 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs @@ -0,0 +1,284 @@ +//! Purpose: +//! Eval registry entry and implementation for `class_implements`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-relation logic for `class_parents()` and `class_uses()` lives here. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_implements", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::{eval_class_metadata_name, eval_class_relation_name_exists}; + +/// Dispatches direct eval calls for the `class_implements` symbol builtin. +pub(in crate::interpreter) fn eval_class_implements_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_class_relation("class_implements", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_implements` symbol builtin. +pub(in crate::interpreter) fn eval_class_implements_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_relation_result("class_implements", evaluated_args, context, values) +} + +/// Evaluates `class_implements()`, `class_parents()`, or `class_uses()`. +pub(in crate::interpreter) fn eval_builtin_class_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let target = eval_expr(&args[0], context, scope, values)?; + if let Some(autoload) = args.get(1) { + let _ = eval_expr(autoload, context, scope, values)?; + } + eval_class_relation_target_result(name, target, context, values) +} + +/// Evaluates materialized class-relation builtin arguments. +pub(in crate::interpreter) fn eval_class_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let target = match evaluated_args { + [target] => *target, + [target, _autoload] => *target, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_relation_target_result(name, target, context, values) +} + +/// Resolves one class-relation target and returns an empty relation set or false. +pub(in crate::interpreter) fn eval_class_relation_target_result( + name: &str, + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "class_implements" | "class_parents" | "class_uses") { + return Err(EvalStatus::RuntimeFatal); + } + let Some(target) = eval_class_relation_target_name(target, context, values)? else { + return values.bool_value(false); + }; + if context.class(&target).is_some() { + return match name { + "class_implements" => { + let names = + eval_class_relation_eval_class_interface_names(&target, context, values)?; + eval_class_relation_names_result(names, values) + } + "class_parents" => { + eval_class_relation_names_result(context.class_parent_names(&target), values) + } + "class_uses" => { + eval_class_relation_names_result(context.class_trait_names(&target), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }; + } + if context.interface(&target).is_some() { + return match name { + "class_implements" => { + let names = + eval_class_relation_eval_interface_parent_names(&target, context, values)?; + eval_class_relation_names_result(names, values) + } + "class_parents" | "class_uses" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } + if context.trait_decl(&target).is_some() { + return match name { + "class_uses" => { + eval_class_relation_names_result(context.trait_trait_names(&target), values) + } + "class_implements" | "class_parents" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } + match name { + "class_implements" => eval_runtime_class_interface_names_result(&target, values), + "class_parents" => { + eval_class_relation_names_result( + eval_runtime_class_parent_names(&target, context), + values, + ) + } + "class_uses" => eval_runtime_class_trait_names_result(&target, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds `class_implements()` data for generated/AOT class metadata. +fn eval_runtime_class_interface_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names = eval_runtime_class_interface_names(class_name, values)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Builds `class_uses()` data for generated/AOT direct trait-use metadata. +fn eval_runtime_class_trait_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names_array = values.reflection_class_trait_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT parent names in PHP's nearest-parent-first order. +fn eval_runtime_class_parent_names( + class_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let mut names = Vec::new(); + let mut current = context.native_class_parent(class_name).map(str::to_string); + let mut seen = std::collections::HashSet::new(); + while let Some(parent) = current { + let parent = parent.trim_start_matches('\\').to_string(); + if !seen.insert(parent.to_ascii_lowercase()) { + break; + } + current = context.native_class_parent(&parent).map(str::to_string); + names.push(parent); + } + names +} + +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +fn eval_class_relation_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_class_relation_push_unique_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +fn eval_class_relation_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_class_relation_push_unique_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Copies a runtime string array into Rust-owned class/interface names. +fn eval_class_relation_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} + +/// Returns whether a class-relation target refers to a known class-like symbol. +fn eval_class_relation_target_name( + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + let name = super::get_class::eval_get_class_result(target, context, values)?; + let name = eval_class_metadata_name(name, values)?; + return Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)); + } + let name = eval_class_metadata_name(target, values)?; + let name = context.resolve_class_like_name(&name).unwrap_or(name); + Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)) +} + +/// Builds a PHP associative class-name array keyed by class-name strings. +fn eval_class_relation_names_result( + names: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(&name)?; + let value = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs new file mode 100644 index 0000000000..cc6b8059b4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_parents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-relation logic lives in `class_implements`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_parents", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_parents` symbol builtin. +pub(in crate::interpreter) fn eval_class_parents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_implements::eval_builtin_class_relation("class_parents", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_parents` symbol builtin. +pub(in crate::interpreter) fn eval_class_parents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_implements::eval_class_relation_result("class_parents", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs new file mode 100644 index 0000000000..81280baa72 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_uses`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared class-relation logic lives in `class_implements`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_uses", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_uses` symbol builtin. +pub(in crate::interpreter) fn eval_class_uses_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_implements::eval_builtin_class_relation("class_uses", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_uses` symbol builtin. +pub(in crate::interpreter) fn eval_class_uses_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::class_implements::eval_class_relation_result("class_uses", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs new file mode 100644 index 0000000000..7ac2761685 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Direct and evaluated-argument dispatch for symbol builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` for migrated symbol dispatch. +//! +//! Key details: +//! - Public dispatch routes through per-builtin leaf wrappers so declaration +//! files own their registry entry and runtime adapter. +//! - Leaf builtin modules own the concrete direct and materialized-argument +//! entry points so this dispatcher only routes public symbol-area calls. + +use super::*; + +/// Routes direct expression-level symbol builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_builtin_symbols_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => super::class_alias::eval_class_alias_declared_call(args, context, scope, values), + "class_attribute_args" => super::class_attribute_args::eval_class_attribute_args_declared_call(args, context, scope, values), + "class_attribute_names" => super::class_attribute_names::eval_class_attribute_names_declared_call(args, context, scope, values), + "class_exists" => super::class_exists::eval_class_exists_declared_call(args, context, scope, values), + "class_get_attributes" => super::class_get_attributes::eval_class_get_attributes_declared_call(args, context, scope, values), + "class_implements" => super::class_implements::eval_class_implements_declared_call(args, context, scope, values), + "class_parents" => super::class_parents::eval_class_parents_declared_call(args, context, scope, values), + "class_uses" => super::class_uses::eval_class_uses_declared_call(args, context, scope, values), + "empty" => super::empty::eval_empty_declared_call(args, context, scope, values), + "enum_exists" => super::enum_exists::eval_enum_exists_declared_call(args, context, scope, values), + "function_exists" => super::function_exists::eval_function_exists_declared_call(args, context, scope, values), + "get_called_class" => super::get_called_class::eval_get_called_class_declared_call(args, context, scope, values), + "get_class" => super::get_class::eval_get_class_declared_call(args, context, scope, values), + "get_class_methods" => super::get_class_methods::eval_get_class_methods_declared_call(args, context, scope, values), + "get_class_vars" => super::get_class_vars::eval_get_class_vars_declared_call(args, context, scope, values), + "get_declared_classes" => super::get_declared_classes::eval_get_declared_classes_declared_call(args, context, scope, values), + "get_declared_interfaces" => super::get_declared_interfaces::eval_get_declared_interfaces_declared_call(args, context, scope, values), + "get_declared_traits" => super::get_declared_traits::eval_get_declared_traits_declared_call(args, context, scope, values), + "get_object_vars" => super::get_object_vars::eval_get_object_vars_declared_call(args, context, scope, values), + "get_parent_class" => super::get_parent_class::eval_get_parent_class_declared_call(args, context, scope, values), + "get_resource_id" => super::get_resource_id::eval_get_resource_id_declared_call(args, context, scope, values), + "get_resource_type" => super::get_resource_type::eval_get_resource_type_declared_call(args, context, scope, values), + "interface_exists" => super::interface_exists::eval_interface_exists_declared_call(args, context, scope, values), + "is_a" => super::is_a::eval_is_a_declared_call(args, context, scope, values), + "is_callable" => super::is_callable::eval_is_callable_declared_call(args, context, scope, values), + "is_subclass_of" => super::is_subclass_of::eval_is_subclass_of_declared_call(args, context, scope, values), + "isset" => super::isset::eval_isset_declared_call(args, context, scope, values), + "method_exists" => super::method_exists::eval_method_exists_declared_call(args, context, scope, values), + "property_exists" => super::property_exists::eval_property_exists_declared_call(args, context, scope, values), + "spl_autoload" => super::spl_autoload::eval_spl_autoload_declared_call(args, context, scope, values), + "spl_autoload_call" => super::spl_autoload_call::eval_spl_autoload_call_declared_call(args, context, scope, values), + "spl_autoload_extensions" => super::spl_autoload_extensions::eval_spl_autoload_extensions_declared_call(args, context, scope, values), + "spl_autoload_functions" => super::spl_autoload_functions::eval_spl_autoload_functions_declared_call(args, context, scope, values), + "spl_autoload_register" => super::spl_autoload_register::eval_spl_autoload_register_declared_call(args, context, scope, values), + "spl_autoload_unregister" => super::spl_autoload_unregister::eval_spl_autoload_unregister_declared_call(args, context, scope, values), + "spl_classes" => super::spl_classes::eval_spl_classes_declared_call(args, context, scope, values), + "spl_object_hash" => super::spl_object_hash::eval_spl_object_hash_declared_call(args, context, scope, values), + "spl_object_id" => super::spl_object_id::eval_spl_object_id_declared_call(args, context, scope, values), + "trait_exists" => super::trait_exists::eval_trait_exists_declared_call(args, context, scope, values), + "unset" => super::unset::eval_unset_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Routes evaluated-argument symbol builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_symbols_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => super::class_alias::eval_class_alias_declared_values_result(evaluated_args, context, values), + "class_attribute_args" => super::class_attribute_args::eval_class_attribute_args_declared_values_result(evaluated_args, context, values), + "class_attribute_names" => super::class_attribute_names::eval_class_attribute_names_declared_values_result(evaluated_args, context, values), + "class_exists" => super::class_exists::eval_class_exists_declared_values_result(evaluated_args, context, values), + "class_get_attributes" => super::class_get_attributes::eval_class_get_attributes_declared_values_result(evaluated_args, context, values), + "class_implements" => super::class_implements::eval_class_implements_declared_values_result(evaluated_args, context, values), + "class_parents" => super::class_parents::eval_class_parents_declared_values_result(evaluated_args, context, values), + "class_uses" => super::class_uses::eval_class_uses_declared_values_result(evaluated_args, context, values), + "empty" => super::empty::eval_empty_declared_values_result(evaluated_args, context, values), + "enum_exists" => super::enum_exists::eval_enum_exists_declared_values_result(evaluated_args, context, values), + "function_exists" => super::function_exists::eval_function_exists_declared_values_result(evaluated_args, context, values), + "get_called_class" => super::get_called_class::eval_get_called_class_declared_values_result(evaluated_args, context, values), + "get_class" => super::get_class::eval_get_class_declared_values_result(evaluated_args, context, values), + "get_class_methods" => super::get_class_methods::eval_get_class_methods_declared_values_result(evaluated_args, context, values), + "get_class_vars" => super::get_class_vars::eval_get_class_vars_declared_values_result(evaluated_args, context, values), + "get_declared_classes" => super::get_declared_classes::eval_get_declared_classes_declared_values_result(evaluated_args, context, values), + "get_declared_interfaces" => super::get_declared_interfaces::eval_get_declared_interfaces_declared_values_result(evaluated_args, context, values), + "get_declared_traits" => super::get_declared_traits::eval_get_declared_traits_declared_values_result(evaluated_args, context, values), + "get_object_vars" => super::get_object_vars::eval_get_object_vars_declared_values_result(evaluated_args, context, values), + "get_parent_class" => super::get_parent_class::eval_get_parent_class_declared_values_result(evaluated_args, context, values), + "get_resource_id" => super::get_resource_id::eval_get_resource_id_declared_values_result(evaluated_args, context, values), + "get_resource_type" => super::get_resource_type::eval_get_resource_type_declared_values_result(evaluated_args, context, values), + "interface_exists" => super::interface_exists::eval_interface_exists_declared_values_result(evaluated_args, context, values), + "is_a" => super::is_a::eval_is_a_declared_values_result(evaluated_args, context, values), + "is_callable" => super::is_callable::eval_is_callable_declared_values_result(evaluated_args, context, values), + "is_subclass_of" => super::is_subclass_of::eval_is_subclass_of_declared_values_result(evaluated_args, context, values), + "isset" => super::isset::eval_isset_declared_values_result(evaluated_args, context, values), + "method_exists" => super::method_exists::eval_method_exists_declared_values_result(evaluated_args, context, values), + "property_exists" => super::property_exists::eval_property_exists_declared_values_result(evaluated_args, context, values), + "spl_autoload" => super::spl_autoload::eval_spl_autoload_declared_values_result(evaluated_args, context, values), + "spl_autoload_call" => super::spl_autoload_call::eval_spl_autoload_call_declared_values_result(evaluated_args, context, values), + "spl_autoload_extensions" => super::spl_autoload_extensions::eval_spl_autoload_extensions_declared_values_result(evaluated_args, context, values), + "spl_autoload_functions" => super::spl_autoload_functions::eval_spl_autoload_functions_declared_values_result(evaluated_args, context, values), + "spl_autoload_register" => super::spl_autoload_register::eval_spl_autoload_register_declared_values_result(evaluated_args, context, values), + "spl_autoload_unregister" => super::spl_autoload_unregister::eval_spl_autoload_unregister_declared_values_result(evaluated_args, context, values), + "spl_classes" => super::spl_classes::eval_spl_classes_declared_values_result(evaluated_args, context, values), + "spl_object_hash" => super::spl_object_hash::eval_spl_object_hash_declared_values_result(evaluated_args, context, values), + "spl_object_id" => super::spl_object_id::eval_spl_object_id_declared_values_result(evaluated_args, context, values), + "trait_exists" => super::trait_exists::eval_trait_exists_declared_values_result(evaluated_args, context, values), + "unset" => super::unset::eval_unset_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs new file mode 100644 index 0000000000..81cee91306 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs @@ -0,0 +1,181 @@ +//! Purpose: +//! Eval registry entry and implementation for `empty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so missing variables are not evaluated normally. + +eval_builtin! { + name: "empty", + area: Symbols, + params: [value], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_empty_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_empty(args, context, scope, values) +} + +/// Evaluates callable `empty(...)` over one already materialized value. +pub(in crate::interpreter) fn eval_empty_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_empty_result(evaluated_args, values) +} + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates callable `empty(...)` over one already materialized value. +pub(in crate::interpreter) fn eval_empty_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = !values.truthy(*value)?; + values.bool_value(empty) +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +pub(in crate::interpreter) fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + if !eval_static_property_isset_result(class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + if !eval_static_property_isset_result(&class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_static_property_isset_result(&class_name, &property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_empty_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. +fn eval_array_access_empty_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !super::isset::eval_array_access_isset_result(object, index, context, values)? { + return Ok(true); + } + let value = eval_array_get_result(object, index, context, values)?; + Ok(!values.truthy(value)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs new file mode 100644 index 0000000000..387e29bc9f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Eval registry entry for `enum_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Class-like existence semantics are shared with `trait_exists()`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "enum_exists", + area: Symbols, + params: [r#enum, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `enum_exists(...)` calls through the `trait_exists` owner. +pub(in crate::interpreter) fn eval_enum_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trait_exists::eval_builtin_class_like_exists("enum_exists", args, context, scope, values) +} + +/// Evaluates materialized `enum_exists(...)` arguments through the `trait_exists` owner. +pub(in crate::interpreter) fn eval_enum_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trait_exists::eval_class_like_exists_result("enum_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs new file mode 100644 index 0000000000..b99c6287e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs @@ -0,0 +1,161 @@ +//! Purpose: +//! Eval registry entry and implementation for `function_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Procedural date/time aliases stay visible to eval because static name +//! resolver rewrites do not run inside runtime eval fragments. + +eval_builtin! { + name: "function_exists", + area: Symbols, + params: [function], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ + "cal_days_in_month", + "cal_from_jd", + "cal_info", + "cal_to_jd", + "date_add", + "date_create", + "date_create_from_format", + "date_create_immutable", + "date_create_immutable_from_format", + "date_date_set", + "date_diff", + "date_format", + "date_get_last_errors", + "date_interval_create_from_date_string", + "date_interval_format", + "date_isodate_set", + "date_modify", + "date_offset_get", + "date_parse", + "date_parse_from_format", + "date_sub", + "date_sun_info", + "date_sunrise", + "date_sunset", + "date_time_set", + "date_timestamp_get", + "date_timestamp_set", + "date_timezone_get", + "date_timezone_set", + "easter_date", + "easter_days", + "frenchtojd", + "gettimeofday", + "gmstrftime", + "gregoriantojd", + "idate", + "jddayofweek", + "jdmonthname", + "jdtofrench", + "jdtogregorian", + "jdtojewish", + "jdtojulian", + "jdtounix", + "jewishtojd", + "juliantojd", + "mktime", + "gmmktime", + "strftime", + "strptime", + "timezone_abbreviations_list", + "timezone_identifiers_list", + "timezone_location_get", + "timezone_name_from_abbr", + "timezone_name_get", + "timezone_offset_get", + "timezone_open", + "timezone_transitions_get", + "timezone_version_get", + "unixtojd", +]; + +/// Evaluates direct `function_exists(...)` calls inside an eval fragment. +pub(in crate::interpreter) fn eval_function_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_function_exists(args, context, scope, values) +} + +/// Evaluates materialized `function_exists(...)` arguments. +pub(in crate::interpreter) fn eval_function_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_function_exists_result(*value, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `function_exists()` inside an eval fragment. +pub(in crate::interpreter) fn eval_builtin_function_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_function_exists_result(value, context, values) +} + +/// Evaluates `function_exists()` from one materialized function-name argument. +pub(in crate::interpreter) fn eval_function_exists_result( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_function_probe_name(value, values)?; + let exists = eval_function_probe_exists(context, &name); + values.bool_value(exists) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +pub(in crate::interpreter) fn eval_function_probe_exists( + context: &ElephcEvalContext, + name: &str, +) -> bool { + !name.contains("::") + && (context.has_function(name) + || eval_php_visible_builtin_exists(name) + || eval_date_procedural_alias_exists(name)) +} + +/// Returns the procedural date/time alias names the eval dispatcher accepts +/// without declarative registry entries (documentation metadata surface). +pub(in crate::interpreter) fn eval_date_procedural_alias_names() -> &'static [&'static str] { + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS +} + +/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. +fn eval_date_procedural_alias_exists(name: &str) -> bool { + let bare = name.rsplit('\\').next().unwrap_or(""); + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) +} + +/// Reads and normalizes a function-probe string argument. +fn eval_function_probe_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_ascii_lowercase()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs new file mode 100644 index 0000000000..4021a30bc5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_called_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - The result uses the late-static-bound class scope when present, matching PHP. + +eval_builtin! { + name: "get_called_class", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_called_class()` calls. +pub(in crate::interpreter) fn eval_get_called_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_called_class(args, context, values) +} + +/// Evaluates materialized `get_called_class()` arguments. +pub(in crate::interpreter) fn eval_get_called_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + eval_get_called_class_result(context, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Evaluates PHP's `get_called_class()` against the current eval method scope. +pub(in crate::interpreter) fn eval_builtin_get_called_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values) +} + +/// Returns the current late-static-bound class name or throws PHP's class-scope error. +pub(in crate::interpreter) fn eval_get_called_class_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + else { + return eval_throw_error( + "get_called_class() must be called from within a class", + context, + values, + ); + }; + values.string(class_name.trim_start_matches('\\')) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs new file mode 100644 index 0000000000..c838be1611 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval-created object class names are resolved from eval context before +//! falling back to runtime object metadata. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_class", + area: Symbols, + params: [object = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_class(...)` calls. +pub(in crate::interpreter) fn eval_get_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_class(args, context, scope, values) +} + +/// Evaluates materialized `get_class(...)` arguments. +pub(in crate::interpreter) fn eval_get_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_get_class_no_arg_result(context, values), + [object] => eval_get_class_result(*object, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's `get_class(...)` over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_get_class_no_arg_result(context, values), + [object] => { + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return eval_throw_error( + "get_class() without arguments must be called from within a class", + context, + values, + ); + }; + values.string(class_name.trim_start_matches('\\')) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +pub(in crate::interpreter) fn eval_get_class_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return values.string(&class_name); + } + } + values.object_class_name(object) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs new file mode 100644 index 0000000000..2b4247f623 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs @@ -0,0 +1,222 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_class_methods`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Method lists are filtered through the current eval scope and PHP visibility +//! rules before materialization. + +eval_builtin! { + name: "get_class_methods", + area: Symbols, + params: [object_or_class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::{ + eval_class_metadata_is_a, eval_class_metadata_target_name, eval_class_relation_name_exists, + eval_indexed_string_array_result, eval_runtime_method_access_metadata, + eval_runtime_string_array_to_vec, eval_same_class_metadata_name, +}; +use std::collections::HashSet; + +/// Dispatches direct eval calls for the `get_class_methods` symbol builtin. +pub(in crate::interpreter) fn eval_get_class_methods_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_class_methods(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_class_methods` symbol builtin. +pub(in crate::interpreter) fn eval_get_class_methods_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_get_class_methods_result(evaluated_args, context, values) +} + +/// Evaluates `get_class_methods()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_methods( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_methods_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_methods()` arguments. +pub(in crate::interpreter) fn eval_get_class_methods_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (class_name, target_is_object) = + eval_class_metadata_target_name(*target, context, values)?; + if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let names = eval_class_method_names_for_scope(&class_name, context, values)?; + eval_indexed_string_array_result(&names, values) +} + +/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. +fn eval_class_method_names_for_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(class_name) || context.has_enum(class_name) { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for name in context.class_method_names(class_name) { + let Some((declaring_class, method)) = context.class_method(class_name, &name) else { + eval_push_unique_method_name(&mut names, &mut seen, name); + continue; + }; + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { + eval_push_unique_method_name(&mut names, &mut seen, name); + } + } + eval_add_current_scope_private_method_names( + &mut names, &mut seen, class_name, context, values, + )?; + eval_add_native_parent_method_names(&mut names, &mut seen, class_name, context, values)?; + return Ok(names); + } + if context.has_interface(class_name) { + return Ok(context.interface_method_names(class_name)); + } + if let Some(trait_decl) = context.trait_decl(class_name) { + return Ok(trait_decl + .methods() + .iter() + .filter(|method| method.visibility() == EvalVisibility::Public) + .map(|method| method.name().to_string()) + .collect()); + } + let method_names = values.reflection_method_names(class_name)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; + let mut seen = names + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect::>(); + eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; + Ok(names) +} + +/// Filters generated runtime methods to the surface visible from the current eval scope. +fn eval_visible_runtime_method_names( + lookup_class_name: &str, + names: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut result = Vec::new(); + for name in names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(lookup_class_name, &name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { + result.push(name); + } + } + Ok(result) +} + +/// Adds generated/AOT parent method names inherited by one eval class. +fn eval_add_native_parent_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(()); + }; + let method_names = values.reflection_method_names(&parent)?; + let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let parent_names = eval_visible_runtime_method_names(&parent, parent_names, context, values)?; + for name in parent_names { + eval_push_unique_method_name(names, seen, name); + } + Ok(()) +} + +/// Adds private methods declared by the current eval scope when PHP would expose them. +fn eval_add_current_scope_private_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(current_class) = context.current_class_scope() else { + return Ok(()); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(()); + } + if let Some(class) = context.class(current_class) { + for method in class.methods() { + if method.visibility() == EvalVisibility::Private { + eval_push_unique_method_name(names, seen, method.name().to_string()); + } + } + return Ok(()); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(()); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(()); + } + let method_names = values.reflection_method_names(current_class)?; + let current_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + for name in current_names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(current_class, &name, values)? + else { + continue; + }; + if visibility == EvalVisibility::Private + && eval_same_class_metadata_name(&declaring_class, current_class) + { + eval_push_unique_method_name(names, seen, name); + } + } + Ok(()) +} + +/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. +fn eval_push_unique_method_name( + names: &mut Vec, + seen: &mut HashSet, + name: String, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs new file mode 100644 index 0000000000..ca11620504 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs @@ -0,0 +1,227 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_class_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval-declared defaults are materialized in the declaring class scope. +//! - Generated/AOT defaults use native callable default metadata when present. + +eval_builtin! { + name: "get_class_vars", + area: Symbols, + params: [r#class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::{ + eval_class_relation_name_exists, eval_resolved_class_metadata_name, + eval_runtime_property_access_metadata, eval_runtime_string_array_to_vec, +}; +use std::collections::HashSet; + +/// Dispatches direct eval calls for the `get_class_vars` symbol builtin. +pub(in crate::interpreter) fn eval_get_class_vars_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_class_vars(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_class_vars` symbol builtin. +pub(in crate::interpreter) fn eval_get_class_vars_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_get_class_vars_result(evaluated_args, context, values) +} + +/// Evaluates `get_class_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_vars_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_vars()` arguments. +pub(in crate::interpreter) fn eval_get_class_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let class_name = eval_resolved_class_metadata_name(*target, context, values)?; + if context.has_class(&class_name) || context.has_enum(&class_name) { + return eval_dynamic_class_vars_result(&class_name, context, values); + } + if context.has_trait(&class_name) { + return eval_dynamic_trait_vars_result(&class_name, context, values); + } + if context.has_interface(&class_name) { + return values.assoc_new(0); + } + if eval_class_relation_name_exists(&class_name, context, values)? { + return eval_runtime_class_vars_result(&class_name, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds `get_class_vars()` for an eval-declared class or enum. +fn eval_dynamic_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(0)?; + let mut emitted_keys = HashSet::new(); + if let Some(enum_decl) = context.enum_decl(class_name) { + let name_value = values.null()?; + result = eval_add_class_var_entry(result, "name", name_value, values)?; + emitted_keys.insert(String::from("name")); + if enum_decl.backing_type().is_some() { + let value_value = values.null()?; + result = eval_add_class_var_entry(result, "value", value_value, values)?; + emitted_keys.insert(String::from("value")); + } + } + for class in context.class_chain(class_name).into_iter().rev() { + for property in class.properties() { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + { + continue; + } + let value = + eval_class_vars_property_default_value(class.name(), property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + } + Ok(result) +} + +/// Builds `get_class_vars()` for an eval-declared trait. +fn eval_dynamic_trait_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(trait_decl) = context.trait_decl(class_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + let trait_name = trait_decl.name().to_string(); + let properties = trait_decl.properties().to_vec(); + let mut result = values.assoc_new(properties.len())?; + let mut emitted_keys = HashSet::new(); + for property in properties { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() + { + continue; + } + let value = eval_class_vars_property_default_value(&trait_name, &property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + Ok(result) +} + +/// Builds `get_class_vars()` data for generated/AOT class metadata. +fn eval_runtime_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let mut result = values.assoc_new(declared_names.len())?; + let mut emitted_keys = HashSet::new(); + for property_name in declared_names { + if emitted_keys.contains(&property_name) { + continue; + } + let Some((declaring_class, visibility, _is_static)) = + eval_runtime_property_access_metadata(class_name, &property_name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + continue; + } + let value = eval_runtime_class_var_default_value( + class_name, + &declaring_class, + &property_name, + context, + values, + )?; + result = eval_add_class_var_entry(result, &property_name, value, values)?; + emitted_keys.insert(property_name); + } + Ok(result) +} + +/// Materializes one eval-declared property default for `get_class_vars()`. +fn eval_class_vars_property_default_value( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = property.default() else { + return values.null(); + }; + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} + +/// Materializes one generated/AOT property default for `get_class_vars()`. +fn eval_runtime_class_var_default_value( + runtime_class: &str, + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(default) = context + .native_property_default(declaring_class, property_name) + .or_else(|| context.native_property_default(runtime_class, property_name)) + { + return materialize_native_callable_default(&default, context, values); + } + values.null() +} + +/// Adds one string-keyed class variable value to an associative result array. +fn eval_add_class_var_entry( + result: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(property_name)?; + values.array_set(result, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs new file mode 100644 index 0000000000..ecf82b10a2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_declared_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - The shared `get_declared_*` array builder lives here because classes are +//! the primary declaration table and the interface/trait variants reuse it. + +eval_builtin! { + name: "get_declared_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_declared_classes()` calls. +pub(in crate::interpreter) fn eval_get_declared_classes_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_declared_symbols("get_declared_classes", args, context, values) +} + +/// Evaluates materialized `get_declared_classes()` arguments. +pub(in crate::interpreter) fn eval_get_declared_classes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + eval_get_declared_symbols_result("get_declared_classes", context, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. +pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( + name: &str, + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) +} + +/// Builds an indexed array for eval-visible declared class-like names. +pub(in crate::interpreter) fn eval_get_declared_symbols_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "get_declared_classes" => { + eval_dynamic_string_array_result(context.declared_class_names(), values) + } + "get_declared_interfaces" => { + eval_dynamic_string_array_result(context.declared_interface_names(), values) + } + "get_declared_traits" => { + eval_dynamic_string_array_result(context.declared_trait_names(), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds one indexed PHP array from runtime-owned strings. +fn eval_dynamic_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs new file mode 100644 index 0000000000..05baddd173 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry for `get_declared_interfaces`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Array construction is shared with `get_declared_classes()`. + +eval_builtin! { + name: "get_declared_interfaces", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_declared_interfaces()` calls. +pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::get_declared_classes::eval_builtin_get_declared_symbols( + "get_declared_interfaces", + args, + context, + values, + ) +} + +/// Evaluates materialized `get_declared_interfaces()` arguments. +pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + super::get_declared_classes::eval_get_declared_symbols_result( + "get_declared_interfaces", + context, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs new file mode 100644 index 0000000000..1549d983ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry for `get_declared_traits`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Array construction is shared with `get_declared_classes()`. + +eval_builtin! { + name: "get_declared_traits", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_declared_traits()` calls. +pub(in crate::interpreter) fn eval_get_declared_traits_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::get_declared_classes::eval_builtin_get_declared_symbols( + "get_declared_traits", + args, + context, + values, + ) +} + +/// Evaluates materialized `get_declared_traits()` arguments. +pub(in crate::interpreter) fn eval_get_declared_traits_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + super::get_declared_classes::eval_get_declared_symbols_result( + "get_declared_traits", + context, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs new file mode 100644 index 0000000000..48b6687604 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs @@ -0,0 +1,355 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_object_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Declared eval properties use storage-name filtering so inaccessible +//! protected/private slots do not leak as public dynamic properties. + +eval_builtin! { + name: "get_object_vars", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::{ + eval_object_class_metadata_name, eval_runtime_property_access_metadata, + eval_runtime_string_array_to_vec, +}; +use std::collections::HashSet; + +/// Dispatches direct eval calls for the `get_object_vars` symbol builtin. +pub(in crate::interpreter) fn eval_get_object_vars_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_object_vars(args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_object_vars` symbol builtin. +pub(in crate::interpreter) fn eval_get_object_vars_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_get_object_vars_result(evaluated_args, context, values) +} + +/// Evaluates `get_object_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_object_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_object_vars_result(&[object], context, values) +} + +/// Evaluates materialized `get_object_vars()` arguments. +pub(in crate::interpreter) fn eval_get_object_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let Ok(identity) = values.object_identity(*object) else { + return eval_public_object_vars_result(*object, values); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_object_class_metadata_name(*object, context, values)?; + return eval_runtime_object_vars_result(*object, &class_name, context, values); + }; + let class_name = class.name().to_string(); + eval_dynamic_object_vars_result(*object, &class_name, context, values) +} + +/// Builds `get_object_vars()` for an eval-declared object. +fn eval_dynamic_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + let mut emitted_keys = HashSet::new(); + let storage_keys = eval_declared_object_storage_names(class_name, context); + result = eval_add_enum_object_vars(result, object, class_name, &mut emitted_keys, context, values)?; + result = eval_add_declared_object_vars( + result, + object, + class_name, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) +} + +/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. +fn eval_runtime_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(declared_names.len() + property_count)?; + let mut emitted_keys = HashSet::new(); + result = eval_add_runtime_scope_private_object_vars( + result, + object, + &mut emitted_keys, + context, + values, + )?; + result = eval_add_runtime_declared_object_vars( + result, + object, + class_name, + &declared_names, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) +} + +/// Adds generated/AOT private properties declared by the current eval class scope. +fn eval_add_runtime_scope_private_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(result); + }; + if !values.object_is_a(object, current_class, false)? { + return Ok(result); + } + let property_names = values.reflection_property_names(current_class)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + for property_name in declared_names { + let Some((_, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, &property_name, values)? + else { + continue; + }; + if is_static + || visibility != EvalVisibility::Private + || emitted_keys.contains(&property_name) + || !values.property_is_initialized(object, &property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(&property_name)?; + let value = values.property_get(object, &property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds generated/AOT declared instance properties visible from the current eval scope. +fn eval_add_runtime_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + property_names: &[String], + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for property_name in property_names { + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(class_name, property_name, values)? + else { + continue; + }; + if is_static + || validate_eval_member_access(&declaring_class, visibility, context).is_err() + || emitted_keys.contains(property_name) + || !values.property_is_initialized(object, property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(property_name)?; + let value = values.property_get(object, property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds synthetic enum properties exposed by PHP enum case objects. +fn eval_add_enum_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(enum_decl) = context.enum_decl(class_name) else { + return Ok(result); + }; + let is_backed = enum_decl.backing_type().is_some(); + result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; + if is_backed { + result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; + } + Ok(result) +} + +/// Adds declared instance properties visible from the current eval scope. +fn eval_add_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + || emitted_keys.contains(property.name()) + { + continue; + } + let storage_property_name = eval_instance_property_storage_name(class.name(), property); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + continue; + } + result = eval_add_object_var( + result, + object, + property.name(), + emitted_keys, + context, + values, + )?; + } + } + Ok(result) +} + +/// Adds one visible object variable to an associative result array. +fn eval_add_object_var( + result: RuntimeCellHandle, + object: RuntimeCellHandle, + property_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + emitted_keys.insert(property_name.to_string()); + let key = values.string(property_name)?; + let value = eval_property_get_result(object, property_name, context, values)?; + values.array_set(result, key, value) +} + +/// Adds public dynamic properties that are not declared storage slots. +fn eval_add_dynamic_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + storage_keys: &HashSet, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || !emitted_keys.insert(key_name.clone()) + { + continue; + } + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns physical storage names used by declared eval object properties. +fn eval_declared_object_storage_names( + class_name: &str, + context: &ElephcEvalContext, +) -> HashSet { + let mut names = HashSet::new(); + for class in context.class_chain(class_name) { + for property in class.properties() { + names.insert(eval_instance_property_storage_name(class.name(), property)); + } + } + names +} + +/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. +fn eval_public_object_vars_result( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether an object has a public bridge-visible property by exact name. +pub(in crate::interpreter) fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs new file mode 100644 index 0000000000..1e28c6fdee --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_parent_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval-created object and class-string parents are resolved before runtime fallback. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_parent_class", + area: Symbols, + params: [object_or_class = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_parent_class(...)` calls. +pub(in crate::interpreter) fn eval_get_parent_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_get_parent_class(args, context, scope, values) +} + +/// Evaluates materialized `get_parent_class(...)` arguments. +pub(in crate::interpreter) fn eval_get_parent_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +pub(in crate::interpreter) fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => { + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_parent_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_parent_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return values.string(""); + }; + let class_name = values.string(class_name.trim_start_matches('\\'))?; + eval_get_parent_class_result(class_name, context, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +pub(in crate::interpreter) fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object_or_class) { + if let Some(class_name) = context.dynamic_object_class_name(identity) { + if let Some(parent) = context.class_parent_names(&class_name).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } + if values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let name = values.string_bytes(object_or_class)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + if context.class(&name).is_some() { + if let Some(parent) = context.class_parent_names(&name).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } + values.parent_class_name(object_or_class) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs new file mode 100644 index 0000000000..3b891bdc13 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `get_resource_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Resource id/type support is shared with `get_resource_type()`. + +eval_builtin! { + name: "get_resource_id", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_resource_id(...)` calls. +pub(in crate::interpreter) fn eval_get_resource_id_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_resource_introspection("get_resource_id", args, context, scope, values) +} + +/// Evaluates materialized `get_resource_id(...)` arguments. +pub(in crate::interpreter) fn eval_get_resource_id_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [resource] => eval_resource_introspection_result("get_resource_id", *resource, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +pub(in crate::interpreter) fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +pub(in crate::interpreter) fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs new file mode 100644 index 0000000000..bd500e3ff1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry for `get_resource_type`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Resource introspection implementation is shared with `get_resource_id()`. + +eval_builtin! { + name: "get_resource_type", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `get_resource_type(...)` calls through the `get_resource_id` owner. +pub(in crate::interpreter) fn eval_get_resource_type_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::get_resource_id::eval_builtin_resource_introspection( + "get_resource_type", + args, + context, + scope, + values, + ) +} + +/// Evaluates materialized `get_resource_type(...)` arguments through the `get_resource_id` owner. +pub(in crate::interpreter) fn eval_get_resource_type_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [resource] => super::get_resource_id::eval_resource_introspection_result( + "get_resource_type", + *resource, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs new file mode 100644 index 0000000000..e64befd12e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs @@ -0,0 +1,85 @@ +//! Purpose: +//! Eval registry entry and implementation for `interface_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Lookup checks eval interface declarations before generated/AOT runtime metadata. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "interface_exists", + area: Symbols, + params: [interface, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `interface_exists(...)` calls against eval and generated metadata. +pub(in crate::interpreter) fn eval_interface_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_interface_exists(args, context, scope, values) +} + +/// Evaluates materialized `interface_exists(...)` arguments. +pub(in crate::interpreter) fn eval_interface_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_interface_exists_result(evaluated_args, context, values) +} + +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +pub(in crate::interpreter) fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, context, values)?, + [name, _autoload] => eval_interface_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. +pub(in crate::interpreter) fn eval_interface_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs new file mode 100644 index 0000000000..8545215abd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs @@ -0,0 +1,276 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_a`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval-created classes are checked first, then generated/AOT object and +//! interface metadata fills inherited relationships. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_a", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(false)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `is_a(...)` calls over eval boxed object cells and class strings. +pub(in crate::interpreter) fn eval_is_a_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_is_a_relation("is_a", args, context, scope, values) +} + +/// Evaluates materialized `is_a(...)` arguments. +pub(in crate::interpreter) fn eval_is_a_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_a_relation_result("is_a", evaluated_args, context, values) +} + +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +pub(in crate::interpreter) fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +pub(in crate::interpreter) fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_like_name(target_class) + .unwrap_or_else(|| target_class.to_string()); + let is_object = values.type_tag(object_or_class)? == 6; + let exclude_self = name == "is_subclass_of"; + let result = if is_object { + dynamic_object_is_a( + object_or_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), + Ok, + )? + } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let source_class = values.string_bytes(object_or_class)?; + let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let resolved_source_class = context + .resolve_class_like_name(&source_class) + .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); + if context.class(&resolved_source_class).is_some() { + eval_class_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.interface(&resolved_source_class).is_some() { + eval_interface_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.trait_decl(&resolved_source_class).is_some() { + !exclude_self + && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) + } else { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } + } else if allow_string { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } else { + false + }; + values.bool_value(result) +} + +/// Returns whether an interface string source satisfies a class-like target. +fn eval_interface_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !exclude_self && eval_class_like_name_matches(source_class, target_class) { + return Ok(true); + } + Ok(eval_interface_runtime_parent_names(source_class, context, values)? + .iter() + .any(|parent| eval_class_like_name_matches(parent, target_class))) +} + +/// Returns whether an eval class-string source satisfies a class-like target. +fn eval_class_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.class_is_a(source_class, target_class, exclude_self) { + return Ok(true); + } + Ok(eval_class_runtime_interface_names(source_class, context, values)? + .iter() + .any(|interface| eval_class_like_name_matches(interface, target_class))) +} + +/// Returns eval class interfaces plus generated/AOT inherited interface names. +fn eval_class_runtime_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus generated/AOT inherited interface names. +fn eval_interface_runtime_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Copies a runtime string array into Rust-owned names. +fn eval_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + let bytes = values.string_bytes(value)?; + result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); + } + Ok(result) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns whether two class-like names match PHP's case-insensitive class-name rules. +fn eval_class_like_name_matches(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns whether an eval-created object matches a dynamic class/interface target. +pub(in crate::interpreter) fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let identity = values.object_identity(object)?; + if context.dynamic_object_is_class(identity, "Closure") { + return Ok(Some( + !exclude_self && eval_class_like_name_matches("Closure", target_class), + )); + } + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(None); + }; + if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { + return Ok(Some(true)); + } + if context.class_native_parent_name(class.name()).is_some() { + return values + .object_is_a(object, target_class, exclude_self) + .map(Some); + } + Ok(Some(false)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs new file mode 100644 index 0000000000..96d212fa66 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs @@ -0,0 +1,524 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_callable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct and dynamic-ref paths preserve `$callable_name` writeback. +//! - Syntax-only callable checks avoid resolving non-object string targets. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_callable", + area: Symbols, + params: [ + value, + syntax_only = EvalBuiltinDefaultValue::Bool(false), + callable_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [callable_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `is_callable(...)` calls inside an eval fragment. +pub(in crate::interpreter) fn eval_is_callable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_is_callable(args, context, scope, values) +} + +/// Evaluates materialized `is_callable(...)` arguments. +pub(in crate::interpreter) fn eval_is_callable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_with_values(evaluated_args, context, values) +} + +/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. +pub(in crate::interpreter) fn eval_builtin_is_callable_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_is_callable_call_with_evaluated_args_from_scope( + &evaluated_args, + Some(scope), + context, + values, + ) +} + +/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. +pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) +} + +/// Evaluates materialized `is_callable()` args with optional special-class callable scope. +fn eval_is_callable_call_with_evaluated_args_from_scope( + evaluated_args: &[EvaluatedCallArg], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["value", "syntax_only", "callable_name"], + evaluated_args, + false, + )?; + let value = required_evaluated_ref_arg(&bound, 0)?; + let syntax_only = optional_evaluated_ref_arg(&bound, 1) + .map(|arg| values.truthy(arg.value)) + .transpose()? + .unwrap_or(false); + let callable_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + eval_is_callable_result( + value.value, + syntax_only, + callable_name_target.as_ref(), + lexical_scope, + context, + values, + ) +} + +/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. +pub(in crate::interpreter) fn eval_is_callable_with_values( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_is_callable_result(*value, false, None, None, context, values), + [value, syntax_only] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + [value, syntax_only, _callable_name] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates positional `is_callable()` arguments inside an eval fragment. +pub(in crate::interpreter) fn eval_builtin_is_callable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_is_callable_result(value, false, None, Some(scope), context, values) + } + [value, syntax_only] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) + } + [value, syntax_only, callable_name] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + let (_, callable_name_target) = + eval_call_arg_value(callable_name, context, scope, values)?; + let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; + eval_is_callable_result( + value, + syntax_only, + Some(&callable_name_target), + Some(scope), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one runtime value is callable from the current eval scope. +pub(in crate::interpreter) fn eval_is_callable_value( + value: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match lexical_scope { + Some(scope) => eval_callable_from_scope(value, context, scope, values), + None => eval_callable(value, context, values), + }; + let Ok(callback) = callback else { + return Ok(false); + }; + eval_callable_probe_exists(&callback, context, values) +} + +/// Evaluates `is_callable()` and writes PHP's display callable name when requested. +fn eval_is_callable_result( + value: RuntimeCellHandle, + syntax_only: bool, + callable_name_target: Option<&EvalReferenceTarget>, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable_name = callable_name_target + .map(|_| eval_callable_display_name(value, context, values)) + .transpose()?; + let callable = if syntax_only { + eval_is_callable_syntax_only(value, context, values)? + } else { + eval_is_callable_value(value, lexical_scope, context, values)? + }; + if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { + let name = values.string(name)?; + eval_write_direct_ref_target( + target, + name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(callable) +} + +/// Returns PHP's syntax-only callable result without requiring the target to exist. +fn eval_is_callable_syntax_only( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + return Ok(true); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + return eval_is_callable_value(value, None, context, values); + } + if values.is_array_like(value)? { + return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); + } + Ok(false) +} + +/// Builds PHP's `$callable_name` output for one probed callable value. +fn eval_callable_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(value)?; + return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let class_name = eval_callable_object_class_name(value, context, values)?; + return Ok(format!("{class_name}::__invoke")); + } + if values.is_array_like(value)? { + return Ok(eval_callable_array_display_name(value, context, values)? + .unwrap_or_else(|| String::from("Array"))); + } + let string = values.cast_string(value)?; + let bytes = values.string_bytes(string); + values.release(string)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Builds PHP's `$callable_name` output for a syntactically valid callable array. +fn eval_callable_array_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(value)? != 2 { + return Ok(None); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(value, zero)?; + let method = values.array_get(value, one)?; + if values.type_tag(method)? != EVAL_TAG_STRING { + return Ok(None); + } + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + let receiver_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?, + _ => return Ok(None), + }; + Ok(Some(format!("{receiver_name}::{method}"))) +} + +/// Returns the PHP-visible class name used when formatting callable object probes. +fn eval_callable_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if context.closure_object_target(identity).is_some() { + return Ok(String::from("Closure")); + } + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + runtime_object_class_name(object, values) +} + +/// Returns whether a normalized eval callback has an invokable target. +fn eval_callable_probe_exists( + callback: &EvaluatedCallable, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), + EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), + EvaluatedCallable::InvokableObject { object } => { + eval_object_method_callable_probe(*object, "__invoke", context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_object_method_callable_probe(*object, method, context, values) + } + } + EvaluatedCallable::StaticMethod { + class_name, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_static_method_callable_probe(class_name, method, context, values) + } + } + } +} + +/// Returns whether one object method can be called from the current eval scope. +fn eval_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(false); + }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(true); + } + let Some(class) = context.dynamic_object_class(identity) else { + return eval_aot_object_method_callable_probe(object, method_name, context, values); + }; + if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { + return Ok(true); + } + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(class.name(), method_name, context) + else { + if eval_dynamic_class_native_method_callable_probe( + class.name(), + method_name, + context, + values, + )? { + return Ok(true); + } + return Ok(eval_instance_magic_method_callable_probe( + class.name(), + context, + )); + }; + if method.is_abstract() { + return Ok(false); + } + if method_name.eq_ignore_ascii_case("__invoke") { + return Ok(true); + } + Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_instance_magic_method_callable_probe(class.name(), context)) +} + +/// Returns whether one static method can be called from the current eval scope. +fn eval_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(true); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() || method.is_abstract() { + return Ok(false); + } + return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) + .is_ok() + || eval_static_magic_method_callable_probe(&class_name, context)); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if eval_static_magic_method_callable_probe(&class_name, context) { + return Ok(true); + } + if let Some(parent) = context.class_native_parent_name(&class_name) { + return eval_aot_static_method_callable_probe( + &parent, + method_name, + context, + values, + ); + } + return Ok(false); + } + eval_aot_static_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether a generated/AOT object method can be called from the current eval scope. +fn eval_aot_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = runtime_object_class_name(object, values)?; + eval_aot_class_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether an eval class can call a generated/AOT parent instance method. +fn eval_dynamic_class_native_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_aot_class_method_callable_probe(&parent, method_name, context, values) +} + +/// Returns whether one generated/AOT class instance method can be called from eval. +fn eval_aot_class_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); + }; + if is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) +} + +/// Returns whether a generated/AOT static method can be called from the current eval scope. +fn eval_aot_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return eval_aot_static_magic_method_callable_probe(class_name, context, values); + }; + if !is_static || is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) +} + +/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. +fn eval_aot_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. +fn eval_aot_static_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Returns whether an eval class has a callable instance `__call()` fallback. +fn eval_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a callable static `__callStatic()` fallback. +fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs new file mode 100644 index 0000000000..7e461b78cf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry for `is_subclass_of`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Relationship semantics are shared with `is_a()` because PHP only changes +//! the self-match and default string-allowance rules. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_subclass_of", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `is_subclass_of(...)` calls through the `is_a` relation owner. +pub(in crate::interpreter) fn eval_is_subclass_of_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::is_a::eval_builtin_is_a_relation("is_subclass_of", args, context, scope, values) +} + +/// Evaluates materialized `is_subclass_of(...)` arguments through the `is_a` relation owner. +pub(in crate::interpreter) fn eval_is_subclass_of_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::is_a::eval_is_a_relation_result("is_subclass_of", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs new file mode 100644 index 0000000000..3e6f882eba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs @@ -0,0 +1,176 @@ +//! Purpose: +//! Eval registry entry and implementation for `isset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so missing variables, object properties, +//! array offsets, and ArrayAccess values keep PHP `isset()` semantics. + +eval_builtin! { + name: "isset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_isset_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_isset(args, context, scope, values) +} + +/// Evaluates callable `isset(...)` over already materialized values. +pub(in crate::interpreter) fn eval_isset_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_isset_result(evaluated_args, values) +} + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates callable `isset(...)` over already materialized values. +pub(in crate::interpreter) fn eval_isset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for value in evaluated_args { + if values.is_null(*value)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +pub(in crate::interpreter) fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + return eval_static_property_isset_result(class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + return eval_static_property_isset_result(&class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_static_property_isset_result(&class_name, &property, context, values); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_isset_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + +/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. +pub(in crate::interpreter) fn eval_array_access_isset_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} + +/// Returns whether an object operand implements the eval-visible `ArrayAccess` contract. +fn eval_array_access_object_matches( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let target_class = "ArrayAccess"; + super::is_a::dynamic_object_is_a(object, target_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, target_class, false), Ok) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs new file mode 100644 index 0000000000..149e5ab854 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs @@ -0,0 +1,362 @@ +//! Purpose: +//! Eval registry entry and implementation for `method_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared member-existence logic for `property_exists()` lives here. + +eval_builtin! { + name: "method_exists", + area: Symbols, + params: [object_or_class, method], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; +use super::super::{ + eval_class_metadata_is_a, eval_class_metadata_name, eval_class_relation_name_exists, + eval_object_class_metadata_name, eval_resolved_class_metadata_name, + eval_runtime_property_access_metadata, eval_same_class_metadata_name, +}; +use super::get_object_vars::eval_object_public_property_exists; + +/// Dispatches direct eval calls for the `method_exists` symbol builtin. +pub(in crate::interpreter) fn eval_method_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_member_exists("method_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `method_exists` symbol builtin. +pub(in crate::interpreter) fn eval_method_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_member_exists_result("method_exists", evaluated_args, context, values) +} + +/// Evaluates `method_exists()` or `property_exists()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_member_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + let member = eval_expr(member, context, scope, values)?; + eval_member_exists_result(name, &[target, member], context, values) +} + +/// Evaluates materialized `method_exists()` or `property_exists()` arguments. +pub(in crate::interpreter) fn eval_member_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let member = eval_class_metadata_name(*member, values)?; + let exists = match name { + "method_exists" => eval_method_exists_target(*target, &member, context, values)?, + "property_exists" => eval_property_exists_target(*target, &member, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Resolves a `method_exists()` target and applies PHP object-vs-string lookup rules. +fn eval_method_exists_target( + target: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, true, context, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, false, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a `property_exists()` target and applies declared and dynamic-property lookup rules. +fn eval_property_exists_target( + target: RuntimeCellHandle, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + if eval_property_exists_on_class(&class_name, property_name, context, values)? { + return Ok(true); + } + if eval_current_scope_private_property_exists_on_object( + &class_name, + property_name, + context, + values, + )? { + return Ok(true); + } + eval_object_public_property_exists(target, property_name, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_property_exists_on_class(&class_name, property_name, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Checks method metadata for one resolved class-like name. +fn eval_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + if target_is_object { + if context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + return Ok(true); + } + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); + } + if context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + let Some((declaring_class, method)) = context.class_method(class_name, method_name) + else { + return Ok(true); + }; + return Ok(method.visibility() != EvalVisibility::Private + || declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))); + } + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); + } + if context.has_interface(class_name) { + return Ok(context + .interface_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if context.has_trait(class_name) { + return Ok(context + .trait_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if target_is_object { + return eval_native_method_exists_on_class( + class_name, + class_name, + method_name, + target_is_object, + context, + values, + ); + } + eval_native_method_exists_on_class( + class_name, + class_name, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT parent method metadata inherited by one eval class. +fn eval_native_parent_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_method_exists_on_class( + class_name, + &parent, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT method metadata for method_exists() semantics. +fn eval_native_method_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, _)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + lookup_class_name, + method_name, + context, + values, + )? + else { + return Ok(false); + }; + if target_is_object || visibility != EvalVisibility::Private { + return Ok(true); + } + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) +} + +/// Checks property metadata for one resolved class-like name. +fn eval_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + if context + .class_property_names(class_name) + .iter() + .any(|name| name == property_name) + { + return Ok(true); + } + return eval_native_parent_property_exists_on_class( + class_name, + property_name, + context, + values, + ); + } + if context.has_interface(class_name) { + return Ok(context + .interface_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + if context.has_trait(class_name) { + return Ok(context + .trait_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + eval_native_property_exists_on_class(class_name, class_name, property_name, values) +} + +/// Checks generated/AOT parent property metadata inherited by one eval class. +fn eval_native_parent_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_property_exists_on_class(class_name, &parent, property_name, values) +} + +/// Checks generated/AOT property metadata for property_exists() semantics. +fn eval_native_property_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _)) = + eval_runtime_property_access_metadata(lookup_class_name, property_name, values)? + else { + return Ok(false); + }; + if visibility != EvalVisibility::Private { + return Ok(true); + } + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) +} + +/// Checks private instance properties declared by the current scope for object targets. +fn eval_current_scope_private_property_exists_on_object( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(false); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(false); + } + if let Some(class) = context.class(current_class) { + return Ok(class.properties().iter().any(|property| { + property.name() == property_name + && property.visibility() == EvalVisibility::Private + && !property.is_static() + })); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(false); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(false); + } + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, property_name, values)? + else { + return Ok(false); + }; + Ok(visibility == EvalVisibility::Private + && !is_static + && eval_same_class_metadata_name(&declaring_class, current_class)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs new file mode 100644 index 0000000000..9e6a81c38b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `property_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Shared member-existence logic lives in `method_exists`. + +eval_builtin! { + name: "property_exists", + area: Symbols, + params: [object_or_class, property], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `property_exists` symbol builtin. +pub(in crate::interpreter) fn eval_property_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::method_exists::eval_builtin_member_exists("property_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `property_exists` symbol builtin. +pub(in crate::interpreter) fn eval_property_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::method_exists::eval_member_exists_result("property_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs new file mode 100644 index 0000000000..6d38e5e963 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_autoload`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval mirrors the main backend's conservative no-op autoload behavior. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload", + area: Symbols, + params: [class, file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload(...)` calls as a no-op stub. +pub(in crate::interpreter) fn eval_spl_autoload_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_autoload_void("spl_autoload", args, context, scope, values) +} + +/// Evaluates materialized `spl_autoload(...)` arguments as a no-op stub. +pub(in crate::interpreter) fn eval_spl_autoload_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_void_result("spl_autoload", evaluated_args, values) +} + +/// Evaluates void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_void( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if args.len() == 1 => {} + "spl_autoload" if (1..=2).contains(&args.len()) => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_spl_autoload_void_result(name, args, values) +} + +/// Evaluates materialized void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_spl_autoload_void_result( + name: &str, + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if evaluated_args.len() == 1 => values.null(), + "spl_autoload" if (1..=2).contains(&evaluated_args.len()) => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs new file mode 100644 index 0000000000..116ff1f4ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Eval registry entry for `spl_autoload_call`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - No-op stub behavior is shared with `spl_autoload()`. + +eval_builtin! { + name: "spl_autoload_call", + area: Symbols, + params: [class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload_call(...)` calls through the `spl_autoload` owner. +pub(in crate::interpreter) fn eval_spl_autoload_call_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::spl_autoload::eval_builtin_spl_autoload_void( + "spl_autoload_call", + args, + context, + scope, + values, + ) +} + +/// Evaluates materialized `spl_autoload_call(...)` arguments through the `spl_autoload` owner. +pub(in crate::interpreter) fn eval_spl_autoload_call_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::spl_autoload::eval_spl_autoload_void_result("spl_autoload_call", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs new file mode 100644 index 0000000000..37f152f305 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_autoload_extensions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - The extension list is eval-local mutable state on the context. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_extensions", + area: Symbols, + params: [file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload_extensions(...)` calls. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_autoload_extensions(args, context, scope, values) +} + +/// Evaluates materialized `spl_autoload_extensions(...)` arguments. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_extensions_result(evaluated_args, context, values) +} + +/// Evaluates `spl_autoload_extensions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_extensions( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match args { + [] => Vec::new(), + [extensions] => vec![eval_expr(extensions, context, scope, values)?], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_spl_autoload_extensions_result(&evaluated_args, context, values) +} + +/// Evaluates materialized `spl_autoload_extensions()` arguments. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => {} + [extensions] if values.type_tag(*extensions)? == EVAL_TAG_NULL => {} + [extensions] => { + let extensions = values.string_bytes(*extensions)?; + let extensions = String::from_utf8(extensions).map_err(|_| EvalStatus::RuntimeFatal)?; + context.set_spl_autoload_extensions(extensions); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + values.string(context.spl_autoload_extensions()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs new file mode 100644 index 0000000000..ec4de7ef4b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_autoload_functions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Eval models an empty autoload function table. + +eval_builtin! { + name: "spl_autoload_functions", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload_functions()` calls. +pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_autoload_functions(args, context, scope, values) +} + +/// Evaluates materialized `spl_autoload_functions()` arguments. +pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_functions_result(evaluated_args, values) +} + +/// Evaluates `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_functions( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_functions_result(args, values) +} + +/// Evaluates materialized `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_spl_autoload_functions_result( + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.array_new(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs new file mode 100644 index 0000000000..6c0e029f39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_autoload_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Registration is a conservative successful stub, mirroring the main backend. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_register", + area: Symbols, + params: [ + callback = EvalBuiltinDefaultValue::Null, + throw = EvalBuiltinDefaultValue::Bool(true), + prepend = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload_register(...)` calls as a successful stub. +pub(in crate::interpreter) fn eval_spl_autoload_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_autoload_bool("spl_autoload_register", args, context, scope, values) +} + +/// Evaluates materialized `spl_autoload_register(...)` arguments as a successful stub. +pub(in crate::interpreter) fn eval_spl_autoload_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_bool_result("spl_autoload_register", evaluated_args, values) +} + +/// Evaluates boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if args.len() <= 3 => {} + "spl_autoload_unregister" if args.len() == 1 => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + values.bool_value(true) +} + +/// Evaluates materialized boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_spl_autoload_bool_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if evaluated_args.len() <= 3 => values.bool_value(true), + "spl_autoload_unregister" if evaluated_args.len() == 1 => values.bool_value(true), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs new file mode 100644 index 0000000000..57b7ddd6bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Eval registry entry for `spl_autoload_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Registration stub behavior is shared with `spl_autoload_register()`. + +eval_builtin! { + name: "spl_autoload_unregister", + area: Symbols, + params: [callback], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_autoload_unregister(...)` calls through the registration owner. +pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::spl_autoload_register::eval_builtin_spl_autoload_bool( + "spl_autoload_unregister", + args, + context, + scope, + values, + ) +} + +/// Evaluates materialized `spl_autoload_unregister(...)` arguments through the registration owner. +pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::spl_autoload_register::eval_spl_autoload_bool_result( + "spl_autoload_unregister", + evaluated_args, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs new file mode 100644 index 0000000000..e3ad7ec773 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - The result is a fixed indexed array of SPL class names. + +eval_builtin! { + name: "spl_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::eval_static_string_array_result; +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_classes` symbol builtin. +pub(in crate::interpreter) fn eval_spl_classes_declared_call( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_classes(args, values) +} + +/// Dispatches evaluated-argument calls for the `spl_classes` symbol builtin. +pub(in crate::interpreter) fn eval_spl_classes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + eval_spl_classes_result(values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +pub(in crate::interpreter) fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs new file mode 100644 index 0000000000..d180480ad4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry for `spl_object_hash`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Object identity semantics are shared with `spl_object_id()`. + +eval_builtin! { + name: "spl_object_hash", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_object_hash(...)` calls through the `spl_object_id` owner. +pub(in crate::interpreter) fn eval_spl_object_hash_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::spl_object_id::eval_builtin_spl_object_identity( + "spl_object_hash", + args, + context, + scope, + values, + ) +} + +/// Evaluates materialized `spl_object_hash(...)` arguments through the `spl_object_id` owner. +pub(in crate::interpreter) fn eval_spl_object_hash_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [object] => { + super::spl_object_id::eval_spl_object_identity_result("spl_object_hash", *object, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs new file mode 100644 index 0000000000..1c567a6241 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and implementation for `spl_object_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - `spl_object_hash()` shares the same object-identity implementation. + +eval_builtin! { + name: "spl_object_id", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `spl_object_id(...)` calls. +pub(in crate::interpreter) fn eval_spl_object_id_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_spl_object_identity("spl_object_id", args, context, scope, values) +} + +/// Evaluates materialized `spl_object_id(...)` arguments. +pub(in crate::interpreter) fn eval_spl_object_id_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [object] => eval_spl_object_identity_result("spl_object_id", *object, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +pub(in crate::interpreter) fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs new file mode 100644 index 0000000000..46201190d8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Eval registry entry and implementation for `trait_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - The shared trait/enum existence probe lives here and `enum_exists()` +//! calls it explicitly. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "trait_exists", + area: Symbols, + params: [r#trait, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `trait_exists(...)` calls. +pub(in crate::interpreter) fn eval_trait_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_class_like_exists("trait_exists", args, context, scope, values) +} + +/// Evaluates materialized `trait_exists(...)` arguments. +pub(in crate::interpreter) fn eval_trait_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_like_exists_result("trait_exists", evaluated_args, context, values) +} + +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +pub(in crate::interpreter) fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, context, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +pub(in crate::interpreter) fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +pub(in crate::interpreter) fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), + "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs new file mode 100644 index 0000000000..ff2d897a23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Eval registry entry and implementation for `unset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so writable operands can be removed. + +eval_builtin! { + name: "unset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. +pub(in crate::interpreter) fn eval_unset_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_unset(args, context, scope, values) +} + +/// Evaluates callable `unset(...)` after values have already been materialized. +pub(in crate::interpreter) fn eval_unset_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_unset_result(evaluated_args, values) +} + +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. +pub(in crate::interpreter) fn eval_builtin_unset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + match arg { + EvalExpr::LoadVar(name) => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + values.null() +} + +/// Evaluates callable `unset(...)` after values have already been materialized. +pub(in crate::interpreter) fn eval_unset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs new file mode 100644 index 0000000000..cd794182c3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs @@ -0,0 +1,626 @@ +//! Purpose: +//! Executes procedural DateTime, DateInterval, DateTimeZone, and calendar aliases +//! that static elephc normally rewrites before type checking. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` +//! - `crate::interpreter::builtins::registry::dispatch` +//! +//! Key details: +//! - The eval parser cannot run the static name-resolver rewrite, so this module +//! mirrors the alias dispatch at runtime and delegates to native AOT bridges. +//! - This file is a deliberate >500 LoC single-scope runtime bridge for +//! procedural date/time aliases; splitting by alias would obscure the shared +//! fallback and timezone-table rules. + +use super::*; + +#[path = "../../../../../../src/list_id_prelude/table.rs"] +mod timezone_identifier_table; + +const EVAL_TZ_VERSION: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../elephc-tz/data/version.data")); +const EVAL_DATETIMEZONE_ALL: i64 = 2047; +const EVAL_DATETIMEZONE_PER_COUNTRY: i64 = 4096; +const EVAL_DATETIMEZONE_PER_COUNTRY_ERROR: &str = concat!( + "DateTimeZone::listIdentifiers(): Argument #2 ($countryCode) must be a two-letter ", + "ISO 3166-1 compatible country code when argument #1 ($timezoneGroup) is ", + "DateTimeZone::PER_COUNTRY", +); + +/// Attempts to execute one direct procedural date/time alias call. +pub(in crate::interpreter) fn eval_date_procedural_alias_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_date_alias_key(name).is_none() { + return Ok(None); + } + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args, context, values) +} + +/// Attempts to execute one procedural date/time alias from positional runtime values. +pub(in crate::interpreter) fn eval_date_procedural_alias_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let evaluated_args = evaluated_args + .iter() + .copied() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args, context, values) +} + +/// Attempts to execute one procedural date/time alias from evaluated call args. +pub(in crate::interpreter) fn eval_date_procedural_alias_with_evaluated_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(name) = eval_date_alias_key(name) else { + return Ok(None); + }; + if eval_date_alias_should_fall_back_to_builtin(&name, &evaluated_args) { + return Ok(None); + } + let args = positional_evaluated_arg_values(evaluated_args)?; + let result = eval_date_alias_result(&name, args, context, values)?; + Ok(Some(result)) +} + +/// Dispatches a normalized alias name to the equivalent runtime operation. +fn eval_date_alias_result( + name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "idate" => eval_idate_alias(args, context, values), + "mktime" | "gmmktime" => eval_mktime_alias(name, args, context, values), + "date_create" => eval_new_datetime_alias("DateTime", args, context, values), + "date_create_immutable" => { + eval_new_datetime_alias("DateTimeImmutable", args, context, values) + } + "date_create_from_format" => { + eval_static_alias("DateTime", "createFromFormat", args, context, values) + } + "date_create_immutable_from_format" => { + eval_static_alias("DateTimeImmutable", "createFromFormat", args, context, values) + } + "date_parse_from_format" => { + eval_static_alias("DateTime", "__elephc_date_parse_from_format", args, context, values) + } + "date_parse" => eval_static_alias("DateTime", "__elephc_date_parse", args, context, values), + "date_sun_info" => { + eval_static_alias("DateTime", "__elephc_date_sun_info", args, context, values) + } + "date_sunrise" => eval_date_sunfunc_alias(false, args, context, values), + "date_sunset" => eval_date_sunfunc_alias(true, args, context, values), + "strptime" => eval_static_alias("DateTime", "__elephc_strptime", args, context, values), + "timezone_name_from_abbr" => eval_static_alias( + "DateTime", + "__elephc_timezone_name_from_abbr", + args, + context, + values, + ), + "cal_to_jd" => eval_static_alias("DateTime", "__elephc_cal_to_jd", args, context, values), + "cal_from_jd" => { + eval_static_alias("DateTime", "__elephc_cal_from_jd", args, context, values) + } + "cal_days_in_month" => eval_static_alias( + "DateTime", + "__elephc_cal_days_in_month", + args, + context, + values, + ), + "cal_info" => eval_static_alias("DateTime", "__elephc_cal_info", args, context, values), + "gregoriantojd" => { + eval_static_alias("DateTime", "__elephc_gregoriantojd", args, context, values) + } + "jdtogregorian" => { + eval_static_alias("DateTime", "__elephc_jdtogregorian", args, context, values) + } + "juliantojd" => { + eval_static_alias("DateTime", "__elephc_juliantojd", args, context, values) + } + "jdtojulian" => eval_static_alias("DateTime", "__elephc_jdtojulian", args, context, values), + "frenchtojd" => { + eval_static_alias("DateTime", "__elephc_frenchtojd", args, context, values) + } + "jdtofrench" => eval_static_alias("DateTime", "__elephc_jdtofrench", args, context, values), + "jewishtojd" => { + eval_static_alias("DateTime", "__elephc_jewishtojd", args, context, values) + } + "jdtojewish" => eval_static_alias("DateTime", "__elephc_jdtojewish", args, context, values), + "jddayofweek" => { + eval_static_alias("DateTime", "__elephc_jddayofweek", args, context, values) + } + "jdmonthname" => { + eval_static_alias("DateTime", "__elephc_jdmonthname", args, context, values) + } + "jdtounix" => eval_static_alias("DateTime", "__elephc_jdtounix", args, context, values), + "unixtojd" => eval_unixtojd_alias(args, context, values), + "easter_days" => eval_easter_alias("__elephc_easter_days", args, context, values), + "easter_date" => eval_easter_alias("__elephc_easter_date", args, context, values), + "gettimeofday" => { + eval_static_alias("DateTime", "__elephc_gettimeofday", args, context, values) + } + "date_get_last_errors" => { + eval_static_alias("DateTime", "getLastErrors", args, context, values) + } + "strftime" => eval_strftime_alias(false, args, context, values), + "gmstrftime" => eval_strftime_alias(true, args, context, values), + "timezone_open" => eval_new_datetime_alias("DateTimeZone", args, context, values), + "timezone_identifiers_list" => eval_timezone_identifiers_alias(args, context, values), + "timezone_location_get" => eval_method_alias(args, 0, "getLocation", &[], context, values), + "timezone_transitions_get" => { + eval_method_alias_tail(args, 0, "getTransitions", context, values) + } + "timezone_abbreviations_list" => { + eval_static_alias("DateTimeZone", "listAbbreviations", args, context, values) + } + "timezone_version_get" => eval_timezone_version_alias(args, values), + "date_interval_create_from_date_string" => { + eval_static_alias("DateInterval", "createFromDateString", args, context, values) + } + "date_diff" => eval_method_alias_tail(args, 0, "diff", context, values), + "date_format" => eval_method_alias(args, 0, "format", &[1], context, values), + "date_add" => eval_method_alias(args, 0, "add", &[1], context, values), + "date_sub" => eval_method_alias(args, 0, "sub", &[1], context, values), + "date_modify" => eval_method_alias(args, 0, "modify", &[1], context, values), + "date_timestamp_get" => eval_method_alias(args, 0, "getTimestamp", &[], context, values), + "date_timestamp_set" => { + eval_method_alias(args, 0, "setTimestamp", &[1], context, values) + } + "date_timezone_get" => eval_method_alias(args, 0, "getTimezone", &[], context, values), + "date_timezone_set" => { + eval_method_alias(args, 0, "setTimezone", &[1], context, values) + } + "date_offset_get" => eval_method_alias(args, 0, "getOffset", &[], context, values), + "date_date_set" => eval_method_alias(args, 0, "setDate", &[1, 2, 3], context, values), + "date_isodate_set" => eval_method_alias_tail(args, 0, "setISODate", context, values), + "date_time_set" => eval_method_alias_tail(args, 0, "setTime", context, values), + "date_interval_format" => eval_method_alias(args, 0, "format", &[1], context, values), + "timezone_name_get" => eval_method_alias(args, 0, "getName", &[], context, values), + "timezone_offset_get" => eval_method_alias(args, 0, "getOffset", &[1], context, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Implements `idate()` as `intval(date(...))`. +fn eval_idate_alias( + args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match args.as_slice() { + [format] => eval_date_result("date", *format, None, context, values), + [format, timestamp] => eval_date_result("date", *format, Some(*timestamp), context, values), + _ => return Err(EvalStatus::RuntimeFatal), + }?; + let cast = values.cast_int(result); + values.release(result)?; + cast +} + +/// Implements `mktime()` and `gmmktime()` optional argument filling. +fn eval_mktime_alias( + name: &str, + args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 6 { + return Err(EvalStatus::RuntimeFatal); + } + let mut full = Vec::with_capacity(6); + let mut temps = Vec::new(); + let date_name = if name == "gmmktime" { "gmdate" } else { "date" }; + for (index, spec) in ["G", "i", "s", "n", "j", "Y"].into_iter().enumerate() { + if let Some(arg) = args.get(index) { + full.push(*arg); + } else { + let default = eval_current_date_part_int(date_name, spec, context, values)?; + temps.push(default); + full.push(default); + } + } + let result = eval_mktime_result( + name, full[0], full[1], full[2], full[3], full[4], full[5], context, values, + ); + for temp in temps { + values.release(temp)?; + } + result +} + +/// Constructs one native DateTime-family object and runs its constructor. +fn eval_new_datetime_alias( + class_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object(class_name)?; + if let Err(status) = + eval_native_constructor_with_evaluated_args(class_name, object, positional_args(args), context, values) + { + let _ = values.release(object); + return Err(status); + } + Ok(object) +} + +/// Calls one native/static method alias with positional arguments. +fn eval_static_alias( + class_name: &str, + method_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_method_call_result(class_name, method_name, positional_args(args), context, values) +} + +/// Calls the injected list-identifiers prelude function, falling back to the +/// synthetic method if the native prelude function is unavailable. +fn eval_timezone_identifiers_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.native_function("__elephc_list_identifiers") { + let bound_args = + bind_evaluated_native_function_args(&function, positional_args(args), context, values)?; + return eval_native_function_with_values(function, bound_args, context, values); + } + eval_timezone_identifiers_filtered_alias(args, context, values) +} + +/// Implements `timezone_identifiers_list()` filtering for eval-only programs. +fn eval_timezone_identifiers_filtered_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let group = eval_timezone_identifier_group(args.first().copied(), values)?; + let country = eval_timezone_identifier_country(args.get(1).copied(), values)?; + if group & EVAL_DATETIMEZONE_PER_COUNTRY != 0 && country.is_empty() { + return eval_timezone_identifiers_country_error(context, values); + } + let rows = eval_timezone_identifier_rows(group, &country); + let mut result = values.array_new(rows.len())?; + for (index, name) in rows.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns the requested DateTimeZone group mask, defaulting to `DateTimeZone::ALL`. +fn eval_timezone_identifier_group( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + value + .map(|value| eval_int_value(value, values)) + .unwrap_or(Ok(EVAL_DATETIMEZONE_ALL)) +} + +/// Returns the requested PER_COUNTRY country code, defaulting to the empty marker. +fn eval_timezone_identifier_country( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(String::new()); + }; + String::from_utf8(values.string_bytes(value)?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns the identifiers matching one DateTimeZone group/country selector. +fn eval_timezone_identifier_rows(group: i64, country: &str) -> Vec<&'static str> { + timezone_identifier_table::TIMEZONE_GROUPS_TABLE + .split(';') + .filter_map(|row| eval_timezone_identifier_row(row, group, country)) + .collect() +} + +/// Returns one table row's identifier when it matches the selector. +fn eval_timezone_identifier_row( + row: &'static str, + group: i64, + country: &str, +) -> Option<&'static str> { + let mut fields = row.split(','); + let name = fields.next()?; + let mask = fields.next()?.parse::().ok()?; + let row_country = fields.next()?; + if group & EVAL_DATETIMEZONE_PER_COUNTRY != 0 { + (row_country == country).then_some(name) + } else { + (mask & group != 0).then_some(name) + } +} + +/// Throws PHP's `ValueError` for PER_COUNTRY calls without a country code. +fn eval_timezone_identifiers_country_error( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(EVAL_DATETIMEZONE_PER_COUNTRY_ERROR)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Calls one object-method alias with selected argument indices. +fn eval_method_alias( + args: Vec, + object_index: usize, + method_name: &str, + arg_indices: &[usize], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(object) = args.get(object_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut method_args = Vec::with_capacity(arg_indices.len()); + for index in arg_indices { + let Some(arg) = args.get(*index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + method_args.push(arg); + } + eval_method_call_result(object, method_name, method_args, context, values) +} + +/// Calls one object-method alias with every argument after the receiver. +fn eval_method_alias_tail( + args: Vec, + object_index: usize, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(object) = args.get(object_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + let method_args = args + .iter() + .enumerate() + .filter_map(|(index, arg)| (index != object_index).then_some(*arg)) + .collect(); + eval_method_call_result(object, method_name, method_args, context, values) +} + +/// Implements `unixtojd($timestamp = time())`. +fn eval_unixtojd_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (args, temp) = match args.as_slice() { + [] => { + let now = eval_time_result(values)?; + (vec![now], Some(now)) + } + [_] => (args, None), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let result = eval_static_alias("DateTime", "__elephc_unixtojd", args, context, values); + if let Some(temp) = temp { + values.release(temp)?; + } + result +} + +/// Implements `easter_days()` and `easter_date()` default-year filling. +fn eval_easter_alias( + method_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let (args, temp) = if args.is_empty() { + let year = eval_current_date_part_int("date", "Y", context, values)?; + (vec![year], Some(year)) + } else { + (args, None) + }; + let result = eval_static_alias("DateTime", method_name, args, context, values); + if let Some(temp) = temp { + values.release(temp)?; + } + result +} + +/// Implements `date_sunrise()` and `date_sunset()` by prepending the synthetic flag. +fn eval_date_sunfunc_alias( + sunset: bool, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=6).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let which = values.int(if sunset { 1 } else { 0 })?; + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(which); + call_args.extend(args); + let result = eval_static_alias("DateTime", "__elephc_date_sunfunc", call_args, context, values); + values.release(which)?; + result +} + +/// Implements `strftime()` and `gmstrftime()` by adding timestamp and UTC flag. +fn eval_strftime_alias( + utc: bool, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() != 1 && args.len() != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut call_args = Vec::with_capacity(3); + call_args.push(args[0]); + let mut temps = Vec::new(); + if let Some(timestamp) = args.get(1) { + call_args.push(*timestamp); + } else { + let timestamp = eval_time_result(values)?; + temps.push(timestamp); + call_args.push(timestamp); + } + let utc = values.bool_value(utc)?; + temps.push(utc); + call_args.push(utc); + let result = eval_static_alias("DateTime", "__elephc_strftime", call_args, context, values); + for temp in temps { + values.release(temp)?; + } + result +} + +/// Returns the bundled timezone database version. +fn eval_timezone_version_alias( + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(EVAL_TZ_VERSION.trim()) +} + +/// Evaluates one current date part as an integer runtime cell. +fn eval_current_date_part_int( + date_name: &str, + spec: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string(spec)?; + let date = match eval_date_result(date_name, format, None, context, values) { + Ok(date) => date, + Err(status) => { + values.release(format)?; + return Err(status); + } + }; + values.release(format)?; + let value = values.cast_int(date); + values.release(date)?; + value +} + +/// Normalizes a possible alias name to its lowercase bare name. +fn eval_date_alias_key(name: &str) -> Option { + let bare = name + .rsplit('\\') + .next() + .unwrap_or("") + .to_ascii_lowercase(); + eval_date_alias_is_supported(&bare).then_some(bare) +} + +/// Returns true when a real builtin should keep handling this named call shape. +fn eval_date_alias_should_fall_back_to_builtin( + name: &str, + args: &[EvaluatedCallArg], +) -> bool { + matches!(name, "mktime" | "gmmktime") && args.iter().any(|arg| arg.name.is_some()) +} + +/// Returns whether eval has runtime dispatch for one procedural date/time alias. +fn eval_date_alias_is_supported(name: &str) -> bool { + matches!( + name, + "idate" + | "mktime" + | "gmmktime" + | "date_create" + | "date_create_immutable" + | "date_create_from_format" + | "date_create_immutable_from_format" + | "date_parse_from_format" + | "date_parse" + | "date_sun_info" + | "date_sunrise" + | "date_sunset" + | "strptime" + | "timezone_name_from_abbr" + | "cal_to_jd" + | "cal_from_jd" + | "cal_days_in_month" + | "cal_info" + | "gregoriantojd" + | "jdtogregorian" + | "juliantojd" + | "jdtojulian" + | "frenchtojd" + | "jdtofrench" + | "jewishtojd" + | "jdtojewish" + | "jddayofweek" + | "jdmonthname" + | "jdtounix" + | "unixtojd" + | "easter_days" + | "easter_date" + | "gettimeofday" + | "date_get_last_errors" + | "strftime" + | "gmstrftime" + | "timezone_open" + | "timezone_identifiers_list" + | "timezone_location_get" + | "timezone_transitions_get" + | "timezone_abbreviations_list" + | "timezone_version_get" + | "date_interval_create_from_date_string" + | "date_diff" + | "date_format" + | "date_add" + | "date_sub" + | "date_modify" + | "date_timestamp_get" + | "date_timestamp_set" + | "date_timezone_get" + | "date_timezone_set" + | "date_offset_get" + | "date_date_set" + | "date_isodate_set" + | "date_time_set" + | "date_interval_format" + | "timezone_name_get" + | "timezone_offset_get" + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs new file mode 100644 index 0000000000..105115aad4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Eval registry entry and implementation for `checkdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Gregorian bounds and leap-year validation are owned by this builtin file. + +use super::super::*; +use super::*; + +eval_builtin! { + name: "checkdate", + area: Time, + params: [month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `checkdate(month, day, year)` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_checkdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_checkdate_result(month, day, year, values) +} + +/// Returns whether the supplied month/day/year tuple is a valid Gregorian date. +pub(in crate::interpreter) fn eval_checkdate_result( + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let month = eval_int_value(month, values)?; + let day = eval_int_value(day, values)?; + let year = eval_int_value(year, values)?; + values.bool_value(eval_checkdate_parts(month, day, year)) +} + +/// Tests PHP `checkdate()` bounds and leap-year behavior for integer components. +fn eval_checkdate_parts(month: i64, day: i64, year: i64) -> bool { + if !(1..=12).contains(&month) || !(1..=32767).contains(&year) { + return false; + } + let days = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if eval_is_leap_year(year) => 29, + 2 => 28, + _ => return false, + }; + (1..=days).contains(&day) +} + +/// Returns whether one Gregorian year is a leap year. +fn eval_is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/date.rs new file mode 100644 index 0000000000..7adcce14de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/date.rs @@ -0,0 +1,255 @@ +//! Purpose: +//! Eval registry entry and implementation for `date` plus shared date-format helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - `gmdate` calls this file for shared formatting and UTC/local timestamp conversion. + +use std::os::unix::ffi::OsStrExt; +use std::sync::Mutex; + +use super::super::*; +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "date", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +static EVAL_TZ_MUTEX: Mutex<()> = Mutex::new(()); + +unsafe extern "C" { + /// Re-reads libc's process-global timezone environment. + fn tzset(); +} + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_date_like("date", args, context, scope, values) +} + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_date_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [format] => { + let format = eval_expr(format, context, scope, values)?; + eval_date_result(name, format, None, context, values) + } + [format, timestamp] => { + let format = eval_expr(format, context, scope, values)?; + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_date_result(name, format, Some(timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. +pub(in crate::interpreter) fn eval_date_result( + name: &str, + format: RuntimeCellHandle, + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string_bytes(format)?; + let timestamp = match timestamp { + Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values)?, + None => eval_current_unix_timestamp()?, + Some(_) => eval_current_unix_timestamp()?, + }; + let tm = match name { + "date" => eval_context_localtime(timestamp, context)?, + "gmdate" => eval_gmtime(timestamp)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let output = eval_format_date_bytes(&format, &tm, timestamp)?; + values.string_bytes_value(&output) +} + +/// Converts one Unix timestamp to eval-timezone broken-down time through libc. +pub(in crate::interpreter) fn eval_context_localtime( + timestamp: i64, + context: &ElephcEvalContext, +) -> Result { + eval_with_timezone(context.default_timezone(), || eval_localtime(timestamp)) +} + +/// Converts one Unix timestamp to process-local broken-down time through libc. +pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Converts one Unix timestamp to UTC broken-down time through libc. +pub(in crate::interpreter) fn eval_gmtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::gmtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Runs one libc timezone-sensitive operation under the eval context timezone. +pub(in crate::interpreter) fn eval_with_timezone( + timezone: &str, + operation: impl FnOnce() -> Result, +) -> Result { + let _guard = EVAL_TZ_MUTEX + .lock() + .map_err(|_| EvalStatus::RuntimeFatal)?; + let previous = std::env::var_os("TZ") + .map(|value| CString::new(value.as_bytes()).map_err(|_| EvalStatus::RuntimeFatal)) + .transpose()?; + eval_apply_process_timezone(timezone)?; + let result = operation(); + eval_restore_process_timezone(previous.as_ref())?; + result +} + +/// Applies one timezone identifier to libc's process-global timezone state. +fn eval_apply_process_timezone(timezone: &str) -> Result<(), EvalStatus> { + let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; + let value = CString::new(timezone).map_err(|_| EvalStatus::RuntimeFatal)?; + let status = unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { tzset() }; + Ok(()) +} + +/// Restores the process timezone that was active before an eval-local conversion. +fn eval_restore_process_timezone(previous: Option<&CString>) -> Result<(), EvalStatus> { + let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; + let status = if let Some(value) = previous { + unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) } + } else { + unsafe { libc::unsetenv(key.as_ptr()) } + }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { tzset() }; + Ok(()) +} + +/// Applies PHP `date()` tokens to one local broken-down timestamp. +pub(in crate::interpreter) fn eval_format_date_bytes( + format: &[u8], + tm: &libc::tm, + timestamp: i64, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut escaped = false; + for byte in format { + if escaped { + output.push(*byte); + escaped = false; + continue; + } + if *byte == b'\\' { + escaped = true; + continue; + } + eval_push_date_token(&mut output, *byte, tm, timestamp)?; + } + if escaped { + output.push(b'\\'); + } + Ok(output) +} + +/// Appends the expansion for one PHP `date()` token, or the token literal. +pub(in crate::interpreter) fn eval_push_date_token( + output: &mut Vec, + token: u8, + tm: &libc::tm, + timestamp: i64, +) -> Result<(), EvalStatus> { + match token { + b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), + b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), + b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), + b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), + b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), + b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), + b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), + b'D' => output + .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'M' => { + output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) + } + b'N' => { + let weekday = tm.tm_wday; + let iso_weekday = if weekday == 0 { 7 } else { weekday }; + output.extend_from_slice(iso_weekday.to_string().as_bytes()); + } + b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), + b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), + b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), + b'g' => { + let hour = tm.tm_hour % 12; + let hour = if hour == 0 { 12 } else { hour }; + output.extend_from_slice(hour.to_string().as_bytes()); + } + b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), + b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), + b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), + _ => output.push(token), + } + Ok(()) +} + +/// Returns a checked month index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_month_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_MONTH_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Returns a checked weekday index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_weekday_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_WEEKDAY_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Appends one zero-padded decimal value with the requested minimum width. +pub(in crate::interpreter) fn eval_push_padded_number( + output: &mut Vec, + value: i64, + width: usize, +) { + output.extend_from_slice(format!("{value:0width$}").as_bytes()); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs new file mode 100644 index 0000000000..7d557aee06 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Eval registry entry and implementation for `date_default_timezone_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The result reads the eval-local default timezone from the context. + +use super::super::super::*; + +eval_builtin! { + name: "date_default_timezone_get", + area: Time, + params: [], + direct: Time, + values: Time, +} + +/// Evaluates PHP `date_default_timezone_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_date_default_timezone_get( + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) +} + +/// Returns the eval-local default timezone identifier. +pub(in crate::interpreter) fn eval_date_default_timezone_get_result( + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(context.default_timezone()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs new file mode 100644 index 0000000000..0b3c7f38ad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `date_default_timezone_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The timezone identifier is stored on the eval context. + +use super::super::super::*; + +eval_builtin! { + name: "date_default_timezone_set", + area: Time, + params: [timezoneId], + direct: Time, + values: Time, +} + +/// Evaluates PHP `date_default_timezone_set($timezoneId)`. +pub(in crate::interpreter) fn eval_builtin_date_default_timezone_set( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [timezone] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let timezone = eval_expr(timezone, context, scope, values)?; + eval_date_default_timezone_set_result(timezone, context, values) +} + +/// Stores one eval-local default timezone identifier and reports success. +pub(in crate::interpreter) fn eval_date_default_timezone_set_result( + timezone: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timezone = values.string_bytes(timezone)?; + let timezone = String::from_utf8_lossy(&timezone).into_owned(); + context.set_default_timezone(timezone); + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs new file mode 100644 index 0000000000..cd37524365 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Eval registry entry and implementation for `getdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - This file owns optional timestamp coercion and array-entry helpers reused by `localtime`. + +use super::super::*; +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "getdate", + area: Time, + params: [timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +/// Evaluates PHP `getdate($timestamp = null)`. +pub(in crate::interpreter) fn eval_builtin_getdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_getdate_result(None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_getdate_result(Some(timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `getdate()` associative array for one optional timestamp. +pub(in crate::interpreter) fn eval_getdate_result( + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let tm = eval_context_localtime(timestamp, context)?; + let mut result = values.assoc_new(11)?; + result = eval_array_set_string_int(result, "seconds", i64::from(tm.tm_sec), values)?; + result = eval_array_set_string_int(result, "minutes", i64::from(tm.tm_min), values)?; + result = eval_array_set_string_int(result, "hours", i64::from(tm.tm_hour), values)?; + result = eval_array_set_string_int(result, "mday", i64::from(tm.tm_mday), values)?; + result = eval_array_set_string_int(result, "wday", i64::from(tm.tm_wday), values)?; + result = eval_array_set_string_int(result, "mon", i64::from(tm.tm_mon + 1), values)?; + result = eval_array_set_string_int(result, "year", i64::from(tm.tm_year + 1900), values)?; + result = eval_array_set_string_int(result, "yday", i64::from(tm.tm_yday), values)?; + result = eval_array_set_string_str( + result, + "weekday", + EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(&tm)?], + values, + )?; + result = eval_array_set_string_str( + result, + "month", + EVAL_MONTH_NAMES[eval_tm_month_index(&tm)?], + values, + )?; + eval_array_set_int_int(result, 0, timestamp, values) +} + + +/// Coerces an optional timestamp argument, treating null/omitted as the current time. +pub(in crate::interpreter) fn eval_optional_timestamp( + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + match timestamp { + Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values), + _ => eval_current_unix_timestamp(), + } +} + +/// Writes one string-keyed integer entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_string_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Writes one string-keyed string entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_string_str( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} + +/// Writes one integer-keyed integer entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_int_int( + array: RuntimeCellHandle, + key: i64, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs new file mode 100644 index 0000000000..87d45745a0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `gmdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - UTC formatting delegates to the shared formatter owned by `date`. + +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gmdate", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +/// Evaluates PHP `gmdate($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_gmdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_date_like("gmdate", args, context, scope, values) +} + +/// Formats one UTC timestamp through the shared `date` formatter. +pub(in crate::interpreter) fn eval_gmdate_result( + format: RuntimeCellHandle, + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_date_result("gmdate", format, timestamp, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs new file mode 100644 index 0000000000..2f3654de50 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `gmmktime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - UTC timestamp construction delegates to the shared mktime helpers. + +use super::*; + +eval_builtin! { + name: "gmmktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `gmmktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_gmmktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_mktime_like("gmmktime", args, context, scope, values) +} + +/// Converts PHP date components to a UTC Unix timestamp through libc `timegm`. +pub(in crate::interpreter) fn eval_gmmktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_mktime_result("gmmktime", hour, minute, second, month, day, year, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/header.rs b/crates/elephc-magician/src/interpreter/builtins/time/header.rs new file mode 100644 index 0000000000..85da2c7cb8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/header.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `header`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Only eval-local side effects observable without a web bridge are modeled. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "header", + area: Time, + params: [ + header, + replace = EvalBuiltinDefaultValue::Bool(true), + response_code = EvalBuiltinDefaultValue::Int(0), + ], + direct: Time, + values: Time, +} + +/// Evaluates PHP `header($header, $replace = true, $response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_header( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [line] => { + let line = eval_expr(line, context, scope, values)?; + eval_header_result(line, None, None, context, values) + } + [line, replace] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + eval_header_result(line, Some(replace), None, context, values) + } + [line, replace, response_code] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let response_code = eval_expr(response_code, context, scope, values)?; + eval_header_result(line, Some(replace), Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies eval-local `header()` side effects that are observable without a web bridge. +pub(in crate::interpreter) fn eval_header_result( + line: RuntimeCellHandle, + replace: Option, + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.string_bytes(line)?; + if let Some(replace) = replace { + let _ = values.truthy(replace)?; + } + if let Some(response_code) = response_code { + let response_code = eval_int_value(response_code, values)?; + let _ = context.replace_http_response_code(response_code); + } + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs new file mode 100644 index 0000000000..87f4747462 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `hrtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Monotonic time is returned as nanoseconds or `[seconds, nanoseconds]`. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hrtime", + area: Time, + params: [as_number = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, +} + +/// Evaluates PHP `hrtime($as_number = false)`. +pub(in crate::interpreter) fn eval_builtin_hrtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_hrtime_result(None, values), + [as_number] => { + let as_number = eval_expr(as_number, context, scope, values)?; + eval_hrtime_result(Some(as_number), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns monotonic time as either nanoseconds or `[seconds, nanoseconds]`. +pub(in crate::interpreter) fn eval_hrtime_result( + as_number: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (seconds, nanoseconds) = eval_monotonic_time()?; + let as_number = as_number + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + if as_number { + let total = seconds + .checked_mul(1_000_000_000) + .and_then(|value| value.checked_add(nanoseconds)) + .ok_or(EvalStatus::RuntimeFatal)?; + return values.int(total); + } + let mut result = values.array_new(2)?; + result = eval_array_set_int_int(result, 0, seconds, values)?; + eval_array_set_int_int(result, 1, nanoseconds, values) +} + +/// Reads the monotonic clock in whole seconds and nanoseconds. +fn eval_monotonic_time() -> Result<(i64, i64), EvalStatus> { + let mut timespec = MaybeUninit::::uninit(); + let status = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + let timespec = unsafe { timespec.assume_init() }; + Ok((timespec.tv_sec, timespec.tv_nsec)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs b/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs new file mode 100644 index 0000000000..81c9000bba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Eval registry entry and implementation for `http_response_code`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Response-code state is eval-local and observable by later calls. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "http_response_code", + area: Time, + params: [response_code = EvalBuiltinDefaultValue::Int(0)], + direct: Time, + values: Time, +} + +/// Evaluates PHP `http_response_code($response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_http_response_code( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => { + let response_code = eval_expr(response_code, context, scope, values)?; + eval_http_response_code_result(Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads or updates the eval-local HTTP response code. +pub(in crate::interpreter) fn eval_http_response_code_result( + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match response_code { + Some(response_code) => context.replace_http_response_code(eval_int_value(response_code, values)?), + None => context.http_response_code(), + }; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs new file mode 100644 index 0000000000..0a364e3004 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Eval registry entry and implementation for `localtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - `getdate` owns the shared timestamp coercion and array-entry helpers. + +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "localtime", + area: Time, + params: [ + timestamp = EvalBuiltinDefaultValue::Null, + associative = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Time, + values: Time, +} + +const EVAL_LOCALTIME_KEYS: &[&str; 9] = &[ + "tm_sec", "tm_min", "tm_hour", "tm_mday", "tm_mon", "tm_year", "tm_wday", "tm_yday", + "tm_isdst", +]; + +/// Evaluates PHP `localtime($timestamp = null, $associative = false)`. +pub(in crate::interpreter) fn eval_builtin_localtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_localtime_result(Some(timestamp), None, context, values) + } + [timestamp, associative] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_localtime_result(Some(timestamp), Some(associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `localtime()` array for one optional timestamp and key mode. +pub(in crate::interpreter) fn eval_localtime_result( + timestamp: Option, + associative: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let associative = associative + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + let tm = eval_context_localtime(timestamp, context)?; + let fields = [ + tm.tm_sec, + tm.tm_min, + tm.tm_hour, + tm.tm_mday, + tm.tm_mon, + tm.tm_year, + tm.tm_wday, + tm.tm_yday, + tm.tm_isdst, + ]; + if associative { + let mut result = values.assoc_new(fields.len())?; + for (key, value) in EVAL_LOCALTIME_KEYS.iter().zip(fields) { + result = eval_array_set_string_int(result, key, i64::from(value), values)?; + } + return Ok(result); + } + let mut result = values.array_new(fields.len())?; + for (index, value) in fields.into_iter().enumerate() { + result = eval_array_set_int_int(result, index as i64, i64::from(value), values)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs new file mode 100644 index 0000000000..06d13a50a9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Eval registry entry and implementation for `microtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The optional argument is accepted for PHP arity parity but does not alter the result. + +use super::super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "microtime", + area: Time, + params: [as_float = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, +} + +/// Evaluates PHP `microtime()` with an optional ignored argument. +pub(in crate::interpreter) fn eval_builtin_microtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_microtime_result(values), + [as_float] => { + let _ = eval_expr(as_float, context, scope, values)?; + eval_microtime_result(values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the current Unix timestamp with microsecond precision as a boxed float. +pub(in crate::interpreter) fn eval_microtime_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let seconds = timestamp.as_secs() as f64; + let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.float(seconds + micros) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs new file mode 100644 index 0000000000..c99473ac0a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Eval registry entry and implementation for `mktime` plus shared mktime helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - `gmmktime` and `strtotime` reuse the timestamp conversion helpers from this file. + +use super::super::*; +use super::*; + +eval_builtin! { + name: "mktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_mktime_like("mktime", args, context, scope, values) +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_mktime_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hour, minute, second, month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hour = eval_expr(hour, context, scope, values)?; + let minute = eval_expr(minute, context, scope, values)?; + let second = eval_expr(second, context, scope, values)?; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_mktime_result(name, hour, minute, second, month, day, year, context, values) +} + +/// Converts PHP date components to a local Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_result( + name: &str, + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = ( + eval_int_cell_as_c_int(hour, values)?, + eval_int_cell_as_c_int(minute, values)?, + eval_int_cell_as_c_int(second, values)?, + eval_int_cell_as_c_int(month, values)?, + eval_int_cell_as_c_int(day, values)?, + eval_int_cell_as_c_int(year, values)?, + ); + let timestamp = match name { + "mktime" => eval_context_mktime_timestamp(args, context)?, + "gmmktime" => eval_gmmktime_timestamp(args)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.int(timestamp) +} + +/// Converts local date components into an eval-timezone Unix timestamp. +pub(in crate::interpreter) fn eval_context_mktime_timestamp( + args: ( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + ), + context: &ElephcEvalContext, +) -> Result { + eval_with_timezone(context.default_timezone(), || { + eval_mktime_timestamp(args.0, args.1, args.2, args.3, args.4, args.5) + }) +} + +/// Converts local date components into a Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_timestamp( + hour: libc::c_int, + minute: libc::c_int, + second: libc::c_int, + month: libc::c_int, + day: libc::c_int, + year: libc::c_int, +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + let timestamp = unsafe { libc::mktime(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts UTC date components into a Unix timestamp through libc `timegm`. +pub(in crate::interpreter) fn eval_gmmktime_timestamp( + args: ( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + ), +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = args.0; + tm.tm_min = args.1; + tm.tm_sec = args.2; + tm.tm_mon = args.3 - 1; + tm.tm_mday = args.4; + tm.tm_year = args.5 - 1900; + tm.tm_isdst = 0; + let timestamp = unsafe { libc::timegm(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. +pub(in crate::interpreter) fn eval_int_cell_as_c_int( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs new file mode 100644 index 0000000000..e1a34f7cae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -0,0 +1,195 @@ +//! Purpose: +//! Orchestrates eval implementations for PHP time, date, sleep, and response +//! header builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Leaf builtin files own their registry declarations and builtin-specific wrappers. +//! - Shared calendar/date helpers live in the most specific builtin file that owns +//! the underlying behavior, such as `date`, `getdate`, `mktime`, or `time`. + +use super::super::*; + +mod aliases; +mod checkdate; +mod date; +mod date_default_timezone_get; +mod date_default_timezone_set; +mod getdate; +mod gmdate; +mod gmmktime; +mod header; +mod hrtime; +mod http_response_code; +mod localtime; +mod microtime; +mod mktime; +mod sleep; +mod strtotime; +mod time; +mod usleep; + +pub(in crate::interpreter) use aliases::*; +pub(in crate::interpreter) use checkdate::*; +pub(in crate::interpreter) use date::*; +pub(in crate::interpreter) use date_default_timezone_get::*; +pub(in crate::interpreter) use date_default_timezone_set::*; +pub(in crate::interpreter) use getdate::*; +pub(in crate::interpreter) use gmdate::*; +pub(in crate::interpreter) use gmmktime::*; +pub(in crate::interpreter) use header::*; +pub(in crate::interpreter) use hrtime::*; +pub(in crate::interpreter) use http_response_code::*; +pub(in crate::interpreter) use localtime::*; +pub(in crate::interpreter) use microtime::*; +pub(in crate::interpreter) use mktime::*; +pub(in crate::interpreter) use sleep::*; +pub(in crate::interpreter) use strtotime::*; +pub(in crate::interpreter) use time::*; +pub(in crate::interpreter) use usleep::*; + +/// Dispatches direct expression-level calls for time builtins. +pub(in crate::interpreter) fn eval_builtin_time_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => eval_builtin_checkdate(args, context, scope, values), + "date" => eval_builtin_date(args, context, scope, values), + "gmdate" => eval_builtin_gmdate(args, context, scope, values), + "date_default_timezone_get" => eval_builtin_date_default_timezone_get(args, context, values), + "date_default_timezone_set" => { + eval_builtin_date_default_timezone_set(args, context, scope, values) + } + "getdate" => eval_builtin_getdate(args, context, scope, values), + "gmmktime" => eval_builtin_gmmktime(args, context, scope, values), + "mktime" => eval_builtin_mktime(args, context, scope, values), + "header" => eval_builtin_header(args, context, scope, values), + "hrtime" => eval_builtin_hrtime(args, context, scope, values), + "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), + "localtime" => eval_builtin_localtime(args, context, scope, values), + "microtime" => eval_builtin_microtime(args, context, scope, values), + "sleep" => eval_builtin_sleep(args, context, scope, values), + "strtotime" => eval_builtin_strtotime(args, context, scope, values), + "time" => eval_builtin_time(args, values), + "usleep" => eval_builtin_usleep(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for time builtins. +pub(in crate::interpreter) fn eval_time_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => { + let [month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_checkdate_result(*month, *day, *year, values) + } + "date" => match evaluated_args { + [format] => eval_date_result("date", *format, None, context, values), + [format, timestamp] => eval_date_result("date", *format, Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gmdate" => match evaluated_args { + [format] => eval_gmdate_result(*format, None, context, values), + [format, timestamp] => eval_gmdate_result(*format, Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "date_default_timezone_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) + } + "date_default_timezone_set" => { + let [timezone] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_date_default_timezone_set_result(*timezone, context, values) + } + "getdate" => match evaluated_args { + [] => eval_getdate_result(None, context, values), + [timestamp] => eval_getdate_result(Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gmmktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gmmktime_result(*hour, *minute, *second, *month, *day, *year, context, values) + } + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result("mktime", *hour, *minute, *second, *month, *day, *year, context, values) + } + "header" => match evaluated_args { + [line] => eval_header_result(*line, None, None, context, values), + [line, replace] => eval_header_result(*line, Some(*replace), None, context, values), + [line, replace, response_code] => { + eval_header_result(*line, Some(*replace), Some(*response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "hrtime" => match evaluated_args { + [] => eval_hrtime_result(None, values), + [as_number] => eval_hrtime_result(Some(*as_number), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "http_response_code" => match evaluated_args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => eval_http_response_code_result(Some(*response_code), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "localtime" => match evaluated_args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values), + [timestamp, associative] => { + eval_localtime_result(Some(*timestamp), Some(*associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values) + } + "strtotime" => match evaluated_args { + [datetime] => eval_strtotime_result(*datetime, None, context, values), + [datetime, base_timestamp] => { + eval_strtotime_result(*datetime, Some(*base_timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) + } + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs new file mode 100644 index 0000000000..6009bec5a6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `sleep`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Negative durations are rejected as runtime fatals. + +use super::super::super::*; +use super::super::*; + +eval_builtin! { + name: "sleep", + area: Time, + params: [seconds], + direct: Time, + values: Time, +} + +/// Evaluates PHP `sleep($seconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [seconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let seconds = eval_expr(seconds, context, scope, values)?; + eval_sleep_result(seconds, values) +} + +/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. +pub(in crate::interpreter) fn eval_sleep_result( + seconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let seconds = eval_int_value(seconds, values)?; + let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_secs(seconds)); + values.int(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs new file mode 100644 index 0000000000..f0876234c9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs @@ -0,0 +1,163 @@ +//! Purpose: +//! Eval registry entry and implementation for `strtotime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The supported parser subset normalizes fixed-width ISO dates through `mktime`. + +use super::super::*; +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strtotime", + area: Time, + params: [datetime, baseTimestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +/// Evaluates PHP `strtotime(datetime, baseTimestamp = null)` for eval's supported subset. +pub(in crate::interpreter) fn eval_builtin_strtotime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [datetime] => { + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, None, context, values) + } + [datetime, base_timestamp] => { + let datetime = eval_expr(datetime, context, scope, values)?; + let base_timestamp = eval_expr(base_timestamp, context, scope, values)?; + eval_strtotime_result(datetime, Some(base_timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one eval `strtotime()` input and boxes the resulting timestamp. +pub(in crate::interpreter) fn eval_strtotime_result( + datetime: RuntimeCellHandle, + base_timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(datetime)?; + let base_timestamp = match base_timestamp { + Some(base_timestamp) if !values.is_null(base_timestamp)? => { + Some(eval_int_value(base_timestamp, values)?) + } + _ => None, + }; + let timestamp = eval_strtotime_bytes(&bytes, base_timestamp, context)?; + values.int(timestamp) +} + +/// Parses eval's supported `strtotime()` strings into local Unix timestamps. +pub(in crate::interpreter) fn eval_strtotime_bytes( + bytes: &[u8], + base_timestamp: Option, + context: &ElephcEvalContext, +) -> Result { + let bytes = eval_trim_ascii_whitespace(bytes); + if bytes.eq_ignore_ascii_case(b"now") { + return match base_timestamp { + Some(timestamp) => Ok(timestamp), + None => eval_current_unix_timestamp(), + }; + } + let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { + return Ok(-1); + }; + eval_context_mktime_timestamp((hour, minute, second, month, day, year), context) +} + +/// Trims ASCII whitespace from both ends of one byte slice. +pub(in crate::interpreter) fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { + let mut start = 0; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &bytes[start..end] +} + +/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. +pub(in crate::interpreter) fn eval_parse_iso_datetime( + bytes: &[u8], +) -> Option<( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, +)> { + if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { + return None; + } + if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { + return None; + } + let year = eval_parse_fixed_digits(bytes, 0, 4)?; + let month = eval_parse_fixed_digits(bytes, 5, 2)?; + let day = eval_parse_fixed_digits(bytes, 8, 2)?; + let (hour, minute, second) = if bytes.len() == 10 { + (0, 0, 0) + } else { + if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { + return None; + } + if bytes.get(13) != Some(&b':') { + return None; + } + let hour = eval_parse_fixed_digits(bytes, 11, 2)?; + let minute = eval_parse_fixed_digits(bytes, 14, 2)?; + let second = if bytes.len() == 19 { + if bytes.get(16) != Some(&b':') { + return None; + } + eval_parse_fixed_digits(bytes, 17, 2)? + } else { + 0 + }; + (hour, minute, second) + }; + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + { + return None; + } + Some((year, month, day, hour, minute, second)) +} + +/// Parses a fixed-width decimal field as a libc-compatible integer. +pub(in crate::interpreter) fn eval_parse_fixed_digits( + bytes: &[u8], + start: usize, + len: usize, +) -> Option { + let end = start.checked_add(len)?; + let field = bytes.get(start..end)?; + let mut value: libc::c_int = 0; + for byte in field { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?; + value = value.checked_add(libc::c_int::from(byte - b'0'))?; + } + Some(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/time.rs b/crates/elephc-magician/src/interpreter/builtins/time/time.rs new file mode 100644 index 0000000000..8744baf8eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/time.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `time`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The current Unix timestamp helper is reused by date parsing and aliases. + +use super::super::super::*; + +eval_builtin! { + name: "time", + area: Time, + params: [], + direct: Time, + values: Time, +} + +/// Evaluates PHP `time()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +pub(in crate::interpreter) fn eval_time_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs new file mode 100644 index 0000000000..5d6a0e004e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `usleep`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Negative durations are rejected as runtime fatals and success returns PHP null. + +use super::super::super::*; +use super::super::*; + +eval_builtin! { + name: "usleep", + area: Time, + params: [microseconds], + direct: Time, + values: Time, +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +pub(in crate::interpreter) fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs new file mode 100644 index 0000000000..0cb7912194 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `boolval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "boolval", + area: Types, + params: [value], + direct: Boolval, + values: Boolval, +} + +/// Evaluates PHP `boolval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_boolval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_boolval_result(value, values) +} + +/// Applies PHP `boolval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_boolval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_bool(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs new file mode 100644 index 0000000000..cc33612feb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `floatval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "floatval", + area: Types, + params: [value], + direct: Floatval, + values: Floatval, +} + +/// Evaluates PHP `floatval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floatval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_floatval_result(value, values) +} + +/// Applies PHP `floatval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_floatval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_float(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs new file mode 100644 index 0000000000..8d75c8e819 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `gettype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Runtime tags are mapped to PHP's historical `gettype()` names. + +use super::super::super::*; + +eval_builtin! { + name: "gettype", + area: Types, + params: [value], + direct: Gettype, + values: Gettype, +} + +/// Evaluates PHP `gettype()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +pub(in crate::interpreter) fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs new file mode 100644 index 0000000000..6354b892e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `intval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "intval", + area: Types, + params: [value], + direct: Intval, + values: Intval, +} + +/// Evaluates PHP `intval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_intval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_intval_result(value, values) +} + +/// Applies PHP `intval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_intval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_int(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs new file mode 100644 index 0000000000..3d20d64187 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_array", + area: Types, + params: [value], + direct: IsArray, + values: IsArray, +} + +/// Evaluates PHP `is_array()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_array_result(value, values) +} + +/// Applies PHP `is_array()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_array_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs new file mode 100644 index 0000000000..0df7c527e5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_bool`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_bool", + area: Types, + params: [value], + direct: IsBool, + values: IsBool, +} + +/// Evaluates PHP `is_bool()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_bool( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_bool_result(value, values) +} + +/// Applies PHP `is_bool()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_bool_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_BOOL) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs new file mode 100644 index 0000000000..e9442978a5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_double`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_double", + area: Types, + params: [value], + direct: IsDouble, + values: IsDouble, +} + +/// Evaluates PHP `is_double()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_double( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_double_result(value, values) +} + +/// Applies PHP `is_double()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_double_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs new file mode 100644 index 0000000000..5f325e3574 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_finite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; + +eval_builtin! { + name: "is_finite", + area: Types, + params: [num], + direct: IsFinite, + values: IsFinite, +} + +/// Evaluates PHP `is_finite()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_finite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_finite_result(num, values) +} + +/// Applies PHP `is_finite()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_finite_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_finite(); + values.bool_value(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs new file mode 100644 index 0000000000..0512825fb2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_float`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_float", + area: Types, + params: [value], + direct: IsFloat, + values: IsFloat, +} + +/// Evaluates PHP `is_float()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_float( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_float_result(value, values) +} + +/// Applies PHP `is_float()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_float_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs new file mode 100644 index 0000000000..62963f58a1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_infinite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; + +eval_builtin! { + name: "is_infinite", + area: Types, + params: [num], + direct: IsInfinite, + values: IsInfinite, +} + +/// Evaluates PHP `is_infinite()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_infinite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_infinite_result(num, values) +} + +/// Applies PHP `is_infinite()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_infinite_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_infinite(); + values.bool_value(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs new file mode 100644 index 0000000000..26bd8acc90 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_int`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_int", + area: Types, + params: [value], + direct: IsInt, + values: IsInt, +} + +/// Evaluates PHP `is_int()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_int_result(value, values) +} + +/// Applies PHP `is_int()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_int_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs new file mode 100644 index 0000000000..a8db000173 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_integer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_integer", + area: Types, + params: [value], + direct: IsInteger, + values: IsInteger, +} + +/// Evaluates PHP `is_integer()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_integer( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_integer_result(value, values) +} + +/// Applies PHP `is_integer()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_integer_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs new file mode 100644 index 0000000000..461bf83a17 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_iterable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Arrays are iterable directly; objects are checked against Traversable-style +//! relationships in the current eval context. + +use super::super::super::*; + +eval_builtin! { + name: "is_iterable", + area: Types, + params: [value], + direct: IsIterable, + values: IsIterable, +} + +/// Evaluates PHP `is_iterable()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_iterable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_iterable_result(value, context, values) +} + +/// Applies PHP `is_iterable()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_iterable_result( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = eval_is_iterable_value(tag, value, context, values)?; + values.bool_value(result) +} + +/// Returns PHP's `is_iterable()` result for arrays and Traversable-compatible objects. +fn eval_is_iterable_value( + tag: u64, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + for target in ["Traversable", "Iterator", "IteratorAggregate"] { + if dynamic_object_is_a(value, target, false, context, values)? + .map_or_else(|| values.object_is_a(value, target, false), Ok)? + { + return Ok(true); + } + } + Ok(false) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs new file mode 100644 index 0000000000..de6fb9d769 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_long`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_long", + area: Types, + params: [value], + direct: IsLong, + values: IsLong, +} + +/// Evaluates PHP `is_long()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_long_result(value, values) +} + +/// Applies PHP `is_long()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_long_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs new file mode 100644 index 0000000000..1d083e5063 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_nan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; + +eval_builtin! { + name: "is_nan", + area: Types, + params: [num], + direct: IsNan, + values: IsNan, +} + +/// Evaluates PHP `is_nan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_nan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_nan_result(num, values) +} + +/// Applies PHP `is_nan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_nan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_nan(); + values.bool_value(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs new file mode 100644 index 0000000000..40035af0b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_null", + area: Types, + params: [value], + direct: IsNull, + values: IsNull, +} + +/// Evaluates PHP `is_null()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_null( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_null_result(value, values) +} + +/// Applies PHP `is_null()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_null_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_NULL) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs new file mode 100644 index 0000000000..8f47dcd534 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_numeric`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Numeric strings follow the legacy ASCII scan shared with the static backend. + +use super::super::super::*; + +eval_builtin! { + name: "is_numeric", + area: Types, + params: [value], + direct: IsNumeric, + values: IsNumeric, +} + +/// Evaluates PHP `is_numeric()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_numeric( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_numeric_result(value, values) +} + +/// Applies PHP `is_numeric()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_numeric_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)); + values.bool_value(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs new file mode 100644 index 0000000000..10483b88aa --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_object`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_object", + area: Types, + params: [value], + direct: IsObject, + values: IsObject, +} + +/// Evaluates PHP `is_object()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_object( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_object_result(value, values) +} + +/// Applies PHP `is_object()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_object_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_OBJECT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs new file mode 100644 index 0000000000..d2b0c4cef7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_real`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_real", + area: Types, + params: [value], + direct: IsReal, + values: IsReal, +} + +/// Evaluates PHP `is_real()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_real( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_real_result(value, values) +} + +/// Applies PHP `is_real()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_real_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs new file mode 100644 index 0000000000..5cb27d8a50 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_resource`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_resource", + area: Types, + params: [value], + direct: IsResource, + values: IsResource, +} + +/// Evaluates PHP `is_resource()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_resource( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_resource_result(value, values) +} + +/// Applies PHP `is_resource()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_resource_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_RESOURCE) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs new file mode 100644 index 0000000000..50f02ff9a2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_scalar`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_scalar", + area: Types, + params: [value], + direct: IsScalar, + values: IsScalar, +} + +/// Evaluates PHP `is_scalar()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_scalar( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_scalar_result(value, values) +} + +/// Applies PHP `is_scalar()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_scalar_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(matches!(tag, EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs new file mode 100644 index 0000000000..48e2bc3a20 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Eval registry entry and implementation for `is_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; + +eval_builtin! { + name: "is_string", + area: Types, + params: [value], + direct: IsString, + values: IsString, +} + +/// Evaluates PHP `is_string()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_string_result(value, values) +} + +/// Applies PHP `is_string()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_string_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_STRING) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs new file mode 100644 index 0000000000..a84aa8fdba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Per-builtin eval registry entries and implementations for scalar type and +//! conversion functions. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers. + +mod boolval; +mod floatval; +mod gettype; +mod intval; +mod is_array; +mod is_bool; +mod is_double; +mod is_finite; +mod is_float; +mod is_infinite; +mod is_int; +mod is_integer; +mod is_iterable; +mod is_long; +mod is_nan; +mod is_null; +mod is_numeric; +mod is_object; +mod is_real; +mod is_resource; +mod is_scalar; +mod is_string; +mod settype; +mod strval; + +pub(in crate::interpreter) use boolval::*; +pub(in crate::interpreter) use floatval::*; +pub(in crate::interpreter) use gettype::*; +pub(in crate::interpreter) use intval::*; +pub(in crate::interpreter) use is_array::*; +pub(in crate::interpreter) use is_bool::*; +pub(in crate::interpreter) use is_double::*; +pub(in crate::interpreter) use is_finite::*; +pub(in crate::interpreter) use is_float::*; +pub(in crate::interpreter) use is_infinite::*; +pub(in crate::interpreter) use is_int::*; +pub(in crate::interpreter) use is_integer::*; +pub(in crate::interpreter) use is_iterable::*; +pub(in crate::interpreter) use is_long::*; +pub(in crate::interpreter) use is_nan::*; +pub(in crate::interpreter) use is_null::*; +pub(in crate::interpreter) use is_numeric::*; +pub(in crate::interpreter) use is_object::*; +pub(in crate::interpreter) use is_real::*; +pub(in crate::interpreter) use is_resource::*; +pub(in crate::interpreter) use is_scalar::*; +pub(in crate::interpreter) use is_string::*; +pub(in crate::interpreter) use settype::*; +pub(in crate::interpreter) use strval::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/types/settype.rs b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs new file mode 100644 index 0000000000..4fb414201c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs @@ -0,0 +1,140 @@ +//! Purpose: +//! Eval registry entry and implementation for `settype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Direct calls preserve the writable first argument target and write the +//! converted value back after source-order argument evaluation. + +use super::super::super::*; + +eval_builtin! { + name: "settype", + area: Types, + params: [var: by_ref, r#type], + by_ref: [var], + direct: none, + values: Settype, +} + +/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. +pub(in crate::interpreter) fn eval_builtin_settype_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (value, target, type_name) = eval_settype_direct_args(args, context, scope, values)?; + let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { + return values.bool_value(false); + }; + eval_write_direct_ref_target( + &target, + converted, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(true) +} + +/// Evaluates and binds direct `settype()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_settype_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut var_target = None; + let mut type_name = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "var", + 1 => "type", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "var" => { + if var_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let (value, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + var_target = Some((value, target)); + } + "type" => { + if type_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + type_name = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (value, target) = var_target.ok_or(EvalStatus::RuntimeFatal)?; + let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; + Ok((value, target, type_name)) +} + +/// Dispatches by-value `settype()` callable calls after argument binding. +pub(in crate::interpreter) fn eval_settype_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values) +} + +/// Applies the eval-supported `settype()` scalar target conversion. +pub(in crate::interpreter) fn eval_settype_cast_value( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_name = values.string_bytes(type_name)?; + let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); + let converted = match type_name.as_str() { + "bool" | "boolean" => Some(values.cast_bool(value)?), + "float" | "double" => Some(values.cast_float(value)?), + "int" | "integer" => Some(values.cast_int(value)?), + "string" => Some(values.cast_string(value)?), + _ => None, + }; + Ok(converted) +} + +/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. +pub(in crate::interpreter) fn eval_settype_value_result( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; + if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { + values.release(converted)?; + return values.bool_value(true); + } + values.bool_value(false) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/strval.rs b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs new file mode 100644 index 0000000000..038ebc1fea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Eval registry entry and implementation for `strval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; + +eval_builtin! { + name: "strval", + area: Types, + params: [value], + direct: Strval, + values: Strval, +} + +/// Evaluates PHP `strval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_strval_result(value, context, values) +} + +/// Applies PHP `strval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_strval_result( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) +} diff --git a/crates/elephc-magician/src/interpreter/constant_eval.rs b/crates/elephc-magician/src/interpreter/constant_eval.rs new file mode 100644 index 0000000000..47bf66993c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/constant_eval.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Evaluates EvalIR constants, dynamic constant fetches, predefined constants, and magic constants. +//! +//! Called from: +//! - `crate::interpreter::eval_expr()` for constant and magic-constant expression nodes. +//! +//! Key details: +//! - Dynamic constants prefer eval context declarations before predefined fallback constants. +//! - Magic file and directory values come from the current eval call-site context. + +use super::*; + +/// Converts one EvalIR constant into a runtime-cell handle. +pub(super) fn eval_const( + value: &EvalConst, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + EvalConst::Null => values.null(), + EvalConst::Bool(value) => values.bool_value(*value), + EvalConst::Int(value) => values.int(*value), + EvalConst::Float(value) => values.float(*value), + EvalConst::String(value) => values.string(value), + } +} + +/// Loads a retained value for one eval-defined dynamic constant. +pub(super) fn eval_const_fetch( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); + } + let Some(value) = context.constant(name) else { + return Err(EvalStatus::RuntimeFatal); + }; + values.retain(value) +} + +/// Fetches a namespaced constant and falls back to the global constant namespace. +pub(super) fn eval_namespaced_const_fetch( + name: &str, + fallback_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); + } + if let Some(value) = context.constant(name) { + return values.retain(value); + } + eval_const_fetch(fallback_name, context, values) +} + +/// Materializes one eval-visible predefined constant into a runtime cell. +fn eval_predefined_constant( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = eval_predefined_constant_value(name) else { + return Ok(None); + }; + match value { + EvalPredefinedConstant::Int(value) => values.int(value).map(Some), + EvalPredefinedConstant::Float(value) => values.float(value).map(Some), + EvalPredefinedConstant::String(value) => values.string(value).map(Some), + } +} + +/// Returns eval-visible predefined constants that do not live in dynamic context. +pub(in crate::interpreter) fn eval_predefined_constant_value( + name: &str, +) -> Option { + match name.trim_start_matches('\\') { + "PATHINFO_DIRNAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_DIRNAME)), + "PATHINFO_BASENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_BASENAME)), + "PATHINFO_EXTENSION" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_EXTENSION)), + "PATHINFO_FILENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_FILENAME)), + "PATHINFO_ALL" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_ALL)), + "FNM_NOESCAPE" => Some(EvalPredefinedConstant::Int(EVAL_FNM_NOESCAPE)), + "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), + "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), + "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), + "LOCK_SH" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_SH)), + "LOCK_EX" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_EX)), + "LOCK_UN" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_UN)), + "LOCK_NB" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_NB)), + "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), + "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), + "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), + "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), + "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), + "PREG_SPLIT_NO_EMPTY" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_NO_EMPTY)), + "PREG_SPLIT_DELIM_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_DELIM_CAPTURE)) + } + "PREG_SPLIT_OFFSET_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_OFFSET_CAPTURE)) + } + "PREG_PATTERN_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_PATTERN_ORDER)), + "PREG_SET_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SET_ORDER)), + "PREG_OFFSET_CAPTURE" => Some(EvalPredefinedConstant::Int(EVAL_PREG_OFFSET_CAPTURE)), + "PREG_UNMATCHED_AS_NULL" => Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)), + "JSON_ERROR_NONE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_NONE)), + "JSON_ERROR_DEPTH" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_DEPTH)), + "JSON_ERROR_STATE_MISMATCH" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_STATE_MISMATCH)) + } + "JSON_ERROR_CTRL_CHAR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_CTRL_CHAR)), + "JSON_ERROR_SYNTAX" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_SYNTAX)), + "JSON_ERROR_UTF8" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF8)), + "JSON_ERROR_RECURSION" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_RECURSION)), + "JSON_ERROR_INF_OR_NAN" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_INF_OR_NAN)), + "JSON_ERROR_UNSUPPORTED_TYPE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_UNSUPPORTED_TYPE, + )), + "JSON_ERROR_INVALID_PROPERTY_NAME" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, + )), + "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), + "JSON_HEX_TAG" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_TAG)), + "JSON_HEX_AMP" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_AMP)), + "JSON_HEX_APOS" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_APOS)), + "JSON_HEX_QUOT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_QUOT)), + "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), + "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), + "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), + "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), + "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), + "JSON_PARTIAL_OUTPUT_ON_ERROR" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, + )), + "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), + "JSON_PRESERVE_ZERO_FRACTION" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PRESERVE_ZERO_FRACTION, + )), + "JSON_INVALID_UTF8_IGNORE" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_INVALID_UTF8_IGNORE)) + } + "JSON_INVALID_UTF8_SUBSTITUTE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_INVALID_UTF8_SUBSTITUTE, + )), + "JSON_THROW_ON_ERROR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_THROW_ON_ERROR)), + "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), + "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), + "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), + "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), + "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), + "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String("/")), + _ => None, + } +} + +/// Returns the PHP OS constant for the host platform running the eval bridge. +fn eval_php_os_name() -> &'static str { + if cfg!(target_os = "macos") { + "Darwin" + } else { + "Linux" + } +} + +/// Resolves one eval magic constant against fragment and dynamic-call metadata. +pub(super) fn eval_magic_const( + magic: &EvalMagicConst, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match magic { + EvalMagicConst::File => values.string(&context.eval_file_magic()), + EvalMagicConst::Dir => values.string(context.call_dir()), + EvalMagicConst::Line(line) => values.int(*line), + EvalMagicConst::Function => values.string( + context + .current_magic_function() + .or_else(|| context.current_function()) + .unwrap_or(""), + ), + EvalMagicConst::Method => values.string( + context + .current_magic_method() + .or_else(|| context.current_function()) + .unwrap_or(""), + ), + EvalMagicConst::Class => values.string(context.current_magic_class().unwrap_or("")), + EvalMagicConst::Namespace => values.string(""), + EvalMagicConst::Trait => values.string(context.current_magic_trait().unwrap_or("")), + } +} diff --git a/crates/elephc-magician/src/interpreter/constants.rs b/crates/elephc-magician/src/interpreter/constants.rs new file mode 100644 index 0000000000..281e2a56ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/constants.rs @@ -0,0 +1,254 @@ +//! Purpose: +//! Defines eval-local PHP compatibility constants and static lookup tables. +//! Builtin modules read these tables to mirror native elephc behavior for dynamic eval. +//! +//! Called from: +//! - `crate::interpreter::builtins` domain modules. +//! - `crate::interpreter` constant and JSON helpers. +//! +//! Key details: +//! - Values here are PHP-visible compatibility data; changing them changes eval semantics. + +use std::sync::atomic::AtomicU64; + +/// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. +pub(super) const EVAL_HASH_ALGOS: &[&str] = &[ + "md2", + "md4", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ripemd128", + "ripemd160", + "ripemd256", + "ripemd320", + "whirlpool", + "crc32", + "crc32b", + "crc32c", + "adler32", + "fnv132", + "fnv1a32", + "fnv164", + "fnv1a64", + "joaat", +]; + +/// Built-in stream wrappers reported by eval `stream_get_wrappers()`. +pub(super) const EVAL_STREAM_WRAPPERS: &[&str] = &[ + "file", + "php", + "data", + "ftp", + "http", + "https", + "ftps", + "compress.zlib", + "compress.bzip2", + "phar", + "glob", +]; + +/// Built-in stream transports reported by eval `stream_get_transports()`. +pub(super) const EVAL_STREAM_TRANSPORTS: &[&str] = &[ + "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", "tlsv1.2", + "tlsv1.3", +]; + +/// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. +pub(super) static EVAL_RANDOM_COUNTER: AtomicU64 = AtomicU64::new(0); + +/// Built-in stream filters reported by eval `stream_get_filters()`. +pub(super) const EVAL_STREAM_FILTERS: &[&str] = &[ + "string.toupper", + "string.tolower", + "string.rot13", + "string.strip_tags", + "convert.base64-encode", + "convert.base64-decode", + "convert.quoted-printable-encode", + "convert.quoted-printable-decode", + "convert.iconv.*", + "dechunk", + "zlib.deflate", + "zlib.inflate", + "bzip2.compress", + "bzip2.decompress", +]; + +/// SPL/core type names reported by eval `spl_classes()`. +/// +/// Mirrors `src/codegen/builtins/spl/mod.rs::SPL_CLASS_NAMES` so dynamic eval +/// exposes the same static registry snapshot as native code. +pub(super) const EVAL_SPL_CLASS_NAMES: &[&str] = &[ + "AppendIterator", + "ArrayAccess", + "ArrayIterator", + "ArrayObject", + "BadFunctionCallException", + "BadMethodCallException", + "CachingIterator", + "CallbackFilterIterator", + "Countable", + "DomainException", + "DirectoryIterator", + "EmptyIterator", + "Error", + "Exception", + "FilterIterator", + "FilesystemIterator", + "GlobIterator", + "InfiniteIterator", + "InvalidArgumentException", + "Iterator", + "IteratorAggregate", + "IteratorIterator", + "JsonSerializable", + "LengthException", + "LimitIterator", + "LogicException", + "MultipleIterator", + "NoRewindIterator", + "OuterIterator", + "OutOfBoundsException", + "OutOfRangeException", + "OverflowException", + "ParentIterator", + "RangeException", + "RecursiveArrayIterator", + "RecursiveCachingIterator", + "RecursiveCallbackFilterIterator", + "RecursiveDirectoryIterator", + "RecursiveFilterIterator", + "RecursiveIterator", + "RecursiveIteratorIterator", + "RecursiveRegexIterator", + "RegexIterator", + "RuntimeException", + "SeekableIterator", + "SplDoublyLinkedList", + "SplFixedArray", + "SplFileInfo", + "SplFileObject", + "SplObserver", + "SplQueue", + "SplStack", + "SplSubject", + "SplTempFileObject", + "Stringable", + "Throwable", + "Traversable", + "TypeError", + "UnderflowException", + "UnexpectedValueException", + "ValueError", +]; + +/// Full English month names used by eval `date()`. +pub(super) const EVAL_MONTH_NAMES: &[&str; 12] = &[ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +/// Short English month names used by eval `date()`. +pub(super) const EVAL_MONTH_SHORT_NAMES: &[&str; 12] = &[ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +/// Full English weekday names used by eval `date()`. +pub(super) const EVAL_WEEKDAY_NAMES: &[&str; 7] = &[ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +/// Short English weekday names used by eval `date()`. +pub(super) const EVAL_WEEKDAY_SHORT_NAMES: &[&str; 7] = + &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +/// Root package manifest used to mirror native `phpversion()` in the eval crate. +pub(super) const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../../Cargo.toml"); + +pub(super) const DEFINE_ALREADY_DEFINED_WARNING: &str = + "Warning: define(): Constant already defined\n"; +pub(super) const HEX2BIN_ODD_LENGTH_WARNING: &str = + "Warning: hex2bin(): Hexadecimal input string must have an even length\n"; +pub(super) const HEX2BIN_INVALID_WARNING: &str = + "Warning: hex2bin(): Input string must be hexadecimal string\n"; +pub(super) const EVAL_PATHINFO_DIRNAME: i64 = 1; +pub(super) const EVAL_PATHINFO_BASENAME: i64 = 2; +pub(super) const EVAL_PATHINFO_EXTENSION: i64 = 4; +pub(super) const EVAL_PATHINFO_FILENAME: i64 = 8; +pub(super) const EVAL_PATHINFO_ALL: i64 = 15; +pub(super) const EVAL_FNM_NOESCAPE: i64 = 1; +pub(super) const EVAL_FNM_PATHNAME: i64 = 2; +pub(super) const EVAL_FNM_PERIOD: i64 = 4; +pub(super) const EVAL_FNM_CASEFOLD: i64 = 16; +pub(super) const EVAL_LOCK_SH: i64 = 1; +pub(super) const EVAL_LOCK_EX: i64 = 2; +pub(super) const EVAL_LOCK_UN: i64 = 3; +pub(super) const EVAL_LOCK_NB: i64 = 4; +pub(super) const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; +pub(super) const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; +pub(super) const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; +pub(super) const EVAL_COUNT_NORMAL: i64 = 0; +pub(super) const EVAL_COUNT_RECURSIVE: i64 = 1; +pub(super) const EVAL_PREG_SPLIT_NO_EMPTY: i64 = 1; +pub(super) const EVAL_PREG_SPLIT_DELIM_CAPTURE: i64 = 2; +pub(super) const EVAL_PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; +pub(super) const EVAL_PREG_PATTERN_ORDER: i64 = 1; +pub(super) const EVAL_PREG_SET_ORDER: i64 = 2; +pub(super) const EVAL_PREG_OFFSET_CAPTURE: i64 = 256; +pub(super) const EVAL_PREG_UNMATCHED_AS_NULL: i64 = 512; +pub(super) const EVAL_JSON_ERROR_NONE: i64 = 0; +pub(super) const EVAL_JSON_ERROR_DEPTH: i64 = 1; +pub(super) const EVAL_JSON_ERROR_STATE_MISMATCH: i64 = 2; +pub(super) const EVAL_JSON_ERROR_CTRL_CHAR: i64 = 3; +pub(super) const EVAL_JSON_ERROR_SYNTAX: i64 = 4; +pub(super) const EVAL_JSON_ERROR_UTF8: i64 = 5; +pub(super) const EVAL_JSON_ERROR_RECURSION: i64 = 6; +pub(super) const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; +pub(super) const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; +pub(super) const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; +pub(super) const EVAL_JSON_ERROR_UTF16: i64 = 10; +pub(super) const EVAL_JSON_HEX_TAG: i64 = 1; +pub(super) const EVAL_JSON_HEX_AMP: i64 = 2; +pub(super) const EVAL_JSON_HEX_APOS: i64 = 4; +pub(super) const EVAL_JSON_HEX_QUOT: i64 = 8; +pub(super) const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; +pub(super) const EVAL_JSON_FORCE_OBJECT: i64 = 16; +pub(super) const EVAL_JSON_NUMERIC_CHECK: i64 = 32; +pub(super) const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; +pub(super) const EVAL_JSON_PRETTY_PRINT: i64 = 128; +pub(super) const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; +pub(super) const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; +pub(super) const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; +pub(super) const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; +pub(super) const EVAL_JSON_INVALID_UTF8_SUBSTITUTE: i64 = 2_097_152; +pub(super) const EVAL_JSON_THROW_ON_ERROR: i64 = 4_194_304; +pub(super) const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; +pub(super) const EVAL_JSON_UTF8_MESSAGE: &str = + "Malformed UTF-8 characters, possibly incorrectly encoded"; diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs new file mode 100644 index 0000000000..d8dac6481f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -0,0 +1,143 @@ +//! Purpose: +//! Holds small interpreter-local control and call-shape types shared across eval execution modules. +//! These types describe control-flow escape values, evaluated call arguments, and parsed builtin state. +//! +//! Called from: +//! - `crate::interpreter` execution, builtin, and call-dispatch helpers. +//! +//! Key details: +//! - Runtime cells are opaque handles; these types do not own or release values by themselves. + +use crate::context::EvalReferenceTarget; +use crate::value::RuntimeCellHandle; + +/// Internal statement-control result used to propagate eval returns and loops. +pub(super) enum EvalControl { + None, + ReturnVoid, + Return(RuntimeCellHandle), + Throw(RuntimeCellHandle), + Break, + Continue, +} + +/// Final result of executing a parsed eval program. +pub enum EvalOutcome { + Value(RuntimeCellHandle), + Throwable(RuntimeCellHandle), +} + +/// One already evaluated function-like call argument. +#[derive(Clone)] +pub(super) struct EvaluatedCallArg { + pub(super) name: Option, + pub(super) value: RuntimeCellHandle, + pub(super) ref_target: Option, +} + +/// One method argument after PHP parameter-order binding and default materialization. +#[derive(Clone)] +pub(super) struct BoundMethodArg { + pub(super) value: RuntimeCellHandle, + pub(super) ref_target: Option, + pub(super) variadic_ref_targets: Vec<(RuntimeCellHandle, EvalReferenceTarget)>, +} + +/// One native function argument list prepared for the descriptor invoker ABI. +pub(super) struct BoundNativeFunctionArgs { + pub(super) values: Vec, + pub(super) ref_slots: Vec, +} + +/// One staged by-reference slot passed to a native function invoker. +pub(super) enum BoundNativeFunctionRefSlot { + Mixed { + original: RuntimeCellHandle, + slot: Box, + target: Option, + }, + RawWord { + tag: u64, + original: u64, + slot: Box, + target: Option, + }, + RawString { + original: [u64; 2], + slot: Box<[u64; 2]>, + target: Option, + }, + OwnedRawWord { + original: u64, + slot: Box, + target: Option, + }, +} + +/// How a callable binder should handle by-reference parameters without caller storage. +#[derive(Clone, Copy)] +pub(super) enum EvalByRefBindingMode<'a> { + RequireTarget, + WarnByValue { + callable_name: &'a str, + }, +} + +/// One already evaluated PHP callback supported by the eval dispatcher. +pub(super) enum EvaluatedCallable { + Named { + name: String, + display_name: String, + }, + BoundClosure { + 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, + }, +} + +/// Bound argument tuple for direct `array_splice()` calls. +pub(super) type EvalArraySpliceDirectArgs = ( + RuntimeCellHandle, + EvalReferenceTarget, + RuntimeCellHandle, + Option, + Option, +); + +/// Parsed flags for one eval `sprintf()` conversion specifier. +#[derive(Clone, Copy)] +pub(super) struct EvalSprintfSpec { + pub(super) left_align: bool, + pub(super) force_sign: bool, + pub(super) space_sign: bool, + pub(super) zero_pad: bool, + pub(super) alternate: bool, + pub(super) width: Option, + pub(super) precision: Option, + pub(super) specifier: u8, +} + +/// Eval-visible predefined constant payloads that are not stored in the dynamic context. +pub(super) enum EvalPredefinedConstant { + Int(i64), + Float(f64), + String(&'static str), +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs new file mode 100644 index 0000000000..a4f5ba50b9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -0,0 +1,404 @@ +//! Purpose: +//! Coordinates call-argument evaluation for user-declared and native functions. +//! Binding and execution paths live in focused child modules. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` and dynamic callable dispatch helpers. +//! +//! Key details: +//! - PHP source evaluation order is preserved before argument binding. +//! - Static locals are persisted through `ElephcEvalContext` after function execution. + +mod closure_execution; +mod function_binding; +mod method_binding; +mod native_execution; + +use super::*; +use std::ffi::c_void; + +pub(in crate::interpreter) use closure_execution::*; +pub(in crate::interpreter) use function_binding::*; +pub(in crate::interpreter) use method_binding::*; +pub(in crate::interpreter) use native_execution::*; + +/// Evaluates an eval-declared user function with PHP-style argument binding. +pub(in crate::interpreter) fn eval_dynamic_function( + function: &EvalFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; + eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) +} + +/// Evaluates and binds native AOT function arguments, filling registered defaults. +pub(in crate::interpreter) fn eval_native_function_call_args( + function: &NativeFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; + bind_evaluated_native_function_args(function, evaluated_args, context, values) +} + +/// Evaluates source-order call arguments while preserving named-argument metadata. +pub(in crate::interpreter) fn eval_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut evaluated_args = Vec::with_capacity(args.len()); + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let spread = eval_expr(arg.value(), context, caller_scope, values)?; + if !values.is_array_like(spread)? { + return Err(EvalStatus::RuntimeFatal); + } + append_unpacked_call_arg_values( + spread, + &mut evaluated_args, + &mut saw_named, + context, + values, + )?; + continue; + } + + if let Some(name) = arg.name() { + saw_named = true; + let (value, ref_target) = + eval_call_arg_value(arg.value(), context, caller_scope, values)?; + evaluated_args.push(EvaluatedCallArg { + name: Some(name.to_string()), + value, + ref_target, + }); + continue; + } + + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let (value, ref_target) = eval_call_arg_value(arg.value(), context, caller_scope, values)?; + evaluated_args.push(EvaluatedCallArg { + name: None, + value, + ref_target, + }); + } + + Ok(evaluated_args) +} + +/// Evaluates one call arg and captures caller-side storage for by-reference parameters. +pub(in crate::interpreter) fn eval_call_arg_value( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + match expr { + EvalExpr::LoadVar(name) => { + let value = visible_scope_cell(context, caller_scope, name) + .map_or_else(|| values.null(), Ok)?; + Ok(( + value, + Some(EvalReferenceTarget::Variable { + scope: caller_scope as *mut ElephcEvalScope, + name: name.clone(), + }), + )) + } + EvalExpr::ArrayGet { array, index } => { + let EvalExpr::LoadVar(array_name) = array.as_ref() else { + return eval_nested_array_element_call_arg_value( + array, + index, + context, + caller_scope, + values, + ); + }; + let array = visible_scope_cell(context, caller_scope, array_name) + .map_or_else(|| values.null(), Ok)?; + let index = eval_expr(index, context, caller_scope, values)?; + let value = eval_array_get_result(array, index, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return Ok((value, None)); + } + Ok(( + value, + Some(EvalReferenceTarget::ArrayElement { + scope: caller_scope as *mut ElephcEvalScope, + array_name: array_name.clone(), + index, + }), + )) + } + EvalExpr::PropertyGet { object, property } => { + let access_scope = context.execution_scope(); + let object = eval_expr(object, context, caller_scope, values)?; + let value = eval_property_get_result(object, property, context, values)?; + validate_property_ref_target(object, property, context, values)?; + Ok(( + value, + Some(EvalReferenceTarget::ObjectProperty { + object, + property: property.clone(), + access_scope, + }), + )) + } + EvalExpr::DynamicPropertyGet { object, property } => { + let access_scope = context.execution_scope(); + let object = eval_expr(object, context, caller_scope, values)?; + let property = eval_dynamic_member_name(property, context, caller_scope, values)?; + let value = eval_property_get_result(object, &property, context, values)?; + validate_property_ref_target(object, &property, context, values)?; + Ok(( + value, + Some(EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + }), + )) + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + eval_static_property_call_arg_value( + class_name, + property.clone(), + access_scope, + context, + values, + ) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = eval_expr(class_name, context, caller_scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_call_arg_value( + class_name, + property.clone(), + access_scope, + context, + values, + ) + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = eval_expr(class_name, context, caller_scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, caller_scope, values)?; + eval_static_property_call_arg_value( + class_name, + property, + access_scope, + context, + values, + ) + } + _ => eval_expr(expr, context, caller_scope, values).map(|value| (value, None)), + } +} + +/// Evaluates an array element whose array expression is itself a writable caller target. +fn eval_nested_array_element_call_arg_value( + array: &EvalExpr, + index: &EvalExpr, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let (array, array_target) = eval_call_arg_value(array, context, caller_scope, values)?; + let index = eval_expr(index, context, caller_scope, values)?; + let value = eval_array_get_result(array, index, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return Ok((value, None)); + } + let Some(array_target) = array_target else { + return Ok((value, None)); + }; + Ok(( + value, + Some(EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target), + index, + }), + )) +} + +/// Evaluates one static-property lvalue and records it as a by-reference call target. +fn eval_static_property_call_arg_value( + class_name: String, + property: String, + access_scope: ElephcEvalExecutionScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + Ok(( + value, + Some(EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + }), + )) +} + +/// Converts a `call_user_func_array` argument array into ordered call arguments. +pub(in crate::interpreter) fn eval_array_call_arg_values( + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(arg_array)?; + let mut evaluated_args = Vec::with_capacity(len); + let mut saw_named = false; + append_unpacked_call_arg_values( + arg_array, + &mut evaluated_args, + &mut saw_named, + context, + values, + )?; + Ok(evaluated_args) +} + +/// Appends one unpacked array's values using PHP named-argument key semantics. +pub(in crate::interpreter) fn append_unpacked_call_arg_values( + array: RuntimeCellHandle, + evaluated_args: &mut Vec, + saw_named: &mut bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let ref_target = eval_array_reference_key(key, values)? + .and_then(|key| context.array_element_alias(array, &key).cloned()); + let arg = match values.type_tag(key)? { + EVAL_TAG_INT => { + if *saw_named { + values.release(key)?; + return Err(EvalStatus::RuntimeFatal); + } + let value = match values.array_get(array, key) { + Ok(value) => value, + Err(status) => { + values.release(key)?; + return Err(status); + } + }; + let (value, ref_target) = + eval_invoker_ref_arg_value_and_target(value, ref_target, values)?; + EvaluatedCallArg { + name: None, + value, + ref_target, + } + } + EVAL_TAG_STRING => { + *saw_named = true; + let name = values.string_bytes(key)?; + let name = match String::from_utf8(name) { + Ok(name) => name, + Err(_) => { + values.release(key)?; + return Err(EvalStatus::RuntimeFatal); + } + }; + let value = match values.array_get(array, key) { + Ok(value) => value, + Err(status) => { + values.release(key)?; + return Err(status); + } + }; + let (value, ref_target) = + eval_invoker_ref_arg_value_and_target(value, ref_target, values)?; + EvaluatedCallArg { + name: Some(name), + value, + ref_target, + } + } + _ => { + values.release(key)?; + return Err(EvalStatus::RuntimeFatal); + } + }; + values.release(key)?; + evaluated_args.push(arg); + } + Ok(()) +} + +/// Converts a descriptor-invoker ref marker into an eval-visible value and writeback target. +fn eval_invoker_ref_arg_value_and_target( + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_INVOKER_REF_CELL { + return Ok((value, ref_target)); + } + let slot = values.raw_value_word(value)? as usize; + let source_tag = values.raw_value_high_word(value)?; + let value = eval_invoker_ref_slot_value(slot, source_tag, values)?; + Ok(( + value, + ref_target.or(Some(EvalReferenceTarget::InvokerSlot { slot, source_tag })), + )) +} + +/// Reads the current PHP value from a native descriptor-invoker by-reference slot. +fn eval_invoker_ref_slot_value( + slot: usize, + source_tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_STRING => { + let words = unsafe { *(slot as *const [u64; 2]) }; + values.raw_string_value(words[0], words[1]) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_MIXED => { + let value = unsafe { *(slot as *const RuntimeCellHandle) }; + values.retain(value) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs new file mode 100644 index 0000000000..8da6060684 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs @@ -0,0 +1,655 @@ +//! Purpose: +//! Executes eval-declared functions and closures with bound scope and captures. +//! +//! Called from: +//! - Dynamic function dispatch and callable/Closure invocation paths. +//! +//! Key details: +//! - Bound `$this`, class scope, reference captures, and static locals survive execution. + +use super::*; + +/// Evaluates an eval-declared function after its positional arguments are prepared. +pub(in crate::interpreter) fn eval_dynamic_function_with_values( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = evaluated_args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) +} + +/// Evaluates an eval-declared function after call arguments preserve names and ref targets. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_flags( + function, + function.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_flags( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_mode( + function, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_mode( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let static_names = static_var_names(function.body()); + context.push_function(function.name()); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + parameter_is_by_ref, + function.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_function(); + return Err(status); + } + }; + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + let mut function_scope = ElephcEvalScope::new(); + bind_method_scope_args( + &mut function_scope, + function.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; + context.pop_function(); + return_result +} + +/// Evaluates one runtime eval closure after callback arguments preserve names and ref targets. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args( + closure: &EvalClosure, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + None, + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with `$this` and an optional binding scope. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with a class scope but no `$this` binding. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( + closure: &EvalClosure, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_evaluated_args(closure, evaluated_args, context, values); + }; + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with scope-only binding and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a scope-only runtime eval closure with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Class binding metadata for a runtime eval closure invocation. +struct EvalClosureBinding { + this_object: Option, + class_scope: String, + called_class: String, +} + +/// Returns the closure function's declared by-reference parameter flags. +fn function_ref_flags(closure: &EvalClosure) -> &[bool] { + closure.function().parameter_is_by_ref() +} + +/// Evaluates one runtime eval closure with optional class and `$this` binding metadata. +fn eval_closure_with_optional_binding( + closure: &EvalClosure, + parameter_is_by_ref: &[bool], + by_ref_mode: EvalByRefBindingMode<'_>, + binding: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let function = closure.function(); + let static_names = static_var_names(function.body()); + let bound_class_pushed = binding.is_some(); + context.push_function(function.name()); + if let Some(binding) = &binding { + context.push_class_scope(binding.class_scope.clone()); + context.push_called_class_scope(binding.called_class.clone()); + } + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + parameter_is_by_ref, + function.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + context.pop_function(); + return Err(status); + } + }; + let mut function_scope = ElephcEvalScope::new(); + bind_closure_captures(&mut function_scope, closure.captures()); + if let Some(object) = binding.and_then(|binding| binding.this_object) { + function_scope.set("this", object, ScopeCellOwnership::Borrowed); + } + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut function_scope, + function.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + let capture_writeback_result = + write_back_closure_ref_captures(closure.captures(), &function_scope, context, values); + let arg_writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); + let return_result = match ( + persist_result, + capture_writeback_result, + arg_writeback_result, + result, + ) { + (Err(status), _, _, _) + | (_, Err(status), _, _) + | (_, _, Err(status), _) + | (_, _, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + context.pop_function(); + return_result +} + +/// Returns the PHP class name used as the bound scope for `Closure::call()`. +pub(in crate::interpreter) fn eval_closure_bound_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + +/// Seeds one closure activation scope with values captured when the closure was created. +fn bind_closure_captures( + function_scope: &mut ElephcEvalScope, + captures: &[EvalClosureCaptureBinding], +) { + for capture in captures { + if let Some(target) = capture.by_ref_target().cloned() { + function_scope.set_reference( + capture.name().to_string(), + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + function_scope.set_reference_target(capture.name().to_string(), target); + } else { + function_scope.set( + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + } + } +} + +/// Writes modified by-reference closure captures back to their defining caller targets. +fn write_back_closure_ref_captures( + captures: &[EvalClosureCaptureBinding], + function_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for capture in captures { + let Some(target) = capture.by_ref_target() else { + continue; + }; + let Some(entry) = function_scope + .entry(capture.name()) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + Ok(()) +} + +/// Persists static local variables from one eval-declared function activation. +pub(in crate::interpreter) fn persist_static_locals( + context: &mut ElephcEvalContext, + function_name: &str, + names: &[String], + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for name in names { + if let Some(cell) = scope.visible_cell(name) { + if let Some(replaced) = + context.set_static_local(function_name.to_string(), name.clone(), cell) + { + values.release(replaced)?; + } + } + } + Ok(()) +} + +/// One source-order static local declaration and its initializer expression. +#[derive(Clone)] +pub(in crate::interpreter) struct EvalStaticVarInitializer { + pub name: String, + pub init: EvalExpr, +} + +/// Returns the distinct static local names declared anywhere in an eval function body. +pub(in crate::interpreter) fn static_var_names(body: &[EvalStmt]) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, _| { + names.push(name.to_string()); + }); + names +} + +/// Returns static local declarations and initializers in first-seen source order. +pub(in crate::interpreter) fn static_var_initializers( + body: &[EvalStmt], +) -> Vec { + let mut vars = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, init| { + vars.push(EvalStaticVarInitializer { + name: name.to_string(), + init: init.clone(), + }); + }); + vars +} + +/// Visits distinct static local declarations in first-seen source order. +fn visit_static_var_declarations( + body: &[EvalStmt], + seen: &mut std::collections::HashSet, + visitor: &mut impl FnMut(&str, &EvalExpr), +) { + for stmt in body { + match stmt { + EvalStmt::StaticVar { name, init } => { + if seen.insert(name.clone()) { + visitor(name, init); + } + } + EvalStmt::DoWhile { body, .. } + | EvalStmt::Foreach { body, .. } + | EvalStmt::For { body, .. } + | EvalStmt::While { body, .. } => visit_static_var_declarations(body, seen, visitor), + EvalStmt::FunctionDecl { .. } => {} + EvalStmt::If { + then_branch, + else_branch, + .. + } => { + visit_static_var_declarations(then_branch, seen, visitor); + visit_static_var_declarations(else_branch, seen, visitor); + } + EvalStmt::Switch { cases, .. } => { + for case in cases { + visit_static_var_declarations(&case.body, seen, visitor); + } + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + visit_static_var_declarations(body, seen, visitor); + for catch in catches { + visit_static_var_declarations(&catch.body, seen, visitor); + } + visit_static_var_declarations(finally_body, seen, visitor); + } + EvalStmt::ArrayAppendVar { .. } + | EvalStmt::ArraySetVar { .. } + | EvalStmt::Break + | EvalStmt::ClassDecl(_) + | EvalStmt::Continue + | EvalStmt::Echo(_) + | EvalStmt::EnumDecl(_) + | EvalStmt::Expr(_) + | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertyArrayAppend { .. } + | EvalStmt::DynamicPropertyArraySet { .. } + | EvalStmt::DynamicPropertyCompoundAssign { .. } + | EvalStmt::DynamicPropertyIncDec { .. } + | EvalStmt::DynamicPropertyReferenceBind { .. } + | EvalStmt::DynamicPropertySet { .. } + | EvalStmt::DynamicStaticPropertyArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyArraySet { .. } + | EvalStmt::DynamicStaticPropertyIncDec { .. } + | EvalStmt::DynamicStaticPropertyReferenceBind { .. } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyNameArraySet { .. } + | EvalStmt::DynamicStaticPropertyNameIncDec { .. } + | EvalStmt::DynamicStaticPropertyNameReferenceBind { .. } + | EvalStmt::DynamicStaticPropertyNameSet { .. } + | EvalStmt::DynamicStaticPropertySet { .. } + | EvalStmt::PropertyReferenceBind { .. } + | EvalStmt::PropertyArrayAppend { .. } + | EvalStmt::PropertyArraySet { .. } + | EvalStmt::PropertyCompoundAssign { .. } + | EvalStmt::PropertyIncDec { .. } + | EvalStmt::PropertySet { .. } + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::Return(_) + | EvalStmt::StaticPropertyArrayAppend { .. } + | EvalStmt::StaticPropertyArraySet { .. } + | EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } + | EvalStmt::StaticPropertySet { .. } + | EvalStmt::StoreVar { .. } + | EvalStmt::Throw(_) + | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetArrayElement { .. } + | EvalStmt::UnsetDynamicProperty { .. } + | EvalStmt::UnsetDynamicStaticProperty { .. } + | EvalStmt::UnsetDynamicStaticPropertyName { .. } + | EvalStmt::UnsetProperty { .. } + | EvalStmt::UnsetStaticProperty { .. } + | EvalStmt::UnsetVar { .. } => {} + } + } +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs new file mode 100644 index 0000000000..8905fe950b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs @@ -0,0 +1,515 @@ +//! Purpose: +//! Binds evaluated arguments for eval-declared and native functions. +//! +//! Called from: +//! - Dynamic function and callable dispatch after source-order argument evaluation. +//! +//! Key details: +//! - Named, variadic, by-reference, and raw native arguments preserve PHP binding rules. + +use super::*; + +/// Binds evaluated positional and named values to declared parameter order. +pub(in crate::interpreter) fn bind_evaluated_function_args( + params: &[String], + evaluated_args: Vec, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Binds already evaluated native AOT function args and fills omitted defaults. +pub(in crate::interpreter) fn bind_evaluated_native_function_args( + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Binds native AOT function args for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn bind_evaluated_native_function_args_for_call_user_func( + callable_name: &str, + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::WarnByValue { callable_name }, + context, + values, + ) +} + +/// Binds already evaluated native AOT function args using the selected by-reference mode. +fn bind_evaluated_native_function_args_with_mode( + function: &NativeFunction, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if native_function_variadic_index(function).is_some() { + return bind_evaluated_native_variadic_function_args( + function, + evaluated_args, + by_ref_mode, + context, + values, + ); + } + let mut bound_args = vec![None; function.param_count()]; + let has_param_names = function.param_names().len() == function.param_count(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + bind_native_function_named_arg( + function, + None, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + bind_native_function_positional_arg( + function, + &mut bound_args, + None, + &mut next_positional, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } + } + + for (position, bound) in bound_args.iter_mut().enumerate() { + if bound.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; + stage_native_function_invoker_args(function, None, bound_args, by_ref_mode, values) +} + +/// Binds a native AOT variadic function while keeping the raw invoker argument layout. +fn bind_evaluated_native_variadic_function_args( + function: &NativeFunction, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let variadic_index = native_function_variadic_index(function).ok_or(EvalStatus::RuntimeFatal)?; + let has_param_names = function.param_names().len() == function.param_count(); + let mut regular_args = vec![None; variadic_index]; + let mut variadic_args = Vec::new(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + if native_function_regular_param_index(function, variadic_index, &name).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + bind_native_function_named_arg( + function, + Some(variadic_index), + &mut regular_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else if next_positional < variadic_index { + bind_native_function_positional_arg( + function, + &mut regular_args, + Some(variadic_index), + &mut next_positional, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + let ref_target = native_function_parameter_ref_target( + function, + Some(variadic_index), + arg.ref_target, + by_ref_mode, + values, + )?; + variadic_args.push(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + } + } + + for (position, bound) in regular_args.iter_mut().enumerate() { + if bound.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = regular_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + bound_args.extend(variadic_args); + apply_native_function_arg_types( + function, + Some(variadic_index), + &mut bound_args, + context, + values, + )?; + stage_native_function_invoker_args( + function, + Some(variadic_index), + bound_args, + by_ref_mode, + values, + ) +} + +/// Applies registered native AOT function parameter types after argument binding. +fn apply_native_function_arg_types( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + let Some(param_type) = function.param_type(param_index) else { + continue; + }; + bound_arg.value = eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + Ok(()) +} + +/// Binds one named native AOT function argument to a non-variadic parameter slot. +fn bind_native_function_named_arg( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(param_index) = native_function_named_param_index(function, variadic_index, name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + Ok(()) +} + +/// Binds one positional native AOT function argument to the next fixed parameter. +fn bind_native_function_positional_arg( + function: &NativeFunction, + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let param_index = *next_positional; + if variadic_index.is_some_and(|index| param_index >= index) + || param_index >= bound_args.len() + || bound_args[param_index].is_some() + { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Returns the caller writeback target required by a native function by-reference parameter. +fn native_function_parameter_ref_target( + function: &NativeFunction, + param_index: Option, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !function.param_by_ref(param_index) { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_function_param_warning_name(function, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } +} + +/// Converts bound values into descriptor-invoker arguments, staging by-reference slots. +fn stage_native_function_invoker_args( + function: &NativeFunction, + variadic_index: Option, + bound_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut invoker_values = Vec::with_capacity(bound_args.len()); + let mut ref_slots = Vec::new(); + for (position, bound_arg) in bound_args.into_iter().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !function.param_by_ref(param_index) { + invoker_values.push(bound_arg.value); + continue; + } + let target = match (bound_arg.ref_target, by_ref_mode) { + (Some(target), _) => Some(target), + (None, EvalByRefBindingMode::WarnByValue { .. }) => None, + (None, EvalByRefBindingMode::RequireTarget) => return Err(EvalStatus::RuntimeFatal), + }; + if let Some(raw_ref_kind) = native_function_raw_ref_kind(function.param_type(param_index)) { + match raw_ref_kind { + NativeFunctionRawRefKind::Scalar { tag } => { + let original = values.raw_value_word(bound_arg.value)?; + let mut slot = Box::new(original); + let marker = + values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + }); + } + NativeFunctionRawRefKind::String => { + let original_ptr = values.raw_value_word(bound_arg.value)?; + let original_len = values.raw_value_high_word(bound_arg.value)?; + let retained = values.retain_raw_string_words(original_ptr, original_len)?; + let mut slot = Box::new([retained.0, retained.1]); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut [u64; 2] as *mut c_void, + EVAL_TAG_STRING, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawString { + original: [retained.0, retained.1], + slot, + target, + }); + } + NativeFunctionRawRefKind::OwnedHeap => { + let source_tag = values.type_tag(bound_arg.value)?; + let original = values.raw_value_word(bound_arg.value)?; + let retained = values.retain_raw_heap_word(original)?; + let mut slot = Box::new(retained); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut u64 as *mut c_void, + source_tag, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + }); + } + } + continue; + } + let original = bound_arg.value; + let retained = values.retain(original)?; + let mut slot = Box::new(retained); + let marker = match values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle) { + Ok(marker) => marker, + Err(status) => { + values.release(retained)?; + return Err(status); + } + }; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::Mixed { + original, + slot, + target, + }); + } + Ok(BoundNativeFunctionArgs { + values: invoker_values, + ref_slots, + }) +} + +/// Returns the PHP parameter name used in by-reference warning diagnostics. +fn native_function_param_warning_name(function: &NativeFunction, param_index: usize) -> String { + function + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) +} + +/// Describes native function by-reference parameters that can use typed raw slots. +enum NativeFunctionRawRefKind { + Scalar { tag: u64 }, + String, + OwnedHeap, +} + +/// Returns the raw-slot strategy for one supported by-reference parameter. +fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Option { + let param_type = param_type?; + if param_type.allows_null() + || param_type.is_intersection() + || param_type.variants().len() != 1 + { + return None; + } + match param_type.variants().first()? { + EvalParameterTypeVariant::Array + | EvalParameterTypeVariant::Class(_) + | EvalParameterTypeVariant::Iterable + | EvalParameterTypeVariant::Object => Some(NativeFunctionRawRefKind::OwnedHeap), + EvalParameterTypeVariant::Bool => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_BOOL }), + EvalParameterTypeVariant::Float => { + Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_FLOAT }) + } + EvalParameterTypeVariant::Int => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_INT }), + EvalParameterTypeVariant::String => Some(NativeFunctionRawRefKind::String), + _ => None, + } +} + +/// Returns the variadic parameter index for a native AOT function, if registered. +pub(super) fn native_function_variadic_index(function: &NativeFunction) -> Option { + (0..function.param_count()).find(|index| function.param_variadic(*index)) +} + +/// Returns the native function parameter index for one named argument. +fn native_function_named_param_index( + function: &NativeFunction, + variadic_index: Option, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Returns the non-variadic native function parameter index for one named argument. +fn native_function_regular_param_index( + function: &NativeFunction, + variadic_index: usize, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| index < variadic_index && param == name) +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs new file mode 100644 index 0000000000..979ce94260 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs @@ -0,0 +1,816 @@ +//! Purpose: +//! Binds and coerces evaluated method arguments against eval signatures. +//! +//! Called from: +//! - Dynamic instance, static, closure, and reflected method invocation. +//! +//! Key details: +//! - Reference targets, scalar coercion, defaults, and object type checks stay aligned by index. + +use super::*; + +/// Binds evaluated method arguments using a selected by-reference target policy. +pub(in crate::interpreter) fn bind_evaluated_method_args_with_ref_mode( + params: &[String], + parameter_types: &[Option], + parameter_defaults: &[Option], + parameter_is_by_ref: &[bool], + parameter_is_variadic: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); + let required_count = + method_required_param_count(params.len(), parameter_defaults, parameter_is_variadic); + let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + let mut variadic_named_args = std::collections::HashSet::new(); + + if let Some(index) = variadic_index { + let array = if evaluated_args_contain_named_variadic_values( + params, + variadic_index, + &evaluated_args, + ) { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_method_arg( + params, + parameter_types, + parameter_is_by_ref, + variadic_index, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + &mut variadic_named_args, + context, + values, + )?; + } else { + bind_dynamic_positional_method_arg( + params, + &mut bound_args, + parameter_types, + parameter_is_by_ref, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + arg.ref_target, + by_ref_mode, + context, + values, + )?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } + if value.is_none() { + if position < required_count { + return Err(EvalStatus::RuntimeFatal); + } + let Some(Some(default)) = parameter_defaults.get(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(BoundMethodArg { + value: eval_method_parameter_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + if let Some(param_type) = parameter_types.get(position).and_then(Option::as_ref) { + let bound = value.as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = eval_method_parameter_value(param_type, bound.value, context, values)?; + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns the minimum argument count for a PHP method signature. +fn method_required_param_count( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns true when evaluated args contain named values captured by a variadic parameter. +fn evaluated_args_contain_named_variadic_values( + params: &[String], + variadic_index: Option, + evaluated_args: &[EvaluatedCallArg], +) -> bool { + let Some(variadic_index) = variadic_index else { + return false; + }; + evaluated_args.iter().any(|arg| { + arg.name.as_ref().is_some_and(|name| { + regular_method_param_index(params, Some(variadic_index), name).is_none() + }) + }) +} + +/// Binds one positional method argument to a fixed parameter or variadic array. +fn bind_dynamic_positional_method_arg( + params: &[String], + bound_args: &mut [Option], + parameter_types: &[Option], + parameter_is_by_ref: &[bool], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let argument_number = variadic_index + .and_then(|index| { + usize::try_from(*next_variadic_index) + .ok() + .and_then(|offset| index.checked_add(offset)) + }) + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; + return bind_dynamic_variadic_arg( + bound_args, + variadic_index, + key, + value, + ref_target, + values, + ); + } + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Binds one named method argument to a fixed parameter or variadic array. +fn bind_dynamic_named_method_arg( + params: &[String], + parameter_types: &[Option], + parameter_is_by_ref: &[bool], + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + variadic_named_args: &mut std::collections::HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + return Ok(()); + } + if variadic_index.is_none() || !variadic_named_args.insert(name.to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + let key = values.string(name)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; + let argument_number = variadic_index + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; + bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) +} + +/// Returns the caller writeback target required by a by-reference method parameter. +fn method_parameter_ref_target( + params: &[String], + parameter_is_by_ref: &[bool], + param_index: Option, + argument_number: usize, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + argument_number + ))?; + Ok(None) + } + } +} + +/// Returns the by-reference flags that should be installed into the callee scope. +pub(in crate::interpreter) fn method_scope_parameter_ref_flags( + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, +) -> Vec { + if matches!(by_ref_mode, EvalByRefBindingMode::RequireTarget) { + return parameter_is_by_ref.to_vec(); + } + parameter_is_by_ref + .iter() + .enumerate() + .map(|(position, is_by_ref)| { + *is_by_ref + && bound_args.get(position).is_some_and(|arg| { + arg.ref_target.is_some() || !arg.variadic_ref_targets.is_empty() + }) + }) + .collect() +} + +/// Applies a variadic parameter type to one captured argument value. +fn eval_variadic_method_parameter_value( + parameter_types: &[Option], + variadic_index: Option, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(param_type) = + variadic_index.and_then(|index| parameter_types.get(index).and_then(Option::as_ref)) + else { + return Ok(value); + }; + eval_method_parameter_value(param_type, value, context, values) +} + +/// Returns the matching non-variadic parameter index for one PHP named argument. +fn regular_method_param_index( + params: &[String], + variadic_index: Option, + name: &str, +) -> Option { + params + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the method variadic array. +fn bind_dynamic_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = values.array_set(bound.value, key, value)?; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } + Ok(()) +} + +/// Applies one eval method parameter type to a bound runtime value. +pub(in crate::interpreter) fn eval_method_parameter_value( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { + return Ok(value); + } + if param_type.is_intersection() { + return Err(EvalStatus::RuntimeFatal); + } + for variant in param_type.variants() { + if let Some(coerced) = + eval_method_parameter_scalar_coercion(variant, value, context, values)? + { + return Ok(coerced); + } + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns whether a value satisfies one eval parameter type without scalar coercion. +fn eval_method_parameter_type_accepts_exact( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + if tag == EVAL_TAG_NULL && param_type.allows_null() { + return Ok(true); + } + if param_type.is_intersection() { + for variant in param_type.variants() { + if !eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(false); + } + } + return Ok(true); + } + for variant in param_type.variants() { + if eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether a value exactly satisfies one non-null eval parameter type atom. +fn eval_method_parameter_variant_accepts_exact( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + tag: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match variant { + EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), + EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), + EvalParameterTypeVariant::Callable => Ok(matches!( + tag, + EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT + )), + EvalParameterTypeVariant::Class(class_name) => { + eval_method_parameter_class_accepts(value, tag, class_name, context, values) + } + EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), + EvalParameterTypeVariant::Iterable => { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if eval_method_parameter_class_accepts(value, tag, "Traversable", context, values)? { + return Ok(true); + } + eval_method_parameter_class_accepts(value, tag, "Iterator", context, values) + } + EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), + EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), + EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), + } +} + +/// Returns whether an object value satisfies one class/interface parameter target. +fn eval_method_parameter_class_accepts( + value: RuntimeCellHandle, + tag: u64, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + let target = eval_method_parameter_runtime_class_name(class_name, context)?; + let identity = values.object_identity(value)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(context.class_is_a(class.name(), &target, false)); + } + values.object_is_a(value, &target, false) +} + +/// Resolves late-bound class keywords inside eval method parameter type checks. +fn eval_method_parameter_runtime_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "static" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Applies PHP weak-mode scalar coercion for supported scalar parameter types. +pub(in crate::interpreter) fn eval_method_parameter_scalar_coercion( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let tag = values.type_tag(value)?; + match variant { + EvalParameterTypeVariant::Bool if eval_method_scalar_coercible_tag(tag) => { + values.cast_bool(value).map(Some) + } + EvalParameterTypeVariant::Float + if eval_method_numeric_coercible_value(value, tag, values)? => + { + values.cast_float(value).map(Some) + } + EvalParameterTypeVariant::Int + if eval_method_numeric_coercible_value(value, tag, values)? => + { + values.cast_int(value).map(Some) + } + EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { + values.cast_string(value).map(Some) + } + EvalParameterTypeVariant::String if tag == EVAL_TAG_OBJECT => { + let coerced = eval_dynamic_object_string_context_value(value, context, values)?; + if values.type_tag(coerced)? == EVAL_TAG_STRING { + Ok(Some(coerced)) + } else { + Ok(None) + } + } + _ => Ok(None), + } +} + +/// Converts objects in string contexts through the applicable `__toString()` dispatch path. +pub(in crate::interpreter) fn eval_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(value); + } + eval_dynamic_object_string_context_value(value, context, values) +} + +/// Invokes `__toString()` for eval-declared objects or throws PHP's missing-hook error. +fn eval_dynamic_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(value)?; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_runtime_object_string_context_value(value, context, values); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") + else { + return eval_throw_object_to_string_error(&called_class_name, context, values); + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() + || method.is_abstract() + || !method.params().is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + value, + Vec::new(), + context, + values, + )?; + eval_tostring_result_to_string(result, values) +} + +/// Invokes the interpreter method dispatcher for AOT/native objects in string contexts. +fn eval_runtime_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(value)?; + let class_name = runtime_object_class_name(value, values)?; + if !eval_runtime_object_has_interpreter_tostring(identity, &class_name, context) + && eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__toString", + context, + values, + )? + .is_none() + { + return eval_throw_object_to_string_error(&class_name, context, values); + } + let result = eval_method_call_result_with_evaluated_args( + value, + "__toString", + Vec::new(), + context, + values, + )?; + eval_tostring_result_to_string(result, values) +} + +/// Returns whether eval owns a synthetic `__toString()` implementation for the runtime object. +fn eval_runtime_object_has_interpreter_tostring( + identity: u64, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.eval_reflection_class_name(identity).is_some() + || context.eval_reflection_function_name(identity).is_some() + || context.eval_reflection_method(identity).is_some() + || context.eval_reflection_property(identity).is_some() + || context.eval_reflection_class_constant(identity).is_some() + || eval_runtime_class_name_has_reflection_tostring(class_name) +} + +/// Returns whether a synthetic reflection class has `__toString()` handled by eval dispatch. +fn eval_runtime_class_name_has_reflection_tostring(class_name: &str) -> bool { + let class_name = class_name.trim_start_matches('\\'); + [ + "ReflectionParameter", + "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", + ] + .iter() + .any(|reflection_class| class_name.eq_ignore_ascii_case(reflection_class)) +} + +/// Throws PHP's catchable object-to-string conversion error. +fn eval_throw_object_to_string_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of class {} could not be converted to string", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} + +/// Normalizes one `__toString()` result to a boxed string cell. +fn eval_tostring_result_to_string( + result: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(result)? == EVAL_TAG_STRING { + return Ok(result); + } + let coerced = values.cast_string(result)?; + values.release(result)?; + Ok(coerced) +} + +/// Returns whether a runtime tag can be weakly coerced to string/bool parameters. +fn eval_method_scalar_coercible_tag(tag: u64) -> bool { + matches!( + tag, + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL + ) +} + +/// Returns whether a runtime value can be weakly coerced to a numeric parameter. +fn eval_method_numeric_coercible_value( + value: RuntimeCellHandle, + tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL => Ok(true), + EVAL_TAG_STRING => Ok(eval_is_numeric_string(&values.string_bytes(value)?)), + _ => Ok(false), + } +} + +/// Materializes a supported eval method parameter default expression. +pub(in crate::interpreter) fn eval_method_parameter_default( + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_method_default_expr_is_supported(default) { + return Err(EvalStatus::UnsupportedConstruct); + } + let mut default_scope = ElephcEvalScope::new(); + eval_expr(default, context, &mut default_scope, values) +} + +/// Returns whether an EvalIR expression can be safely evaluated as a method default. +fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements + .iter() + .all(eval_method_default_array_element_is_supported), + EvalExpr::Const(_) | EvalExpr::Magic(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + eval_method_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_method_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_method_default_call_arg_is_supported) + } + EvalExpr::NewAnonymousClass { .. } => false, + EvalExpr::NullCoalesce { value, default } => { + eval_method_default_expr_is_supported(value) + && eval_method_default_expr_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_method_default_expr_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_method_default_expr_is_supported) + && eval_method_default_expr_is_supported(else_branch) + } + EvalExpr::Cast { expr, .. } => eval_method_default_expr_is_supported(expr), + EvalExpr::Unary { expr, .. } => eval_method_default_expr_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_method_default_expr_is_supported(left) + && eval_method_default_expr_is_supported(right) + } + _ => false, + } +} + +/// Returns whether one object-construction argument is safe inside a method default. +fn eval_method_default_call_arg_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_method_default_expr_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +fn eval_method_default_array_element_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_method_default_expr_is_supported(value), + EvalArrayElement::Reference(_) => false, + EvalArrayElement::KeyValue { key, value } => { + eval_method_default_expr_is_supported(key) + && eval_method_default_expr_is_supported(value) + } + EvalArrayElement::KeyReference { .. } => false, + } +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +fn eval_method_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} + +/// Binds one positional dynamic-call value to the next declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_positional_arg( + bound_args: &mut [Option], + next_positional: &mut usize, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[*next_positional] = Some(value); + *next_positional += 1; + Ok(()) +} + +/// Binds one named dynamic-call value to the matching declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_named_arg( + params: &[String], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs new file mode 100644 index 0000000000..0bcc35274f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs @@ -0,0 +1,253 @@ +//! Purpose: +//! Executes registered native functions and balances temporary argument arrays. +//! +//! Called from: +//! - Dynamic function dispatch after native signature binding. +//! +//! Key details: +//! - By-reference writeback and temporary runtime-cell ownership are handled together. + +use super::*; + +/// Evaluates a registered AOT function through its descriptor-compatible invoker. +pub(in crate::interpreter) fn eval_native_function( + function: NativeFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + eval_native_function_call_args(&function, args, context, caller_scope, values)?; + eval_native_function_with_values(function, evaluated_args, context, values) +} + +/// Invokes a registered AOT function after its arguments have been bound and staged. +pub(in crate::interpreter) fn eval_native_function_with_values( + function: NativeFunction, + bound_args: BoundNativeFunctionArgs, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !function.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } + let variadic_index = native_function_variadic_index(&function); + if variadic_index.is_none() && bound_args.values.len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(variadic_index) = variadic_index { + if bound_args.values.len() < function.required_param_count().min(variadic_index) { + return Err(EvalStatus::RuntimeFatal); + } + } + let arg_array = match build_native_function_arg_array(&bound_args, values) { + Ok(arg_array) => arg_array, + Err(status) => { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } + }; + let result = unsafe { function.call(arg_array) }; + if let Err(status) = values.release(arg_array) { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } + let result = values.native_call_result(result); + let writeback = write_back_native_function_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => { + eval_declared_native_return_value(function.return_type(), None, None, result, context, values) + } + } +} + +/// Builds the positional runtime array passed to descriptor-compatible native invokers. +fn build_native_function_arg_array( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result { + let arg_array = values.array_new(bound_args.values.len())?; + for (index, value) in bound_args.values.iter().copied().enumerate() { + let index = match values.int(index as i64) { + Ok(index) => index, + Err(status) => { + values.release(arg_array)?; + return Err(status); + } + }; + if let Err(status) = values.array_set(arg_array, index, value) { + values.release(arg_array)?; + return Err(status); + } + } + Ok(arg_array) +} + +/// Releases retained raw native-function by-reference staging slots without writeback. +fn cleanup_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + match ref_slot { + BoundNativeFunctionRefSlot::RawString { original, slot, .. } => { + let words = **slot; + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + } + BoundNativeFunctionRefSlot::OwnedRawWord { original, slot, .. } => { + let word = **slot; + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + } + BoundNativeFunctionRefSlot::Mixed { slot, .. } => { + values.release(**slot)?; + } + BoundNativeFunctionRefSlot::RawWord { .. } => {} + } + } + Ok(()) +} + +/// Writes changed staged native-function by-reference slots back to eval caller targets. +fn write_back_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + match ref_slot { + BoundNativeFunctionRefSlot::Mixed { + original, + slot, + target, + } => { + let value = **slot; + if value == *original { + values.release(value)?; + continue; + } + let Some(target) = target else { + values.release(value)?; + continue; + }; + let current = match eval_reference_target_value(target, context, values) { + Ok(current) => current, + Err(status) => { + values.release(value)?; + return Err(status); + } + }; + if current == value { + values.release(value)?; + continue; + } + if let Err(status) = eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) { + values.release(value)?; + return Err(status); + } + } + BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + } => { + let word = **slot; + if word == *original { + continue; + } + let Some(target) = target else { + continue; + }; + let value = values.raw_word_value(*tag, word)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + BoundNativeFunctionRefSlot::RawString { + original, + slot, + target, + } => { + let words = **slot; + if target.is_none() { + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; + if words == *original { + values.release_raw_string_words(words[0], words[1])?; + continue; + } + let value = values.raw_string_value(words[0], words[1]); + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + } => { + let word = **slot; + if target.is_none() { + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; + if word == *original { + values.release_raw_heap_word(word)?; + continue; + } + let value = values.raw_heap_word_value(word); + values.release_raw_heap_word(word)?; + values.release_raw_heap_word(*original)?; + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + } + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs new file mode 100644 index 0000000000..1b406f8c54 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -0,0 +1,378 @@ +//! Purpose: +//! Evaluates EvalIR expressions, match expressions, and function-like calls. +//! +//! Called from: +//! - `crate::interpreter::statements` for expression statements and expression-bearing statements. +//! - Eval builtin modules when they need to evaluate unevaluated argument expressions. +//! +//! Key details: +//! - PHP call argument evaluation order is preserved before binding or ABI-like materialization. +//! - Language constructs such as `eval`, `isset`, `empty`, and `unset` receive unevaluated expressions. + +use super::*; + +mod calls; + +pub(in crate::interpreter) use calls::*; +mod evaluation; + +pub(in crate::interpreter) use evaluation::{ + eval_array_access_object_matches, eval_array_get_result, eval_binary_result, + eval_dynamic_class_name, eval_dynamic_member_name, eval_match_expr, +}; +use evaluation::*; + +/// Evaluates one expression to an opaque runtime-cell handle. +pub(in crate::interpreter) fn eval_expr( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match expr { + EvalExpr::Array(elements) => { + if elements + .iter() + .any(|element| { + matches!( + element, + EvalArrayElement::KeyValue { .. } + | EvalArrayElement::KeyReference { .. } + ) + }) + { + eval_assoc_array(elements, context, scope, values) + } else { + eval_indexed_array(elements, context, scope, values) + } + } + EvalExpr::ArrayGet { array, index } => { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + eval_array_get_result(array, index, context, values) + } + EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), + EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), + EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::Closure { + function, + captures, + is_static, + } => eval_closure_expr(function, captures, *is_static, context, scope, values), + EvalExpr::FunctionCallable { + name, + fallback_name, + } => eval_function_callable_expr(name, fallback_name.as_deref(), context, values), + EvalExpr::InvokableCallable { object } => { + eval_invokable_callable_expr(object, context, scope, values) + } + EvalExpr::MethodCallable { object, method } => { + eval_method_callable_expr(object, method, context, scope, values) + } + EvalExpr::StaticMethodCallable { class_name, method } => { + eval_static_method_callable_expr(class_name, method, context, scope, values) + } + EvalExpr::DynamicStaticMethodCallable { class_name, method } => { + eval_dynamic_static_method_callable_expr(class_name, method, context, scope, values) + } + EvalExpr::DynamicCall { callee, args } => { + eval_dynamic_call(callee, args, context, scope, values) + } + EvalExpr::DynamicMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + &method, + evaluated_args, + context, + values, + ) + } + EvalExpr::DynamicNewObject { class_name, args } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let args = eval_method_call_arg_values(args, context, scope, values)?; + eval_new_object_result(&class_name, args, context, scope, values) + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_get_result(object, &property, context, values) + } + EvalExpr::DynamicStaticMethodCall { + class_name, + method, + args, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_static_method_call_result_from_scope( + &class_name, + &method, + evaluated_args, + scope, + context, + values, + ) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_get_result(&class_name, property, context, values) + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_get_result(&class_name, &property, context, values) + } + EvalExpr::DynamicClassConstantFetch { + class_name, + constant, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_class_constant_fetch_result(&class_name, constant, context, values) + } + EvalExpr::DynamicClassConstantNameFetch { + class_name, + constant, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let constant = eval_dynamic_member_name(constant, context, scope, values)?; + eval_class_constant_fetch_result(&class_name, &constant, context, values) + } + EvalExpr::DynamicClassNameFetch { class_name } => { + let class_name = eval_expr(class_name, context, scope, values)?; + eval_dynamic_class_name_fetch_result(class_name, context, values) + } + EvalExpr::Include { + path, + required, + once, + } => eval_include_expr(path, *required, *once, context, scope, values), + EvalExpr::InstanceOf { value, target } => { + eval_instanceof_expr(value, target, context, scope, values) + } + EvalExpr::LoadVar(name) => { + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } + EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), + EvalExpr::Match { + subject, + arms, + default, + } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), + EvalExpr::Clone(object) => { + let object = eval_expr(object, context, scope, values)?; + eval_object_clone_result(object, context, values) + } + EvalExpr::NamespacedCall { + name, + fallback_name, + args, + } => eval_namespaced_call(name, fallback_name, args, context, scope, values), + EvalExpr::NamespacedConstFetch { + name, + fallback_name, + } => eval_namespaced_const_fetch(name, fallback_name, context, values), + EvalExpr::NewObject { class_name, args } => { + let args = eval_method_call_arg_values(args, context, scope, values)?; + let class_name = eval_new_object_class_name(class_name, context)?; + eval_new_object_result(&class_name, args, context, scope, values) + } + EvalExpr::NewAnonymousClass { class, args } => { + ensure_eval_anonymous_class_decl(class, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + let class = context + .class(class.name()) + .cloned() + .ok_or(EvalStatus::RuntimeFatal)?; + eval_dynamic_class_new_object(&class, evaluated_args, context, scope, values) + } + EvalExpr::StaticMethodCall { + class_name, + method, + args, + } => { + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_static_method_call_result_from_scope( + class_name, + method, + evaluated_args, + scope, + context, + values, + ) + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => eval_static_property_get_result(class_name, property, context, values), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => eval_class_constant_fetch_result(class_name, constant, context, values), + EvalExpr::ClassNameFetch { class_name } => { + eval_class_name_fetch_result(class_name, context, values) + } + EvalExpr::MethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + method, + evaluated_args, + context, + values, + ) + } + EvalExpr::NullsafeMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + method, + evaluated_args, + context, + values, + ) + } + EvalExpr::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + &method, + evaluated_args, + context, + values, + ) + } + EvalExpr::NullCoalesce { value, default } => { + let value = eval_expr(value, context, scope, values)?; + if values.is_null(value)? { + eval_expr(default, context, scope, values) + } else { + Ok(value) + } + } + EvalExpr::NullsafePropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + eval_property_get_result(object, property, context, values) + } + EvalExpr::NullsafeDynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_get_result(object, &property, context, values) + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_get_result(object, property, context, values) + } + EvalExpr::Print(inner) => { + let value = eval_expr(inner, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + values.echo(value)?; + values.int(1) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + if let Some(then_branch) = then_branch { + eval_expr(then_branch, context, scope, values) + } else { + Ok(condition) + } + } else { + eval_expr(else_branch, context, scope, values) + } + } + EvalExpr::Unary { op, expr } => { + let value = eval_expr(expr, context, scope, values)?; + match op { + EvalUnaryOp::Plus => { + let zero = values.int(0)?; + values.add(zero, value) + } + EvalUnaryOp::Negate => { + let zero = values.int(0)?; + values.sub(zero, value) + } + EvalUnaryOp::LogicalNot => { + let truthy = values.truthy(value)?; + values.bool_value(!truthy) + } + EvalUnaryOp::BitNot => values.bit_not(value), + } + } + EvalExpr::Binary { op, left, right } => { + if *op == EvalBinOp::LogicalAnd { + let left = eval_expr(left, context, scope, values)?; + if !values.truthy(left)? { + return values.bool_value(false); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } + if *op == EvalBinOp::LogicalOr { + let left = eval_expr(left, context, scope, values)?; + if values.truthy(left)? { + return values.bool_value(true); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_binary_result(*op, left, right, context, values) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls.rs b/crates/elephc-magician/src/interpreter/expressions/calls.rs new file mode 100644 index 0000000000..a44858ef0b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls.rs @@ -0,0 +1,196 @@ +//! Purpose: +//! Evaluates function-like EvalIR calls and first-class callable expressions. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` for call-shaped expressions. +//! +//! Key details: +//! - Source-sensitive constructs and by-reference builtins keep unevaluated or +//! ref-target arguments before ordinary registry direct-call dispatch. +//! - Dynamic callables preserve PHP source-order argument evaluation before +//! normalized callable invocation. + +use super::*; + +mod first_class; +mod first_class_support; + +pub(in crate::interpreter) use first_class::*; +use first_class_support::*; + +/// Returns cloned positional argument expressions, rejecting named arguments. +pub(in crate::interpreter) fn positional_call_arg_exprs( + args: &[EvalCallArg], +) -> Result, EvalStatus> { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.iter().map(|arg| arg.value().clone()).collect()) +} + +/// Evaluates method-call arguments, preserving named metadata for eval method binding. +pub(in crate::interpreter) fn eval_method_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_call_arg_values(args, context, scope, values) +} + +/// Evaluates supported function-like calls from a runtime eval fragment. +pub(in crate::interpreter) fn eval_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_expr_language_construct_name(name) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + if name == "flock" { + return eval_builtin_flock(args, context, scope, values); + } + if name == "preg_match" { + return eval_builtin_preg_match_call(args, context, scope, values); + } + if name == "preg_match_all" { + return eval_builtin_preg_match_all_call(args, context, scope, values); + } + if name == "is_callable" { + return eval_builtin_is_callable_call(args, context, scope, values); + } + if matches!(name, "fsockopen" | "pfsockopen") { + return eval_builtin_fsockopen_call(args, context, scope, values); + } + if let Some(result) = eval_date_procedural_alias_call(name, args, context, scope, values)? { + return Ok(result); + } + if name == "stream_select" { + return eval_builtin_stream_select_call(args, context, scope, values); + } + if name == "stream_socket_accept" { + return eval_builtin_stream_socket_accept_call(args, context, scope, values); + } + if name == "stream_socket_recvfrom" { + return eval_builtin_stream_socket_recvfrom_call(args, context, scope, values); + } + if matches!( + name, + "array_pop" + | "array_push" + | "array_shift" + | "array_splice" + | "array_unshift" + | "array_walk" + | "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" + | "settype" + | "uasort" + | "uksort" + | "usort" + ) { + return eval_builtin_array_mutating_declared_call(name, args, context, scope, values); + } + if eval_php_visible_builtin_exists(name) { + if eval_call_args_are_plain_positional(args) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + return eval_builtin_call(name, args, context, scope, values); + } + + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Evaluates an unqualified namespaced function call with PHP's global fallback. +pub(in crate::interpreter) fn eval_namespaced_call( + name: &str, + fallback_name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + eval_call(fallback_name, args, context, scope, values) +} + +/// Evaluates a variable or expression callable and dispatches it with source-order arguments. +pub(in crate::interpreter) fn eval_dynamic_call( + callee: &EvalExpr, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_expr(callee, context, scope, values)?; + if values.type_tag(callback)? == EVAL_TAG_OBJECT { + let is_closure_object = values + .object_identity(callback) + .ok() + .and_then(|identity| context.closure_object_target(identity)) + .is_some(); + if !is_closure_object { + eval_invokable_object_precheck(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + return eval_invokable_object_call_result(callback, evaluated_args, context, values); + } + } + let callback = eval_callable(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Returns true for language constructs that need unevaluated argument expressions. +pub(in crate::interpreter) fn eval_expr_language_construct_name(name: &str) -> bool { + matches!(name, "empty" | "eval" | "isset" | "unset") +} + +/// Returns true when every source argument is plain positional. +pub(in crate::interpreter) fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { + args.iter() + .all(|arg| arg.name().is_none() && !arg.is_spread()) +} + +/// Evaluates registry-backed direct builtins and language constructs after positional-only validation. +pub(in crate::interpreter) fn eval_positional_expr_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "eval" { + return eval_nested_eval(args, context, scope, values); + } + + if let Some(result) = eval_declared_builtin_direct_call(name, args, context, scope, values)? { + return Ok(result); + } + + Err(EvalStatus::UnsupportedConstruct) +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs b/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs new file mode 100644 index 0000000000..34d3917b9f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs @@ -0,0 +1,487 @@ +//! Purpose: +//! Materializes PHP first-class callable expressions into eval Closure objects. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` through `expressions::calls`. +//! +//! Key details: +//! - Object and static-method callables validate visibility, abstract methods, +//! late-static receiver metadata, and generated/AOT bridge scope before capture. + +use super::*; + +/// Resolves a first-class function callable name with PHP namespace fallback rules. +pub(in crate::interpreter) fn eval_function_callable_expr( + name: &str, + fallback_name: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_function_probe_exists(context, name) { + return eval_closure_object_expr( + EvalClosureObjectTarget::Named(name.trim_start_matches('\\').to_ascii_lowercase()), + context, + values, + ); + } + if let Some(fallback_name) = fallback_name { + if eval_function_probe_exists(context, fallback_name) { + return eval_closure_object_expr( + EvalClosureObjectTarget::Named( + fallback_name.trim_start_matches('\\').to_ascii_lowercase(), + ), + context, + values, + ); + } + } + eval_throw_error( + &format!("Call to undefined function {}()", name.trim_start_matches('\\')), + context, + values, + ) +} + +/// Materializes an invokable-object first-class callable as a PHP `Closure` object. +pub(in crate::interpreter) fn eval_invokable_callable_expr( + object: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + eval_invokable_object_precheck(object, context, values)?; + eval_closure_object_expr( + EvalClosureObjectTarget::InvokableObject { object }, + context, + values, + ) +} + +/// Materializes an object method first-class callable and records captured AOT bridge scope. +pub(in crate::interpreter) fn eval_method_callable_expr( + object: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_method_callable_target(object, method, context, values)?; + eval_closure_object_expr(target, context, values) +} + +/// Validates and builds the retained target for an object method first-class callable. +fn eval_method_callable_target( + object: RuntimeCellHandle, + method_name: String, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: None, + native_class: None, + bridge_scope: None, + }); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + return eval_native_object_method_callable_target( + object, + &class_name, + &class_name, + method_name, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_instance_magic_callable_for_class(&called_class_name, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); + } + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + return eval_native_object_method_callable_target( + object, + &parent, + &called_class_name, + method_name, + context, + values, + ); + } + if eval_instance_magic_callable_for_class(&called_class_name, context) { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); + } + eval_first_class_undefined_method_error(&called_class_name, &method_name, context, values) +} + +/// Validates generated/AOT object-method first-class callable metadata. +fn eval_native_object_method_callable_target( + object: RuntimeCellHandle, + native_class: &str, + called_class_name: &str, + method_name: String, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(native_class, &method_name, context, values)? + else { + if eval_native_instance_magic_callable_for_class(native_class, context, values)? + || !values.class_exists(native_class)? + { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + called_class_name, + &method_name, + context, + values, + ); + }; + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_instance_magic_callable_for_class(native_class, context, values)? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); + } + Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: Some(native_class.to_string()), + bridge_scope: Some(declaring_class), + }) +} + +/// Materializes a first-class static method callable while retaining late-static metadata. +pub(in crate::interpreter) fn eval_static_method_callable_expr( + class_name: &str, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_static_method_callable_target( + receiver.dispatch_class, + method, + Some(receiver.called_class), + Some(scope), + context, + values, + )?; + eval_closure_object_expr(target, context, values) +} + +/// Materializes a runtime-class static first-class callable as a PHP `Closure` object. +pub(in crate::interpreter) fn eval_dynamic_static_method_callable_expr( + class_name: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let receiver = resolve_eval_static_method_receiver(&class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_static_method_callable_target( + receiver.dispatch_class, + method, + Some(receiver.called_class), + Some(scope), + context, + values, + )?; + eval_closure_object_expr(target, context, values) +} + +/// Validates and builds the retained target for a static-method first-class callable. +fn eval_static_method_callable_target( + dispatch_class: String, + method_name: String, + called_class: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_static_magic_callable_for_class(&dispatch_class, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if !method.is_static() { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + if context.has_class(&dispatch_class) { + if let Some(parent) = context.class_native_parent_name(&dispatch_class) { + return eval_native_static_method_callable_target( + dispatch_class, + parent, + method_name, + called_class, + lexical_scope, + context, + values, + ); + } + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + eval_native_static_method_callable_target( + dispatch_class.clone(), + dispatch_class, + method_name, + called_class, + lexical_scope, + context, + values, + ) +} + +/// Validates generated/AOT static-method first-class callable metadata. +fn eval_native_static_method_callable_target( + dispatch_class: String, + native_class: String, + method_name: String, + called_class: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + &method_name, + context, + values, + )? + else { + if context + .native_static_method_signature(&native_class, &method_name) + .is_some() + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + if eval_native_static_magic_callable_for_class(&native_class, context, values)? + || !values.class_exists(&native_class)? + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + }; + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_static_magic_callable_for_class(&native_class, context, values)? { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }) +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs b/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs new file mode 100644 index 0000000000..b5634bfde2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Shared magic-call probes and TypeError helpers for first-class callables. +//! +//! Called from: +//! - `crate::interpreter::expressions::calls::first_class`. +//! +//! Key details: +//! - Error messages preserve PHP visibility labels and current eval class scope. +//! - AOT magic-call probes use generated method dispatch metadata. + +use super::*; + +/// Returns whether an eval class has an instance magic-call fallback for a callable. +pub(super) fn eval_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a static magic-call fallback for a callable. +pub(super) fn eval_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} + +/// Returns whether an AOT class has an instance magic-call fallback for a callable. +pub(super) fn eval_native_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether an AOT class has a static magic-call fallback for a callable. +pub(super) fn eval_native_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Throws PHP's first-class callable error for an inaccessible method. +pub(super) fn eval_first_class_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_callable_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_callable_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an instance method used statically. +pub(super) fn eval_first_class_non_static_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an abstract method target. +pub(super) fn eval_first_class_abstract_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for a missing method target. +pub(super) fn eval_first_class_undefined_method_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Returns the current PHP scope label used in callable access errors. +fn eval_callable_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label for callable access errors. +fn eval_callable_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} diff --git a/crates/elephc-magician/src/interpreter/expressions/evaluation.rs b/crates/elephc-magician/src/interpreter/expressions/evaluation.rs new file mode 100644 index 0000000000..810cf0dd46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/evaluation.rs @@ -0,0 +1,391 @@ +//! Purpose: +//! Shared evaluated-expression helpers for operators, class names, closures, and match. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()`. +//! +//! Key details: +//! - Helpers operate after the parent evaluator has selected the expression shape +//! and preserve PHP runtime conversion, class-alias, and closure-capture rules. + +use super::*; + +/// Applies one already-evaluated binary operation with eval runtime semantics. +pub(in crate::interpreter) fn eval_binary_result( + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match op { + EvalBinOp::Add => values.add(left, right), + EvalBinOp::Sub => values.sub(left, right), + EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Div => values.div(left, right), + EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::Pow => values.pow(left, right), + EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight => values.bitwise(op, left, right), + EvalBinOp::Concat => { + let left = eval_string_context_value(left, context, values)?; + let right = eval_string_context_value(right, context, values)?; + values.concat(left, right) + } + EvalBinOp::LogicalXor => { + let left_truthy = values.truthy(left)?; + let right_truthy = values.truthy(right)?; + values.bool_value(left_truthy ^ right_truthy) + } + EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => values.compare(op, left, right), + EvalBinOp::Spaceship => values.spaceship(left, right), + EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. +pub(in crate::interpreter) fn eval_dynamic_member_name( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Reads an array element or dispatches `ArrayAccess::offsetGet()` for objects. +pub(in crate::interpreter) fn eval_array_get_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_OBJECT { + if let Some(target) = eval_array_reference_key(index, values)? + .and_then(|key| context.array_element_alias(array, &key).cloned()) + { + return eval_reference_target_value(&target, context, values); + } + return values.array_get(array, index); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_method_call_result(array, "offsetGet", vec![index], context, values) +} + +/// Returns whether an object value satisfies PHP's `ArrayAccess` interface. +pub(in crate::interpreter) fn eval_array_access_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "ArrayAccess", false, context, values)? + .map_or_else(|| values.object_is_a(value, "ArrayAccess", false), Ok) +} + +/// Evaluates one PHP scalar cast expression through the runtime conversion hooks. +pub(super) fn eval_cast_expr( + target: &EvalCastType, + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + match target { + EvalCastType::Int => values.cast_int(value), + EvalCastType::Float => values.cast_float(value), + EvalCastType::String => { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) + } + EvalCastType::Bool => values.cast_bool(value), + } +} + +/// Constructs an object after the target class name and constructor arguments have been evaluated. +pub(super) fn eval_new_object_result( + class_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(object) = + eval_reflection_owner_new_object(class_name, args.clone(), context, values)? + { + return Ok(object); + } + if let Some(class) = context.class(class_name).cloned() { + return eval_dynamic_class_new_object(&class, args, context, scope, values); + } + let object = values.new_object(class_name)?; + if let Err(err) = + eval_native_constructor_with_evaluated_args(class_name, object, args, context, values) + { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} + +/// Resolves special class names used by `new` while preserving AOT fallback names. +pub(super) fn eval_new_object_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Resolves a runtime class-name value used by dynamic class operations. +pub(in crate::interpreter) fn eval_dynamic_class_name( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(class_name)? { + EVAL_TAG_STRING => { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(class_name, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the runtime class name for `$object::class` and rejects non-object dynamic receivers. +pub(super) fn eval_dynamic_class_name_fetch_result( + class_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(class_name)?; + if tag == EVAL_TAG_OBJECT { + let class_name = eval_instanceof_object_target_name(class_name, context, values)?; + return values.string(&class_name); + } + eval_throw_type_error( + &format!( + "Cannot use \"::class\" on {}", + eval_class_name_fetch_type_error_name(tag) + ), + context, + values, + ) +} + +/// Returns PHP's type label for dynamic `::class` TypeError diagnostics. +fn eval_class_name_fetch_type_error_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "null", + _ => "null", + } +} + +/// Evaluates PHP's `instanceof` operator over static and dynamic class targets. +pub(super) fn eval_instanceof_expr( + value: &EvalExpr, + target: &EvalInstanceOfTarget, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(value, context, scope, values)?; + let result = match target { + EvalInstanceOfTarget::ClassName(class_name) => { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return values.bool_value(false); + } + let target_class = eval_instanceof_static_target_name(class_name, context)?; + eval_instanceof_object_result(value, &target_class, context, values)? + } + EvalInstanceOfTarget::Expr(target) => { + let target = eval_expr(target, context, scope, values)?; + let target_class = eval_instanceof_dynamic_target_name(target, context, values)?; + if values.type_tag(value)? == EVAL_TAG_OBJECT { + eval_instanceof_object_result(value, &target_class, context, values)? + } else { + false + } + } + }; + values.bool_value(result) +} + +/// Resolves a static `instanceof` target according to eval class aliases and scope keywords. +fn eval_instanceof_static_target_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(eval_instanceof_resolved_target_name(class_name, context)), + } +} + +/// Resolves a dynamic `instanceof` target cell to the PHP class name it represents. +fn eval_instanceof_dynamic_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_STRING => { + let target = eval_instanceof_string_target_name(target, values)?; + Ok(eval_instanceof_resolved_target_name(&target, context)) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(target, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads and normalizes one string-valued dynamic `instanceof` target. +fn eval_instanceof_string_target_name( + target: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(target)?; + let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(target.trim_start_matches('\\').to_string()) +} + +/// Reads the runtime class of an object-valued dynamic `instanceof` target. +fn eval_instanceof_object_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(target)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + let class_name = values.object_class_name(target)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Applies eval alias resolution to a target class name without requiring it to exist. +fn eval_instanceof_resolved_target_name(target: &str, context: &ElephcEvalContext) -> String { + context + .resolve_class_name(target) + .unwrap_or_else(|| target.trim_start_matches('\\').to_string()) +} + +/// Tests one object cell against a resolved `instanceof` target class/interface name. +fn eval_instanceof_object_result( + value: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, target_class, false, context, values)? + .map_or_else(|| values.object_is_a(value, target_class, false), Ok) +} + +/// Materializes one eval closure literal as a PHP-visible `Closure` object. +pub(super) fn eval_closure_expr( + function: &EvalFunction, + captures: &[crate::eval_ir::EvalClosureCapture], + is_static: bool, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bindings = Vec::with_capacity(captures.len()); + for capture in captures { + bindings.push(eval_closure_capture(capture, context, scope, values)?); + } + let closure = EvalClosure::new(function.clone(), bindings, is_static); + let name = context.define_closure(closure); + eval_closure_object_expr(EvalClosureObjectTarget::Named(name), context, values) +} + +/// Materializes one PHP-visible `Closure` object for an eval callable target. +pub(super) fn eval_closure_object_expr( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object_target(identity, target); + Ok(object) +} + +/// Evaluates one closure capture from the defining scope. +fn eval_closure_capture( + capture: &crate::eval_ir::EvalClosureCapture, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if capture.by_ref() { + let expr = EvalExpr::LoadVar(capture.name().to_string()); + let (value, target) = eval_call_arg_value(&expr, context, scope, values)?; + return Ok(EvalClosureCaptureBinding::new( + capture.name(), + value, + target, + )); + } + let value = if let Some(value) = visible_scope_cell(context, scope, capture.name()) { + values.retain(value)? + } else { + values.null()? + }; + Ok(EvalClosureCaptureBinding::new(capture.name(), value, None)) +} + +/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. +pub(in crate::interpreter) fn eval_match_expr( + subject: &EvalExpr, + arms: &[EvalMatchArm], + default: Option<&EvalExpr>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(subject, context, scope, values)?; + for arm in arms { + for pattern in &arm.patterns { + let pattern = eval_expr(pattern, context, scope, values)?; + let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; + if values.truthy(matched)? { + return eval_expr(&arm.value, context, scope, values); + } + } + } + default + .map(|expr| eval_expr(expr, context, scope, values)) + .unwrap_or(Err(EvalStatus::RuntimeFatal)) +} diff --git a/crates/elephc-magician/src/interpreter/include_exec.rs b/crates/elephc-magician/src/interpreter/include_exec.rs new file mode 100644 index 0000000000..4de98746d0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/include_exec.rs @@ -0,0 +1,200 @@ +//! Purpose: +//! Executes nested `eval(...)`, include, include_once, require, and require_once expressions. +//! This keeps source-file loading and PHP open/close-tag handling outside the core interpreter loop. +//! +//! Called from: +//! - `crate::interpreter::eval_positional_expr_call()` for `eval(...)`. +//! - `crate::interpreter::eval_expr()` for include/require expression nodes. +//! +//! Key details: +//! - Included code runs against the current eval context and materialized scope. +//! - Missing include emits a warning and returns false; missing require is fatal. + +use super::*; +use crate::parse_cache::parse_fragment_cached; + +/// Evaluates nested `eval(...)` calls against the current materialized scope. +pub(super) fn eval_nested_eval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [code] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let code = eval_expr(code, context, scope, values)?; + let code = values.string_bytes(code)?; + let program = parse_fragment_cached(&code).map_err(EvalParseError::status)?; + execute_program_with_context(context, program.as_ref(), scope, values) +} + +/// Evaluates an eval-fragment include or require expression. +pub(super) fn eval_include_expr( + path: &EvalExpr, + required: bool, + once: bool, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_expr(path, context, scope, values)?; + let path = eval_path_string(path, values)?; + let resolved_path = eval_resolve_include_path(&path, context); + let include_key = eval_include_key(&resolved_path); + if once && context.has_included_file(&include_key) { + return values.bool_value(true); + } + let bytes = match std::fs::read(&resolved_path) { + Ok(bytes) => bytes, + Err(_) => return eval_include_missing_file(&path, required, values), + }; + context.mark_included_file(include_key); + eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) +} + +/// Returns the include/require result for a file that cannot be opened. +fn eval_include_missing_file( + path: &str, + required: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let construct = if required { "require" } else { "include" }; + values.warning(&format!( + "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" + ))?; + values.warning(&format!( + "Warning: {construct}(): Failed opening '{path}' for inclusion\n" + ))?; + if required { + Err(EvalStatus::RuntimeFatal) + } else { + values.bool_value(false) + } +} + +/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. +fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { + let raw_path = std::path::Path::new(path); + if raw_path.is_absolute() || raw_path.exists() { + return raw_path.to_path_buf(); + } + if context.call_dir().is_empty() { + return raw_path.to_path_buf(); + } + let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); + if caller_path.exists() { + caller_path + } else { + raw_path.to_path_buf() + } +} + +/// Builds the stable include_once key for a resolved path. +fn eval_include_key(path: &std::path::Path) -> String { + std::fs::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() +} + +/// Executes a local include file, alternating raw output and PHP code blocks. +fn eval_execute_include_bytes( + bytes: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut cursor = 0; + while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { + eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; + let close = eval_find_php_close_tag(bytes, code_start); + let code_end = close.unwrap_or(bytes.len()); + match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? + { + EvalControl::None => {} + EvalControl::ReturnVoid => return values.null(), + EvalControl::Return(value) => return Ok(value), + EvalControl::Throw(value) => { + context.set_pending_throw(value); + return Err(EvalStatus::UncaughtThrowable); + } + EvalControl::Break | EvalControl::Continue => { + return Err(EvalStatus::UnsupportedConstruct); + } + } + let Some(close) = close else { + return values.int(1); + }; + cursor = close + 2; + } + eval_echo_include_bytes(&bytes[cursor..], values)?; + values.int(1) +} + +/// Parses and executes one PHP code block from an included file. +fn eval_execute_include_code( + code: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let program = parse_fragment_cached(code).map_err(EvalParseError::status)?; + let previous = context.call_site(); + let file = path.to_string_lossy().into_owned(); + let dir = path + .parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .unwrap_or_default(); + context.set_call_site(file.clone(), dir, 1); + context.set_file_magic_override(Some(file)); + let result = execute_statements(program.statements(), context, scope, values); + context.set_call_site(previous.0, previous.1, previous.2); + context.set_file_magic_override(previous.3); + result +} + +/// Echoes raw non-PHP include bytes through the eval value hooks. +fn eval_echo_include_bytes( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bytes.is_empty() { + return Ok(()); + } + let output = values.string_bytes_value(bytes)?; + values.echo(output) +} + +/// Finds the next ` Option<(usize, usize)> { + bytes + .get(start..)? + .windows(5) + .position(eval_is_php_open_tag) + .map(|offset| { + let tag_start = start + offset; + (tag_start, tag_start + 5) + }) +} + +/// Returns true when a five-byte window is a case-insensitive ` bool { + window.len() == 5 + && window[0] == b'<' + && window[1] == b'?' + && window[2].eq_ignore_ascii_case(&b'p') + && window[3].eq_ignore_ascii_case(&b'h') + && window[4].eq_ignore_ascii_case(&b'p') +} + +/// Finds the next PHP closing tag after a code block start. +fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { + bytes + .get(start..)? + .windows(2) + .position(|window| window == b"?>") + .map(|offset| start + offset) +} diff --git a/crates/elephc-magician/src/interpreter/libc_shims.rs b/crates/elephc-magician/src/interpreter/libc_shims.rs new file mode 100644 index 0000000000..1f6898483c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/libc_shims.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declares libc routines used by eval builtins that mirror PHP process and network helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` +//! - `crate::interpreter::builtins::filesystem` +//! +//! Key details: +//! - These bindings are unsafe FFI declarations only; call sites own pointer validation and +//! PHP-compatible fallback behavior. + +unsafe extern "C" { + /// Reverse-resolves one socket address through libc's `gethostbyaddr`. + #[link_name = "gethostbyaddr"] + pub(super) fn libc_gethostbyaddr( + addr: *const libc::c_void, + len: libc::socklen_t, + type_: libc::c_int, + ) -> *mut libc::hostent; + + /// Looks up one IP protocol entry by protocol name or alias. + #[link_name = "getprotobyname"] + pub(super) fn libc_getprotobyname(name: *const libc::c_char) -> *mut libc::protoent; + + /// Looks up one IP protocol entry by protocol number. + #[link_name = "getprotobynumber"] + pub(super) fn libc_getprotobynumber(proto: libc::c_int) -> *mut libc::protoent; + + /// Looks up one internet service entry by service name and protocol. + #[link_name = "getservbyname"] + pub(super) fn libc_getservbyname( + name: *const libc::c_char, + proto: *const libc::c_char, + ) -> *mut libc::servent; + + /// Looks up one internet service entry by port and protocol. + #[link_name = "getservbyport"] + pub(super) fn libc_getservbyport( + port: libc::c_int, + proto: *const libc::c_char, + ) -> *mut libc::servent; + + /// Sets the process file-creation mask and returns the previous mask. + pub(super) fn umask(mask: u32) -> u32; +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs new file mode 100644 index 0000000000..27d861cd7e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -0,0 +1,480 @@ +//! Purpose: +//! Interprets EvalIR against a materialized caller scope. +//! The interpreter is generic over runtime value operations so it can execute +//! by manipulating opaque elephc runtime-cell handles. +//! +//! Called from: +//! - Future `crate::__elephc_eval_execute()` implementation. +//! - `cargo test -p elephc-magician` for scope/value-flow validation. +//! +//! Key details: +//! - This module does not own PHP values. Constants and operations are delegated +//! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. + +mod array_literals; +pub mod builtin_metadata; +mod builtin_interfaces; +mod builtins; +mod constant_eval; +mod constants; +mod control; +mod dynamic_functions; +mod expressions; +mod include_exec; +mod libc_shims; +mod reflection; +mod return_type_compat; +mod return_values; +mod runtime_ops; +mod scope_cells; +mod statements; +mod throwables; + +use crate::context::{ + ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayReferenceKey, EvalReferenceTarget, + EvalClosure, EvalClosureCaptureBinding, EvalClosureObjectTarget, NativeCallableDefault, + NativeCallableSignature, NativeFunction, +}; +use crate::errors::{EvalParseError, EvalStatus}; +use crate::eval_ir::{ + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, + EvalCastType, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, + EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInstanceOfTarget, + EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, + EvalParameterType, EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, + EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, +}; +#[cfg(test)] +use crate::parser::parse_fragment; +use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; +use crate::value::RuntimeCellHandle; +use array_literals::*; +use builtin_interfaces::*; +use builtins::*; +use constant_eval::*; +use constants::*; +pub use control::EvalOutcome; +use control::{ + BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, EvalArraySpliceDirectArgs, + EvalByRefBindingMode, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, + EvaluatedCallArg, EvaluatedCallable, +}; +use dynamic_functions::*; +use expressions::*; +use include_exec::*; +use libc_shims::*; +use reflection::*; +use return_type_compat::*; +use return_values::*; +pub use runtime_ops::RuntimeValueOps; +use runtime_ops::*; +use scope_cells::*; +#[cfg(not(test))] +pub(crate) use statements::eval_dynamic_destructor_for_object_cell; +use statements::*; +use throwables::*; +use std::ffi::{CStr, CString}; +use std::mem::MaybeUninit; +use std::net::ToSocketAddrs; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::sync::atomic::Ordering; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Executes an EvalIR program and returns the eval result cell. +pub fn execute_program( + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut context = ElephcEvalContext::new(); + execute_program_with_context(&mut context, program, scope, values) +} + +/// Executes an EvalIR program with a persistent eval context for dynamic declarations. +pub fn execute_program_with_context( + context: &mut ElephcEvalContext, + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_program_outcome_with_context(context, program, scope, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes an EvalIR program and preserves escaping Throwable cells. +pub fn execute_program_outcome_with_context( + context: &mut ElephcEvalContext, + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(program.statements(), context, scope, values) { + Ok(EvalControl::None | EvalControl::ReturnVoid) => values.null().map(EvalOutcome::Value), + Ok(EvalControl::Return(result)) => Ok(EvalOutcome::Value(result)), + Ok(EvalControl::Throw(result)) => Ok(EvalOutcome::Throwable(result)), + Ok(EvalControl::Break | EvalControl::Continue) => Err(EvalStatus::UnsupportedConstruct), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Executes a zero-argument function declared in the shared eval context. +pub fn execute_context_function_zero_args( + context: &mut ElephcEvalContext, + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + execute_context_function(context, name, Vec::new(), values) +} + +/// Executes a function declared in the shared eval context with prepared argument cells. +pub fn execute_context_function( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_context_function_outcome(context, name, args, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes a function declared in the shared eval context and preserves thrown cells. +pub fn execute_context_function_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + context + .function(name) + .cloned() + .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { + match eval_dynamic_function_with_values(&function, args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } + }) +} + +/// Executes a named eval-context callable with arguments from a PHP array container. +pub fn execute_context_function_call_array( + context: &mut ElephcEvalContext, + name: &str, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_context_function_call_array_outcome(context, name, arg_array, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes a named eval-context callable from an argument array and preserves thrown cells. +pub fn execute_context_function_call_array_outcome( + context: &mut ElephcEvalContext, + name: &str, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = eval_array_call_arg_values(arg_array, context, values)?; + match eval_callable_with_call_array_args(name, evaluated_args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Executes a callback value with a prepared argument array in the shared eval context. +pub fn execute_context_callable_call_array_outcome( + context: &mut ElephcEvalContext, + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_call_user_func_array_with_values(callback, arg_array, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Probes whether a callback value is callable in the shared eval context. +pub fn execute_context_is_callable( + context: &ElephcEvalContext, + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_value(callback, None, context, values) +} + +/// Constructs a class declared in the shared eval context with prepared positional arguments. +pub fn execute_context_new_object_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + execute_context_try_new_object_outcome(context, name, args, values)? + .ok_or(EvalStatus::UnsupportedConstruct) +} + +/// Attempts to construct an eval-declared class, returning `None` when it is absent. +pub fn execute_context_try_new_object_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class) = context.class(name).cloned() else { + return Ok(None); + }; + let evaluated_args = args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + let mut scope = ElephcEvalScope::new(); + match eval_dynamic_class_new_object(&class, evaluated_args, context, &mut scope, values) { + Ok(result) => Ok(Some(EvalOutcome::Value(result))), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .map(Some) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Calls a method on a value that may be an eval-created object. +pub fn execute_context_method_call_outcome( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + method: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_method_call_result(object, method, args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Calls a static method on a class-like symbol known to the shared eval context. +pub fn execute_context_static_method_call_outcome( + context: &mut ElephcEvalContext, + class_name: &str, + method: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + match eval_static_method_call_result(class_name, method, evaluated_args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Resolves object class-name builtins against eval dynamic-object metadata first. +pub fn execute_context_object_class_name( + context: &mut ElephcEvalContext, + lookup: &str, + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match lookup { + "get_class" => eval_get_class_result(object_or_class, context, values), + "get_parent_class" => eval_get_parent_class_result(object_or_class, context, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Resolves class/interface/trait relation metadata through eval dynamic metadata. +pub fn execute_context_class_relation( + context: &mut ElephcEvalContext, + name: &str, + target: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_relation_result(name, &[target], context, values) +} + +/// Fetches a class-like constant through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_class_constant_fetch( + context: &mut ElephcEvalContext, + class_name: &str, + constant_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_class_constant_fetch_result(class_name, constant_name, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Reads a static property through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_static_property_get( + context: &mut ElephcEvalContext, + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_static_property_get_result(class_name, property_name, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Writes a static property through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_static_property_set( + context: &mut ElephcEvalContext, + class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match eval_static_property_set_result(class_name, property_name, value, context, values) { + Ok(()) => Ok(None), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .map(Some) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Tests an object relation against eval dynamic-object metadata before AOT metadata. +pub fn execute_context_object_is_a( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(false); + } + let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_like_name(target_class) + .unwrap_or_else(|| target_class.to_string()); + dynamic_object_is_a( + object, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object, &resolved_target_class, exclude_self), + Ok, + ) +} + +/// Tests an object relation when the target is a runtime string or object cell. +pub fn execute_context_object_is_a_dynamic( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + target: RuntimeCellHandle, + exclude_self: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let target_class = match values.type_tag(target)? { + EVAL_TAG_STRING => { + let bytes = values.string_bytes(target)?; + let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + target.trim_start_matches('\\').to_string() + } + EVAL_TAG_OBJECT => { + let identity = values.object_identity(target)?; + if let Some(class) = context.dynamic_object_class(identity) { + class.name().to_string() + } else { + let class_name = values.object_class_name(target)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + class_name.trim_start_matches('\\').to_string() + } + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + execute_context_object_is_a(context, object, &target_class, exclude_self, values) +} + +/// Tests whether a method or property exists through eval dynamic metadata. +pub fn execute_context_member_exists( + context: &mut ElephcEvalContext, + name: &str, + target: RuntimeCellHandle, + member: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_member_exists_result(name, &[target, member], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} + +/// Returns the current interpreter availability status for the ABI stub. +pub fn current_stub_status() -> EvalStatus { + EvalStatus::UnsupportedConstruct +} + +#[cfg(test)] +mod tests; diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs new file mode 100644 index 0000000000..a82462c6f3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -0,0 +1,349 @@ +//! Purpose: +//! Coordinates eval-aware Reflection dispatch and shared metadata shapes. +//! Owner-specific APIs, construction, lookup, formatting, and runtime access +//! live in focused child modules. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` for `new Reflection*`. +//! - `crate::interpreter::statements` for Reflection method dispatch. +//! +//! Key details: +//! - Shared metadata types stay here so every Reflection owner uses one contract. +//! - Generated/AOT targets use focused runtime hooks for supported point lookups. + +mod callable_api; +mod class_api; +mod class_construction; +mod class_lookup; +mod class_member_api; +mod constant_construction; +mod flags; +mod formatting; +mod function_construction; +mod function_metadata; +mod invocation; +mod member_api; +mod member_construction; +mod member_metadata; +mod owner_materialization; +mod parameter_construction; +mod parameter_metadata; +mod property_access; +mod property_helpers; + +use super::*; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, + NativeCallableObjectDefaultArg, +}; +use crate::eval_ir::EvalSourceLocation; + +pub(in crate::interpreter) use callable_api::*; +pub(in crate::interpreter) use class_api::*; +pub(in crate::interpreter) use class_construction::*; +use class_lookup::*; +pub(in crate::interpreter) use class_member_api::*; +use constant_construction::*; +use flags::*; +use formatting::*; +use function_construction::*; +use function_metadata::*; +use invocation::*; +pub(in crate::interpreter) use member_api::*; +pub(in crate::interpreter) use member_construction::*; +use member_metadata::*; +use owner_materialization::*; +use parameter_construction::*; +use parameter_metadata::*; +use property_access::*; +use property_helpers::*; + +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; +const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; +const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; +const EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE: u64 = 64; +const EVAL_REFLECTION_CLASS_FLAG_CLONEABLE: u64 = 128; +const EVAL_REFLECTION_CLASS_FLAG_INTERNAL: u64 = 256; +const EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED: u64 = 512; +const EVAL_REFLECTION_CLASS_FLAG_ITERABLE: u64 = 1024; +const EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS: u64 = 2048; +const EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT: u64 = 40; +const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; +const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS: u64 = 1; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION: u64 = 2; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD: u64 = 4; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY: u64 = 8; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT: u64 = 16; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; +const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; +const EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED: u64 = 16384; +const EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT: u64 = 40; +const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; +const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; +const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; +const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; +const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; +const EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE: u64 = 256; +const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; +const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; +const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; + +/// Exception category and message for failed ReflectionClass instantiation. +pub(in crate::interpreter) enum EvalReflectionInstantiationError { + ThrowableError(String), + ReflectionException(String), +} + +/// Eval metadata needed to materialize one `ReflectionClass` owner object. +struct EvalReflectionClassMetadata { + resolved_name: String, + source_location: Option, + attributes: Vec, + flags: u64, + modifiers: u64, + interface_names: Vec, + trait_names: Vec, + method_names: Vec, + property_names: Vec, + parent_class_name: Option, +} + +/// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. +struct EvalReflectionMemberMetadata { + declaring_class_name: Option, + source_file: Option, + source_location: Option, + attributes: Vec, + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_promoted: bool, + is_dynamic: bool, + modifiers: u64, + type_metadata: Option, + settable_type_metadata: Option, + return_type_metadata: Option, + default_value: Option, + default_value_trait_origin: Option, + required_parameter_count: usize, + parameters: Vec, +} + +/// Eval metadata needed to materialize one `ReflectionParameter` object. +struct EvalReflectionParameterMetadata { + name: String, + declaring_class_name: Option, + declaring_function: Option, + attributes: Vec, + position: usize, + is_optional: bool, + is_variadic: bool, + is_passed_by_reference: bool, + is_promoted: bool, + has_type: bool, + allows_null: bool, + is_array_type: bool, + is_callable_type: bool, + type_metadata: Option, + default_value: Option, + default_value_constant_name: Option, +} + +/// PHP-visible magic constant scope for one reflected parameter default. +#[derive(Clone)] +struct EvalReflectionParameterMagicScope { + function_name: String, + method_name: String, + class_name: Option, + trait_name: Option, +} + +/// Eval metadata needed for `ReflectionParameter::getDeclaringFunction()`. +#[derive(Clone)] +struct EvalReflectionDeclaringFunctionMetadata { + name: String, + declaring_class_name: Option, + magic_scope: Option, + attributes: Vec, + flags: u64, + required_parameter_count: usize, +} + +/// Eval metadata needed to materialize one parameter `ReflectionType` object. +#[derive(Clone)] +struct EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind, +} + +/// Eval reflection parameter type object variants. +#[derive(Clone)] +enum EvalReflectionParameterTypeKind { + Named(EvalReflectionNamedTypeMetadata), + Union(EvalReflectionUnionTypeMetadata), + Intersection(EvalReflectionIntersectionTypeMetadata), +} + +/// Property hook kind accepted by `ReflectionProperty` hook APIs. +#[derive(Clone, Copy)] +enum EvalReflectionPropertyHook { + Get, + Set, +} + +/// Constructor selector accepted by `ReflectionParameter`. +#[derive(Clone)] +enum EvalReflectionParameterSelector { + Name(String), + Position(i64), +} + +impl EvalReflectionPropertyHook { + /// Returns the associative-array key PHP uses for this hook kind. + const fn key(self) -> &'static str { + match self { + Self::Get => "get", + Self::Set => "set", + } + } + + /// Returns the PHP-visible synthetic hook method name. + fn reflected_method_name(self, property_name: &str) -> String { + format!("${}::{}", property_name, self.key()) + } + + /// Returns the internal eval method name that stores the hook body. + fn synthetic_method_name(self, property_name: &str) -> String { + match self { + Self::Get => property_hook_get_method(property_name), + Self::Set => property_hook_set_method(property_name), + } + } +} + +/// Eval metadata needed to materialize one `ReflectionNamedType` object. +#[derive(Clone)] +struct EvalReflectionNamedTypeMetadata { + name: String, + allows_null: bool, + is_builtin: bool, +} + +/// Registered ReflectionFunctionAbstract target metadata for simple method dispatch. +enum EvalReflectionFunctionMethodTarget { + Function { + name: String, + static_key: Option, + static_variables: Vec, + closure_captures: Vec, + parameters: Vec, + source_location: Option, + closure_target: Option, + is_variadic: bool, + is_static: bool, + is_closure: bool, + is_deprecated: bool, + return_type_metadata: Option, + }, + Method { + declaring_class: Option, + name: String, + static_key: Option, + static_variables: Vec, + source_file: Option, + parameters: Vec, + source_location: Option, + visibility: Option, + is_variadic: bool, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_deprecated: bool, + return_type_metadata: Option, + }, +} + +/// Eval metadata needed to materialize one `ReflectionUnionType` object. +#[derive(Clone)] +struct EvalReflectionUnionTypeMetadata { + types: Vec, + allows_null: bool, +} + +/// Eval metadata needed to materialize one `ReflectionIntersectionType` object. +#[derive(Clone)] +struct EvalReflectionIntersectionTypeMetadata { + types: Vec, +} + +/// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. +pub(in crate::interpreter) fn eval_reflection_owner_new_object( + class_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match reflection_owner_kind(class_name) { + Some(EVAL_REFLECTION_OWNER_CLASS) => { + eval_reflection_class_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_OBJECT) => { + eval_reflection_object_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_ENUM) => { + eval_reflection_enum_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_FUNCTION) => { + eval_reflection_function_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_METHOD) => { + eval_reflection_method_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_PROPERTY) => { + eval_reflection_property_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_PARAMETER) => { + eval_reflection_parameter_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT) => { + eval_reflection_class_constant_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE) => eval_reflection_enum_case_new( + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE, + evaluated_args, + context, + values, + ), + Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE) => eval_reflection_enum_case_new( + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE, + evaluated_args, + context, + values, + ), + Some(_) => Err(EvalStatus::RuntimeFatal), + None => Ok(None), + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/callable_api.rs b/crates/elephc-magician/src/interpreter/reflection/callable_api.rs new file mode 100644 index 0000000000..742385ba17 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/callable_api.rs @@ -0,0 +1,610 @@ +//! Purpose: +//! Implements callable, parameter, and type Reflection method dispatch. +//! +//! Called from: +//! - `crate::interpreter::statements` for ReflectionFunctionAbstract owners. +//! +//! Key details: +//! - Invocation, metadata predicates, prototypes, and PHP string forms share one target lookup. + +use super::*; + +/// Handles eval-backed `ReflectionMethod::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_method_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = context + .eval_reflection_method(identity) + .map(|(declaring_class, method)| (declaring_class.to_string(), method.to_string())) + else { + return Ok(None); + }; + let (object, method_args) = if is_invoke { + eval_reflection_method_invoke_args(evaluated_args)? + } else { + eval_reflection_method_invoke_args_array(evaluated_args, context, values)? + }; + eval_reflection_method_invoke_dispatch( + &declaring_class, + &reflected_method, + object, + method_args, + context, + values, + ) + .map(Some) +} + +/// Handles eval-backed `ReflectionFunction::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_function_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some(function_name) = context + .eval_reflection_function_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let function_args = if is_invoke { + evaluated_args + .into_iter() + .map(eval_reflection_method_forwarded_value_arg) + .collect() + } else { + eval_reflection_function_invoke_args_array(evaluated_args, context, values)? + }; + eval_reflection_function_invoke_dispatch(&function_name, function_args, context, values) + .map(Some) +} + +/// Handles eval-backed ReflectionFunctionAbstract name/origin metadata calls. +pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_short_name(&target)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_namespace_name(&target)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) + .map(Some) + } + "getfilename" | "getstartline" | "getendline" => { + let (source_file, source_location) = + eval_reflection_function_method_source_location(&target); + eval_reflection_source_location_result( + method_key.as_str(), + source_file, + source_location, + evaluated_args, + context, + values, + ) + } + "isinternal" | "returnsreference" | "isgenerator" | "hastentativereturntype" => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + "isclosure" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) + } + "isdeprecated" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_deprecated(&target)) + .map(Some) + } + "isanonymous" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, + "hasreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_return_type(&target).is_some()) + .map(Some) + } + "isuserdefined" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(true).map(Some) + } + "isvariadic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_variadic(&target)) + .map(Some) + } + "isstatic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_static(&target)) + .map(Some) + } + "isdisabled" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, + "getreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + match eval_reflection_function_method_return_type(&target) { + Some(type_metadata) => { + eval_reflection_type_object_result(type_metadata, values).map(Some) + } + None => values.null().map(Some), + } + } + "gettentativereturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.null().map(Some) + } + "getstaticvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_method_static_variables_result(&target, context, values) + .map(Some) + } + "getclosureusedvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_used_variables_result(&target, values).map(Some) + } + "getclosurethis" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_this_result(&target, values).map(Some) + } + "getclosurescopeclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_scope_class_result(&target, context, values) + .map(Some) + } + "getclosurecalledclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_called_class_result(&target, context, values) + .map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionFunction::__toString()` and `ReflectionMethod::__toString()`. +pub(in crate::interpreter) fn eval_reflection_function_method_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_function_method_to_string(&target); + values.string(&rendered).map(Some) +} + +/// Handles eval-backed `ReflectionParameter::isArray()` and `isCallable()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(expected_type) = eval_reflection_parameter_legacy_type_name(method_name) else { + return Ok(None); + }; + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let type_value = values.method_call(object, "getType", Vec::new())?; + if values.is_null(type_value)? { + return values.bool_value(false).map(Some); + } + if !eval_reflection_object_has_class(type_value, "ReflectionNamedType", values)? { + return values.bool_value(false).map(Some); + } + let name = values.method_call(type_value, "getName", Vec::new())?; + let bytes = values.string_bytes(name)?; + let name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values + .bool_value(name.eq_ignore_ascii_case(expected_type)) + .map(Some) +} + +/// Handles eval-backed `ReflectionParameter::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_parameter_object_to_string(object, values)?; + values.string(&rendered).map(Some) +} + +/// Handles eval-backed `ReflectionType::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_type_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(rendered) = eval_reflection_type_object_to_string(object, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&rendered).map(Some) +} + +/// Formats one ReflectionParameter object through its public metadata methods. +pub(super) fn eval_reflection_parameter_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let position = eval_reflection_no_arg_int_method(object, "getPosition", values)?; + let name = eval_reflection_no_arg_string_method(object, "getName", values)?; + let is_optional = eval_reflection_no_arg_bool_method(object, "isOptional", values)?; + let is_passed_by_reference = + eval_reflection_no_arg_bool_method(object, "isPassedByReference", values)?; + let is_variadic = eval_reflection_no_arg_bool_method(object, "isVariadic", values)?; + let type_value = values.method_call(object, "getType", Vec::new())?; + let type_text = if values.is_null(type_value)? { + None + } else { + eval_reflection_type_object_to_string(type_value, values)? + }; + + let mut signature_parts = Vec::new(); + if let Some(type_text) = type_text { + signature_parts.push(type_text); + } + let mut variable = String::new(); + if is_passed_by_reference { + variable.push('&'); + } + if is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(&name); + signature_parts.push(variable); + let requiredness = if is_optional { "optional" } else { "required" }; + let default = eval_reflection_parameter_object_default_to_string(object, values)? + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + + Ok(format!( + "Parameter #{} [ <{}> {}{} ]", + position, + requiredness, + signature_parts.join(" "), + default + )) +} + +/// Formats a ReflectionParameter default through its public metadata methods. +pub(super) fn eval_reflection_parameter_object_default_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_no_arg_bool_method(object, "isDefaultValueAvailable", values)? { + return Ok(None); + } + if eval_reflection_no_arg_bool_method(object, "isDefaultValueConstant", values)? { + let constant_name = + eval_reflection_no_arg_string_method(object, "getDefaultValueConstantName", values)?; + if !constant_name.is_empty() { + return Ok(Some(constant_name)); + } + } + let default_value = values.method_call(object, "getDefaultValue", Vec::new())?; + eval_reflection_runtime_default_value_to_string(default_value, values).map(Some) +} + +/// Formats one materialized scalar-ish default value for reflection string output. +pub(super) fn eval_reflection_runtime_default_value_to_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_NULL => String::from("NULL"), + EVAL_TAG_BOOL => { + if values.truthy(value)? { + String::from("true") + } else { + String::from("false") + } + } + EVAL_TAG_INT | EVAL_TAG_FLOAT => String::from_utf8_lossy(&values.string_bytes(value)?) + .into_owned(), + EVAL_TAG_STRING => { + let value = String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(); + format!("'{value}'") + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC if values.array_len(value)? == 0 => String::from("[]"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + +/// Calls one no-arg Reflection method and returns its string result. +pub(super) fn eval_reflection_no_arg_string_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_reflection_string_arg(value, values) +} + +/// Calls one no-arg Reflection method and returns its bool result. +pub(super) fn eval_reflection_no_arg_bool_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + +/// Calls one no-arg Reflection method and returns its int result. +pub(super) fn eval_reflection_no_arg_int_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_int_value(value, values) +} + +/// Formats one eval-visible ReflectionType object if the value is a retained type object. +pub(super) fn eval_reflection_type_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_kind = if eval_reflection_object_has_class(object, "ReflectionNamedType", values)? { + Some(("ReflectionNamedType", "")) + } else if eval_reflection_object_has_class(object, "ReflectionUnionType", values)? { + Some(("ReflectionUnionType", "|")) + } else if eval_reflection_object_has_class(object, "ReflectionIntersectionType", values)? { + Some(("ReflectionIntersectionType", "&")) + } else { + None + }; + let Some((class_name, separator)) = type_kind else { + return Ok(None); + }; + let rendered = if class_name == "ReflectionNamedType" { + eval_reflection_named_type_to_string(object, values)? + } else { + eval_reflection_composite_type_to_string(object, separator, values)? + }; + Ok(Some(rendered)) +} + +/// Formats one eval-visible ReflectionNamedType object from its public methods. +pub(super) fn eval_reflection_named_type_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_reflection_type_method_string(object, "getName", values)?; + let allows_null = eval_reflection_type_method_bool(object, "allowsNull", values)?; + if allows_null && name != "mixed" { + Ok(format!("?{name}")) + } else { + Ok(name) + } +} + +/// Formats one eval-visible ReflectionUnionType or ReflectionIntersectionType object. +pub(super) fn eval_reflection_composite_type_to_string( + object: RuntimeCellHandle, + separator: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let types = values.method_call(object, "getTypes", Vec::new())?; + let mut names = Vec::new(); + for position in 0..values.array_len(types)? { + let key = values.array_iter_key(types, position)?; + let member = values.array_get(types, key)?; + names.push(eval_reflection_type_method_string(member, "getName", values)?); + } + if separator == "|" && eval_reflection_type_method_bool(object, "allowsNull", values)? { + names.push(String::from("null")); + } + Ok(names.join(separator)) +} + +/// Calls one no-arg ReflectionType method and returns its string result. +pub(super) fn eval_reflection_type_method_string( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Calls one no-arg ReflectionType method and returns its bool result. +pub(super) fn eval_reflection_type_method_bool( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + +/// Maps a legacy ReflectionParameter predicate method to its target named type. +pub(super) fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("isArray") { + Some("array") + } else if method_name.eq_ignore_ascii_case("isCallable") { + Some("callable") + } else { + None + } +} + +/// Returns whether one runtime object cell has the requested PHP class name. +pub(super) fn eval_reflection_object_has_class( + object: RuntimeCellHandle, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(false); + } + let actual = values.object_class_name(object)?; + let bytes = values.string_bytes(actual); + values.release(actual)?; + let actual = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(actual + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name)) +} + +/// Handles eval-backed `ReflectionMethod::hasPrototype()` and `getPrototype()` calls. +pub(in crate::interpreter) fn eval_reflection_method_prototype_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_has_prototype = method_name.eq_ignore_ascii_case("hasPrototype"); + let is_get_prototype = method_name.eq_ignore_ascii_case("getPrototype"); + if !is_has_prototype && !is_get_prototype { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = + context + .eval_reflection_method(identity) + .map(|(declaring_class, method_name)| { + (declaring_class.to_string(), method_name.to_string()) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let Some((prototype_class, prototype_method)) = eval_reflection_method_prototype_target( + &declaring_class, + &reflected_method, + context, + values, + )? + else { + if is_has_prototype { + return values.bool_value(false).map(Some); + } + return eval_throw_reflection_exception( + &format!( + "Method {}::{} does not have a prototype", + declaring_class, reflected_method + ), + context, + values, + ); + }; + if is_has_prototype { + return values.bool_value(true).map(Some); + } + let Some(metadata) = + eval_reflection_prototype_method_metadata(&prototype_class, &prototype_method, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &prototype_method, + &metadata, + context, + values, + ) + .map(Some) +} + +/// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. +pub(in crate::interpreter) fn eval_reflection_set_accessible_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setAccessible") { + return Ok(None); + } + if context.eval_reflection_method(identity).is_none() + && context.eval_reflection_property(identity).is_none() + { + return Ok(None); + } + let _ = bind_evaluated_function_args(&[String::from("accessible")], evaluated_args)?; + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_api.rs b/crates/elephc-magician/src/interpreter/reflection/class_api.rs new file mode 100644 index 0000000000..4a10aa3f39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_api.rs @@ -0,0 +1,899 @@ +//! Purpose: +//! Implements eval-backed `ReflectionClass` query and static-property APIs. +//! It keeps the public method dispatch layer separate from owner construction. +//! +//! Called from: +//! - `crate::interpreter::statements` while dispatching Reflection methods. +//! +//! Key details: +//! - Queries combine eval declarations with focused AOT metadata hooks. +//! - PHP-visible errors and missing-member results are preserved at this boundary. + +use super::*; + +/// Handles eval-backed `ReflectionClass::implementsInterface()` calls. +pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("implementsInterface") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("interface")], evaluated_args)?; + let interface_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_interface_exists(&interface_name, context, values)? { + if eval_reflection_non_interface_exists(&interface_name, context, values)? { + return eval_throw_reflection_exception( + &format!("{} is not an interface", interface_name), + context, + values, + ); + } + return eval_throw_reflection_exception( + &format!("Interface \"{}\" does not exist", interface_name), + context, + values, + ); + } + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_implements_interface_name( + &reflected_name, + &interface_name, + context, + values, + )? + } else if eval_runtime_interface_exists(&reflected_name, values)? { + eval_reflection_same_class_like_name(&reflected_name, &interface_name) + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &interface_name, false); + values.release(reflected_class)?; + result? + }; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass::isSubclassOf()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isSubclassOf") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("class")], evaluated_args)?; + let target_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&target_name, context) + && !values.class_exists(&target_name)? + && !eval_runtime_interface_exists(&target_name, values)? + && !values.trait_exists(&target_name)? + && !values.enum_exists(&target_name)? + { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", target_name), + context, + values, + ); + } + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_is_subclass_of_name( + &reflected_name, + &target_name, + context, + values, + )? + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &target_name, true)?; + values.release(reflected_class)?; + result + }; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass::isInstance()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInstance") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + let object = args[0]; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let result = dynamic_object_is_a(object, &reflected_name, false, context, values)? + .map_or_else(|| values.object_is_a(object, &reflected_name, false), Ok)?; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass` source-location metadata calls. +pub(in crate::interpreter) fn eval_reflection_class_source_location_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_key = method_name.to_ascii_lowercase(); + if !matches!( + method_key.as_str(), + "getfilename" | "getstartline" | "getendline" + ) { + return Ok(None); + } + let Some(reflected_name) = context.eval_reflection_class_name(identity) else { + return Ok(None); + }; + let (source_file, source_location) = + if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + (None, metadata.source_location) + } else { + eval_reflection_aot_class_source_metadata(reflected_name, values)? + }; + eval_reflection_source_location_result( + method_key.as_str(), + source_file.as_deref(), + source_location, + evaluated_args, + context, + values, + ) +} + +/// Returns AOT source-file and line metadata for a generated ReflectionClass. +fn eval_reflection_aot_class_source_metadata( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(flags) = values.reflection_class_flags(class_name.trim_start_matches('\\'))? else { + return Ok((None, None)); + }; + let Some(source_location) = eval_reflection_aot_class_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionClass source lines packed into high flag bits. +fn eval_reflection_aot_class_source_location_from_flags(flags: u64) -> Option { + let start_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + let end_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + +/// Handles eval-backed `ReflectionClass` scalar metadata methods. +pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&metadata.resolved_name).map(Some) + } + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_short_name(&metadata.resolved_name)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_namespace_name(&metadata.resolved_name)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_namespace_name(&metadata.resolved_name).is_empty()) + .map(Some) + } + "getinterfacenames" => { + eval_reflection_bind_no_args(evaluated_args)?; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + eval_reflection_string_array_result(&interface_names, values).map(Some) + } + "gettraitnames" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_string_array_result(&metadata.trait_names, values).map(Some) + } + "getparentclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_related_class_result( + EVAL_REFLECTION_OWNER_CLASS, + metadata.parent_class_name.as_deref(), + true, + context, + values, + ) + .map(Some) + } + "getconstructor" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_constructor_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + true, + context, + values, + ) + .map(Some) + } + "getmodifiers" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.int(metadata.modifiers as i64).map(Some) + } + "isfinal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_FINAL, + evaluated_args, + values, + ), + "isabstract" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ABSTRACT, + evaluated_args, + values, + ), + "isinterface" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + evaluated_args, + values, + ), + "istrait" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_TRAIT, + evaluated_args, + values, + ), + "isenum" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ENUM, + evaluated_args, + values, + ), + "isreadonly" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_READONLY, + evaluated_args, + values, + ), + "isanonymous" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS, + evaluated_args, + values, + ), + "isinstantiable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, + evaluated_args, + values, + ), + "iscloneable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_CLONEABLE, + evaluated_args, + values, + ), + "isiterable" | "isiterateable" => { + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_class_flag_result( + flags, + EVAL_REFLECTION_CLASS_FLAG_ITERABLE, + evaluated_args, + values, + ) + } + "isinternal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERNAL, + evaluated_args, + values, + ), + "isuserdefined" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + evaluated_args, + values, + ), + _ => Ok(None), + } +} + +/// Handles `ReflectionClass::__toString()` calls for eval-visible class metadata. +pub(in crate::interpreter) fn eval_reflection_class_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_class_to_string(&reflected_name, context, values)?; + values.string(&rendered).map(Some) +} + +/// Returns one boolean ReflectionClass flag after validating a no-arg call. +fn eval_reflection_class_flag_result( + flags: u64, + flag: u64, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(flags & flag != 0).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasMethod()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_method_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasMethod") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .method_names + .iter() + .any(|name| name.eq_ignore_ascii_case(&requested_name)) + } else { + eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? + .is_some() + }; + values.bool_value(exists).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasProperty()` and inherited `ReflectionObject` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_property_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasProperty") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let mut exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .property_names + .iter() + .any(|name| name == &property_name) + } else { + eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &property_name, + context, + values, + )? + .is_some() + || eval_reflection_native_interface_property_requirement( + &reflected_name, + &property_name, + context, + ) + .is_some() + }; + if !exists { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let dynamic_exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &property_name, + values, + ); + values.release(dynamic_object)?; + exists = dynamic_exists?; + } + } + values.bool_value(exists).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + let constant_names = eval_reflection_constant_names(&reflected_name, context, values)?; + values + .bool_value(constant_names.iter().any(|name| name == &constant_name)) + .map(Some) +} + +/// Handles eval-backed `ReflectionEnum` methods that are not inherited from `ReflectionClass`. +pub(in crate::interpreter) fn eval_reflection_enum_methods_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(object, "ReflectionEnum", values)? { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(enum_name) = context.resolve_enum_name(&reflected_name) else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hascase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = context + .enum_decl(&enum_name) + .and_then(|enum_decl| enum_decl.case(&requested_name)) + .is_some(); + values.bool_value(exists).map(Some) + } + "getcase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let owner_kind = eval_reflection_enum_case_owner_kind(&enum_name, context)?; + let result = eval_reflection_enum_case_object_result( + owner_kind, + &enum_name, + &requested_name, + context, + values, + )?; + Ok(Some(result)) + } + "getcases" => { + eval_reflection_bind_no_args(evaluated_args)?; + let (case_names, owner_kind) = { + let enum_decl = context.enum_decl(&enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + (case_names, eval_reflection_enum_case_owner_kind(&enum_name, context)?) + }; + let mut result = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let case_object = eval_reflection_enum_case_object_result( + owner_kind, &enum_name, case_name, context, values, + )?; + let key = values.int(index as i64)?; + result = values.array_set(result, key, case_object)?; + } + Ok(Some(result)) + } + "isbacked" => { + eval_reflection_bind_no_args(evaluated_args)?; + let is_backed = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type) + .is_some(); + values.bool_value(is_backed).map(Some) + } + "getbackingtype" => { + eval_reflection_bind_no_args(evaluated_args)?; + let backing_type = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type); + let Some(backing_type) = backing_type else { + return values.null().map(Some); + }; + let metadata = eval_reflection_enum_backing_type_metadata(backing_type); + eval_reflection_type_object_result(&metadata, values).map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionClass::getInterfaces()` and `getTraits()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let relation_kind = if method_name.eq_ignore_ascii_case("getInterfaces") { + "interfaces" + } else if method_name.eq_ignore_ascii_case("getTraits") { + "traits" + } else { + return Ok(None); + }; + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + if relation_kind == "interfaces" { + eval_reflection_eval_metadata_interface_names(&metadata, context, values)? + } else { + metadata.trait_names + } + } else if relation_kind == "interfaces" { + eval_reflection_aot_class_interface_names(&reflected_name, values)? + } else { + eval_reflection_aot_class_trait_names(&reflected_name, values)? + }; + eval_reflection_class_object_map_result(&names, context, values).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getTraitAliases()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getTraitAliases") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let aliases = if context.trait_decl(&reflected_name).is_some() { + context.trait_trait_aliases(&reflected_name) + } else if eval_reflection_class_like_exists(&reflected_name, context) { + context.class_trait_aliases(&reflected_name) + } else { + eval_reflection_aot_class_trait_aliases(&reflected_name, values)? + }; + eval_reflection_string_assoc_result(aliases, values).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + if let Some(value) = + eval_reflection_constant_value(&reflected_name, &constant_name, context, values)? + { + return Ok(Some(value)); + } + values.bool_value(false).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstants") { + return Ok(None); + } + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(names.len())?; + for name in names { + if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context, values)? + { + continue; + } + let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context, values)? + else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getDefaultProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getDefaultProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_default_property_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(member) = + eval_reflection_default_property_metadata(&reflected_name, &name, context, values)? + else { + continue; + }; + let Some(default) = member.default_value.as_ref() else { + continue; + }; + let key = values.string(&name)?; + let value = eval_reflection_member_default_value(&member, default, context, values)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getStaticProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_static_property_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(value) = + eval_reflection_static_property_value(&reflected_name, &name, context, values)? + else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let (property_name, default_value) = + eval_reflection_static_property_value_args(evaluated_args)?; + let property_name = eval_reflection_string_arg(property_name, values)?; + if let Some(value) = + eval_reflection_static_property_value(&reflected_name, &property_name, context, values)? + { + return Ok(Some(value)); + } + if let Some(default_value) = default_value { + return Ok(Some(default_value)); + } + eval_throw_reflection_exception( + &format!( + "Property {}::${} does not exist", + reflected_name, property_name + ), + context, + values, + ) +} + +/// Handles eval-backed `ReflectionClass::setStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args( + &[String::from("name"), String::from("value")], + evaluated_args, + )?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let Some(member) = + eval_reflection_static_property_metadata(&reflected_name, &property_name, context, values)? + else { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + }; + if !member.is_static { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + } + if eval_reflection_class_like_exists(&reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, args[1]) + { + values.release(replaced)?; + } + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_set(&reflected_name, &property_name, args[1]) + })?; + if updated { + return values.null().map(Some); + } + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + } + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_construction.rs b/crates/elephc-magician/src/interpreter/reflection/class_construction.rs new file mode 100644 index 0000000000..8c61840608 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_construction.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Constructs Reflection owners for eval and AOT class-like targets. +//! It also centralizes instantiability and constructor visibility metadata. +//! +//! Called from: +//! - `crate::interpreter::reflection` for ReflectionClass/Object/Enum construction. +//! +//! Key details: +//! - Class-like flags merge eval declarations with focused AOT runtime metadata. + +use super::*; + +/// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. +pub(super) fn eval_reflection_class_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &reflected_name, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionObject` from an object instance. +pub(super) fn eval_reflection_object_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + let tag = values.type_tag(args[0])?; + if tag != EVAL_TAG_OBJECT { + return super::class_lookup::eval_throw_type_error( + &format!( + "ReflectionObject::__construct(): Argument #1 ($object) must be of type object, {} given", + eval_reflection_type_error_type_name(tag) + ), + context, + values, + ); + } + let reflected_name = eval_reflection_object_class_name(args[0], context, values)?; + let Some(object) = eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_OBJECT, + &reflected_name, + context, + values, + )? + else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_set(object, "__object", args[0]) + })?; + Ok(Some(object)) +} + +/// Materializes class metadata for `ReflectionClass` or `ReflectionObject`. +pub(super) fn eval_reflection_class_owner_object_result( + owner_kind: u64, + reflected_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) else { + if reflected_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + owner_kind, context, values, + ) + .map(Some); + } + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(reflected_name, values)? + else { + return Ok(None); + }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + values, + )?; + let interface_names = eval_reflection_aot_class_interface_names(reflected_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(reflected_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(reflected_name, values)?; + let attributes = context.native_class_attributes(reflected_name); + return eval_reflection_owner_object( + owner_kind, + reflected_name, + &attributes, + &interface_names, + &trait_names, + &method_names, + &property_names, + parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ) + .map(Some); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object( + owner_kind, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) + .map(Some) +} + +/// Builds the minimal ReflectionClass metadata object for PHP's builtin Closure class. +pub(super) fn eval_reflection_builtin_closure_class_object_result( + owner_kind: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + let modifiers = eval_reflection_class_modifiers(true, false, false, false); + eval_reflection_owner_object( + owner_kind, + "Closure", + &[], + &[], + &[], + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionEnum` object for a declared enum. +pub(super) fn eval_reflection_enum_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let reflected_name = context + .resolve_enum_name(&class_name) + .or_else(|| context.resolve_class_like_name(&class_name)) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if context.enum_decl(&reflected_name).is_some() { + return eval_reflection_enum_object_result(&reflected_name, context, values).map(Some); + } + if eval_reflection_class_like_exists(&reflected_name, context) { + return eval_throw_reflection_exception( + &format!("Class \"{}\" is not an enum", reflected_name), + context, + values, + ); + } + if eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return Ok(None); + } + eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ) +} + +/// Materializes one eval-backed `ReflectionEnum` owner object. +pub(super) fn eval_reflection_enum_object_result( + enum_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let reflected_name = context + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.has_enum(&metadata.resolved_name) { + return Err(EvalStatus::RuntimeFatal); + } + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_ENUM, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + metadata.flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Resolves a ReflectionClass constructor target from a class-name string or object. +pub(super) fn eval_reflection_class_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + return eval_reflection_object_class_name(target, context, values); + } + eval_reflection_string_arg(target, values) +} + +/// Returns generated/AOT class flags for synthetic ReflectionClass fallback objects. +pub(super) fn eval_reflection_aot_class_flags( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let is_class = values.class_exists(runtime_class_name)?; + let is_interface = eval_runtime_interface_exists(runtime_class_name, values)?; + let is_trait = values.trait_exists(runtime_class_name)?; + let is_enum = values.enum_exists(runtime_class_name)?; + if !(is_class || is_interface || is_trait || is_enum) { + return Ok(None); + } + let mut flags = 0; + if eval_reflection_class_like_is_internal(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + } else { + flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + } + let mut class_flags = values.reflection_class_flags(runtime_class_name)?.unwrap_or(0); + if is_enum { + class_flags &= !EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + flags |= class_flags + & (EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ABSTRACT + | EVAL_REFLECTION_CLASS_FLAG_READONLY); + if is_interface { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; + } + if is_trait { + flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + if eval_reflection_builtin_class_is_iterable(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + if is_class && !is_enum && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 { + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__construct", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__clone", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } + } + let modifiers = eval_reflection_class_modifiers( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + is_enum, + ); + Ok(Some((flags, modifiers))) +} + +/// Returns AOT class modifiers relevant to validating an eval `extends` clause. +pub(in crate::interpreter) fn eval_reflection_aot_class_inheritance_modifiers( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + if flags + & (EVAL_REFLECTION_CLASS_FLAG_INTERFACE + | EVAL_REFLECTION_CLASS_FLAG_TRAIT + | EVAL_REFLECTION_CLASS_FLAG_ENUM) + != 0 + { + return Ok(None); + } + Ok(Some(( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + ))) +} + +/// Returns the catchable error for generated/AOT allocation without constructor, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_without_constructor_error( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + Ok(eval_reflection_non_instantiable_error_message( + class_name, flags, + )) +} + +/// Returns the catchable error for generated/AOT public ReflectionClass construction, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_public_instantiation_error( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + if let Some(message) = eval_reflection_non_instantiable_error_message(class_name, flags) { + return Ok(Some(EvalReflectionInstantiationError::ThrowableError(message))); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == 0 { + return Ok(Some( + EvalReflectionInstantiationError::ReflectionException(format!( + "Access to non-public constructor of class {}", + class_name.trim_start_matches('\\') + )), + )); + } + Ok(None) +} + +/// Builds PHP's non-instantiable class-like message from ReflectionClass flags. +pub(super) fn eval_reflection_non_instantiable_error_message(class_name: &str, flags: u64) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + return Some(format!("Cannot instantiate abstract class {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + return Some(format!("Cannot instantiate interface {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + return Some(format!("Cannot instantiate trait {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_ENUM != 0 { + return Some(format!("Cannot instantiate enum {}", class_name)); + } + None +} + +/// Returns whether an absent or public AOT lifecycle method allows public reflection. +pub(super) fn eval_reflection_aot_lifecycle_method_allows_public_reflection( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(true); + }; + Ok(flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 + && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0) +} + +/// Returns AOT constructor access metadata when the constructor is not public. +pub(in crate::interpreter) fn eval_reflection_aot_non_public_constructor( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, "__construct")? else { + return Ok(None); + }; + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(runtime_class_name, "__construct")? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some((declaring_class, visibility))) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs b/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs new file mode 100644 index 0000000000..c48ed1ba4f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs @@ -0,0 +1,814 @@ +//! Purpose: +//! Resolves class-like relations, members, attributes, flags, and lookup errors. +//! +//! Called from: +//! - Reflection class APIs, constructors, formatters, and member metadata builders. +//! +//! Key details: +//! - Eval declarations, aliases, interfaces, traits, and AOT metadata use case-insensitive lookup. + +use super::*; + +/// Returns true when a ReflectionClass member passes an optional modifier filter. +pub(super) fn eval_reflection_member_matches_filter( + member: &EvalReflectionMemberMetadata, + filter: Option, +) -> bool { + match filter { + Some(filter) => member.modifiers & filter != 0, + None => true, + } +} + +/// Parses the optional ReflectionClass member filter argument. +pub(super) fn eval_reflection_member_filter( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut filter = None; + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + if name != "filter" { + return Err(EvalStatus::RuntimeFatal); + } + } + if filter.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + filter = Some(arg.value); + } + let Some(filter) = filter else { + return Ok(None); + }; + if values.is_null(filter)? { + return Ok(None); + } + let cast_filter = values.cast_int(filter)?; + let bytes = values.string_bytes(cast_filter)?; + values.release(cast_filter)?; + let text = std::str::from_utf8(&bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + text.parse::() + .map(|value| Some(value as u64)) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns generated AOT member names for one reflected class. +pub(super) fn eval_reflection_aot_member_names( + owner_kind: u64, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + values.reflection_method_names(runtime_class_name)? + } else { + values.reflection_property_names(runtime_class_name)? + }; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns generated AOT interface names for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_interface_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns eval metadata interface names expanded with generated/AOT ancestors. +pub(super) fn eval_reflection_eval_metadata_interface_names( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(&metadata.resolved_name) || context.has_enum(&metadata.resolved_name) { + eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values) + } else if context.has_interface(&metadata.resolved_name) { + eval_reflection_eval_interface_parent_names(&metadata.resolved_name, context, values) + } else { + Ok(metadata.interface_names.clone()) + } +} + +/// Returns eval metadata flags corrected for generated/AOT inherited interfaces. +pub(super) fn eval_reflection_eval_metadata_flags( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut flags = metadata.flags; + if flags & EVAL_REFLECTION_CLASS_FLAG_ITERABLE == 0 + && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 + && context.has_class(&metadata.resolved_name) + && eval_reflection_interface_names_include_iterable( + &eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values)?, + ) + { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + Ok(flags) +} + +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +pub(super) fn eval_reflection_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_reflection_aot_class_interface_names(&parent, values)? { + eval_reflection_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +pub(super) fn eval_reflection_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns whether one interface list includes PHP iterable marker interfaces. +pub(super) fn eval_reflection_interface_names_include_iterable(interface_names: &[String]) -> bool { + interface_names.iter().any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +pub(super) fn eval_reflection_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns generated AOT trait names for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_trait_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_trait_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns generated AOT trait aliases for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_trait_aliases( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let alias_names_array = values.reflection_class_trait_alias_names(runtime_class_name)?; + let alias_names = eval_reflection_string_array_to_vec(alias_names_array, values)?; + values.release(alias_names_array)?; + let alias_sources_array = values.reflection_class_trait_alias_sources(runtime_class_name)?; + let alias_sources = eval_reflection_string_array_to_vec(alias_sources_array, values)?; + values.release(alias_sources_array)?; + if alias_names.len() != alias_sources.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(alias_names.into_iter().zip(alias_sources).collect()) +} + +/// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. +pub(super) fn eval_reflection_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_reflection_string_arg(value, values)?); + } + Ok(result) +} + +/// Returns member metadata for one ReflectionClass member-array entry. +pub(super) fn eval_reflection_member_metadata( + owner_kind: u64, + class_name: &str, + name: &str, + context: &ElephcEvalContext, +) -> Option { + match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => eval_reflection_method_metadata(class_name, name, context), + EVAL_REFLECTION_OWNER_PROPERTY => { + eval_reflection_property_metadata(class_name, name, context) + } + _ => None, + } +} + +/// Returns the eval-retained class-like attributes plus canonical reflected name. +pub(super) fn eval_reflection_class_like_attributes( + name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(class) = context.class(name) { + let is_enum = context.has_enum(class.name()); + let mut flags = EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + if class.is_final() { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; + } + if class.is_abstract() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + if class.is_readonly_class() && !is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + if eval_reflection_class_is_instantiable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } + if eval_reflection_class_is_cloneable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } + if eval_reflection_class_is_iterable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + if class.is_anonymous() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS; + } + let modifiers = eval_reflection_class_modifiers( + class.is_final(), + class.is_abstract(), + class.is_readonly_class(), + is_enum, + ); + return Some(EvalReflectionClassMetadata { + resolved_name: class.name().trim_start_matches('\\').to_string(), + source_location: class.source_location(), + attributes: class.attributes().to_vec(), + interface_names: context.class_interface_names(class.name()), + trait_names: context.class_trait_names(class.name()), + method_names: context.class_method_names(class.name()), + property_names: context.class_property_names(class.name()), + parent_class_name: eval_reflection_parent_class_name(class, context), + flags, + modifiers, + }); + } + if let Some(interface) = context.interface(name) { + return Some(EvalReflectionClassMetadata { + resolved_name: interface.name().trim_start_matches('\\').to_string(), + source_location: interface.source_location(), + attributes: interface.attributes().to_vec(), + interface_names: context.interface_parent_names(interface.name()), + trait_names: Vec::new(), + method_names: context.interface_method_names(interface.name()), + property_names: context.interface_property_names(interface.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 0, + }); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(EvalReflectionClassMetadata { + resolved_name: trait_decl.name().trim_start_matches('\\').to_string(), + source_location: trait_decl.source_location(), + attributes: trait_decl.attributes().to_vec(), + interface_names: Vec::new(), + trait_names: context.trait_trait_names(trait_decl.name()), + method_names: context.trait_method_names(trait_decl.name()), + property_names: context.trait_property_names(trait_decl.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 0, + }); + } + context + .enum_decl(name) + .map(|enum_decl| EvalReflectionClassMetadata { + resolved_name: enum_decl.name().trim_start_matches('\\').to_string(), + source_location: enum_decl.source_location(), + attributes: enum_decl.attributes().to_vec(), + interface_names: context.class_interface_names(enum_decl.name()), + trait_names: context.class_trait_names(enum_decl.name()), + method_names: context.class_method_names(enum_decl.name()), + property_names: context.class_property_names(enum_decl.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ENUM + | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 32, + }) +} + +/// Returns the PHP-visible parent class name for ReflectionClass metadata. +pub(super) fn eval_reflection_parent_class_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + context.class_parent_names(class.name()).into_iter().next() +} + +/// Returns PHP's `ReflectionClass::isInstantiable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_instantiable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__construct") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + +/// Returns PHP's `ReflectionClass::isCloneable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_cloneable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__clone") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_iterable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_interface_names(class.name()) + .iter() + .any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for compiler-injected class names. +pub(super) fn eval_reflection_builtin_class_is_iterable(class_name: &str) -> bool { + matches!( + class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str(), + "__elephcappenditeratorarrayiterator" + | "appenditerator" + | "arrayiterator" + | "arrayobject" + | "cachingiterator" + | "callbackfilteriterator" + | "directoryiterator" + | "emptyiterator" + | "filesystemiterator" + | "generator" + | "globiterator" + | "infiniteiterator" + | "internaliterator" + | "iteratoriterator" + | "limititerator" + | "multipleiterator" + | "norewinditerator" + | "parentiterator" + | "recursivearrayiterator" + | "recursivecachingiterator" + | "recursivecallbackfilteriterator" + | "recursivedirectoryiterator" + | "recursiveiteratoriterator" + | "recursiveregexiterator" + | "regexiterator" + | "spldoublylinkedlist" + | "splfixedarray" + | "splfileobject" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "splqueue" + | "splstack" + | "spltempfileobject" + ) +} + +/// Returns whether one reflected class-like name belongs to compiler-injected metadata. +pub(super) fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { + let trimmed = class_name.trim_start_matches('\\'); + if EVAL_SPL_CLASS_NAMES + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(trimmed)) + { + return true; + } + matches!( + trimmed.to_ascii_lowercase().as_str(), + "__elephcappenditeratorarrayiterator" + | "fiber" + | "fibererror" + | "generator" + | "internaliterator" + | "jsonexception" + | "phar" + | "phardata" + | "pharfileinfo" + | "php_user_filter" + | "reflectionattribute" + | "reflectionclass" + | "reflectionclassconstant" + | "reflectionenum" + | "reflectionenumbackedcase" + | "reflectionenumunitcase" + | "reflectionexception" + | "reflectionfunction" + | "reflectionintersectiontype" + | "reflectionmethod" + | "reflectionnamedtype" + | "reflectionparameter" + | "reflectionproperty" + | "reflectionuniontype" + | "sortdirection" + | "splheap" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "stdclass" + ) +} + +/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_class_modifiers( + is_final: bool, + is_abstract: bool, + is_readonly_class: bool, + is_enum: bool, +) -> u64 { + let mut modifiers = 0; + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly_class && !is_enum { + modifiers |= 65_536; + } + modifiers +} + +/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final: bool) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_final { + modifiers |= 32; + } + modifiers +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_method_modifiers( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_property_modifiers( + visibility: EvalVisibility, + set_visibility: Option, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(EvalVisibility::Private) => modifiers |= 32 | 4096, + Some(EvalVisibility::Protected) => modifiers |= 2048, + _ if is_readonly && visibility == EvalVisibility::Public => modifiers |= 2048, + _ => {} + } + modifiers +} + +/// Returns declaring class, attributes, visibility, finality, and enum-case kind. +pub(super) fn eval_reflection_class_constant_metadata( + class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalVisibility, bool, bool)>, EvalStatus> { + if let Some(enum_decl) = context.enum_decl(class_name) { + if let Some(case) = enum_decl.case(constant_name) { + return Ok(Some(( + enum_decl.name().to_string(), + case.attributes().to_vec(), + EvalVisibility::Public, + false, + true, + ))); + } + } + if let Some(metadata) = context + .class_constant(class_name, constant_name) + .map(|(declaring_class, constant)| { + ( + declaring_class, + constant.attributes().to_vec(), + constant.visibility(), + constant.is_final(), + false, + ) + }) { + return Ok(Some(metadata)); + } + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(runtime_class_name, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(runtime_class_name, constant_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let attributes = eval_reflection_aot_constant_attributes( + runtime_class_name, + &declaring_class, + constant_name, + context, + ); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some(( + declaring_class, + attributes, + visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE != 0, + ))) +} + +/// Returns registered generated/AOT class-constant attributes for one reflected constant. +pub(super) fn eval_reflection_aot_constant_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_constant_attributes(declaring_class_name, constant_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_constant_attributes(runtime_class_name, constant_name) +} + +/// Returns true when a name resolves to an eval-declared class-like symbol. +pub(super) fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { + context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) +} + +/// Returns true when a name resolves to eval or runtime class-like metadata. +pub(super) fn eval_reflection_class_like_or_runtime_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_reflection_class_like_exists(name, context) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + || values.enum_exists(name)?) +} + +/// Returns true when one name exists as an eval or runtime interface. +pub(super) fn eval_reflection_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) +} + +/// Returns true when one name exists as a non-interface class-like symbol. +pub(super) fn eval_reflection_non_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || values.trait_exists(name)? + { + return Ok(true); + } + values.enum_exists(name) +} + +/// Returns true when reflected eval metadata implements or extends an interface name. +pub(super) fn eval_reflection_class_implements_interface_name( + reflected_name: &str, + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_interface(reflected_name) { + if eval_reflection_same_class_like_name(reflected_name, interface_name) { + return Ok(true); + } + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, interface_name))); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, interface_name))); + } + Ok(false) +} + +/// Returns true when reflected eval metadata is a subclass or subinterface of a target. +pub(super) fn eval_reflection_class_is_subclass_of_name( + reflected_name: &str, + target_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_interface(reflected_name) { + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, target_name))); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + if context.class_is_a(reflected_name, target_name, true) { + return Ok(true); + } + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, target_name))); + } + Ok(false) +} + +/// Returns true when two PHP class-like names match case-insensitively. +pub(super) fn eval_reflection_same_class_like_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Creates a catchable `ReflectionException` and propagates it through eval throw state. +pub(super) fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates a catchable `ValueError` and propagates it through eval throw state. +pub(super) fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates a catchable `TypeError` and propagates it through eval throw state. +pub(super) fn eval_throw_type_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("TypeError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Returns PHP's type name spelling used in argument type error messages. +pub(super) fn eval_reflection_type_error_type_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_STRING => "string", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_NULL => "null", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_OBJECT => "object", + _ => "unknown", + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs b/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs new file mode 100644 index 0000000000..9106a29e0b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs @@ -0,0 +1,435 @@ +//! Purpose: +//! Implements ReflectionClass member and reflected-constant collection APIs. +//! +//! Called from: +//! - `crate::interpreter::statements` for ReflectionClass member dispatch. +//! +//! Key details: +//! - Eval and AOT members share filtering, object construction, and missing-member errors. + +use super::*; + +/// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_constant_names(&reflected_name, context, values)? + .iter() + .any(|name| name == &requested_name) + { + return values.bool_value(false).map(Some); + } + eval_reflection_class_constant_object_result(&reflected_name, &requested_name, context, values) + .map(Some) +} + +/// Handles eval-backed `ReflectionClass::getReflectionConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstants") { + return Ok(None); + } + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context, values)?; + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in &names { + if !eval_reflection_constant_matches_filter(reflected_name.as_str(), name, filter, context, values)? + { + continue; + } + let object = + eval_reflection_class_constant_object_result(&reflected_name, name, context, values)?; + let key = values.int(index)?; + result = values.array_set(result, key, object)?; + index += 1; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getMethods()` and `getProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_members_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethods") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperties") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + return eval_reflection_member_object_array_result( + owner_kind, + &reflected_name, + &names, + filter, + context, + values, + ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) + .map(Some); + } + let native_interface_property_names = + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + eval_reflection_native_interface_property_names(&reflected_name, context) + } else { + Vec::new() + }; + let names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names(owner_kind, &reflected_name, values)? + } else { + native_interface_property_names + }; + eval_reflection_aot_member_object_array_result( + owner_kind, + &reflected_name, + &names, + filter, + context, + values, + ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) + .map(Some) +} + +/// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_member_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethod") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperty") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let Some(member_name) = + eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) + else { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &requested_name, + context, + values, + )? { + let member_name = requested_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &member_name, + &member, + context, + values, + ) + .map(Some); + } + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + &requested_name, + context, + ) + { + let member = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + if let Some(member) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &requested_name, + context, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &requested_name, + values, + ); + values.release(dynamic_object)?; + if exists? { + let member = eval_reflection_dynamic_property_metadata(&reflected_name); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } + } + let message_name = eval_reflection_class_like_attributes(&reflected_name, context) + .map(|metadata| metadata.resolved_name) + .unwrap_or_else(|| reflected_name.clone()); + let message = + eval_reflection_missing_member_message(owner_kind, &message_name, &requested_name); + return eval_throw_reflection_exception(&message, context, values); + }; + let member = + eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_member_object_result(owner_kind, &member_name, &member, context, values) + .map(Some) +} + +/// Returns generated/AOT constant names visible through eval ReflectionClass. +pub(super) fn eval_reflection_aot_constant_names( + reflected_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let names_array = values.reflection_constant_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns constant names from eval metadata or generated/AOT runtime metadata. +pub(super) fn eval_reflection_constant_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_interface(reflected_name) { + Ok(context.interface_constant_names(reflected_name)) + } else if context.has_trait(reflected_name) { + Ok(context.trait_constant_names(reflected_name)) + } else if context.has_class(reflected_name) || context.has_enum(reflected_name) { + Ok(context.class_constant_names(reflected_name)) + } else { + eval_reflection_aot_constant_names(reflected_name, values) + } +} + +/// Returns a materialized eval constant value for Reflection without visibility checks. +pub(super) fn eval_reflection_eval_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(case) = context.enum_case(reflected_name, constant_name) { + return Some(case); + } + let (declaring_class, constant) = context.class_constant(reflected_name, constant_name)?; + context.class_constant_cell(&declaring_class, constant.name()) +} + +/// Returns a materialized eval or AOT constant value for Reflection without visibility checks. +pub(super) fn eval_reflection_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_constant_value( + reflected_name, + constant_name, + context, + )); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + values.reflection_constant_value(runtime_class_name, constant_name) +} + +/// Builds one eval-backed `ReflectionClassConstant` object for a visible constant name. +pub(super) fn eval_reflection_class_constant_object_result( + reflected_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = + eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_value = + eval_reflection_constant_value(reflected_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + constant_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(constant_value), + None, + context, + values, + ) +} + +/// Returns whether one class constant passes an optional `ReflectionClassConstant` filter. +pub(super) fn eval_reflection_constant_matches_filter( + reflected_name: &str, + constant_name: &str, + filter: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(filter) = filter else { + return Ok(true); + }; + Ok(eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .is_some_and(|(_, _, visibility, is_final, _)| { + eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 + })) +} + +/// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. +pub(super) fn eval_reflection_member_name( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, + context: &ElephcEvalContext, +) -> Option { + let metadata = eval_reflection_class_like_attributes(reflected_name, context)?; + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + names.into_iter().find(|name| match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), + EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, + _ => false, + }) +} + +/// Builds PHP-compatible missing-member messages for eval ReflectionClass lookups. +pub(super) fn eval_reflection_missing_member_message( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, +) -> String { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + format!( + "Method {}::{}() does not exist", + reflected_name, requested_name + ) + } else { + format!( + "Property {}::${} does not exist", + reflected_name, requested_name + ) + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs b/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs new file mode 100644 index 0000000000..c7b511efc7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs @@ -0,0 +1,280 @@ +//! Purpose: +//! Constructs reflected class constants and enum cases. +//! +//! Called from: +//! - `crate::interpreter::reflection` for ReflectionClassConstant and enum-case owners. +//! +//! Key details: +//! - Missing constants and non-case targets preserve PHP exception categories. + +use super::*; + +/// Builds an eval-backed `ReflectionClassConstant` object for a class constant or enum case. +pub(super) fn eval_reflection_class_constant_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let class_name = eval_reflection_string_arg(args[0], values)?; + let constant_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_class_constant_object_result_or_throw( + &class_name, + &constant_name, + context, + values, + ) +} + +/// Builds a `ReflectionClassConstant` object or throws PHP's catchable error. +pub(super) fn eval_reflection_class_constant_object_result_or_throw( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class_name, attributes, visibility, is_final, is_enum_case)) = + eval_reflection_class_constant_metadata(class_name, constant_name, context, values)? + else { + return eval_reflection_missing_class_constant_exception( + class_name, + constant_name, + context, + values, + ); + }; + let constant_value = eval_reflection_constant_value(class_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + constant_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(constant_value), + None, + context, + values, + ) + .map(Some) +} + +/// Throws the catchable ReflectionException for a missing class-like constant. +pub(super) fn eval_reflection_missing_class_constant_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!("Constant {}::{} does not exist", reflected_name, constant_name), + context, + values, + ) +} + +/// Builds an eval-backed ReflectionEnumUnitCase/BackedCase object for an enum case. +pub(super) fn eval_reflection_enum_case_new( + owner_kind: u64, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let enum_name = eval_reflection_string_arg(args[0], values)?; + let case_name = eval_reflection_string_arg(args[1], values)?; + let Some(enum_decl) = context.enum_decl(&enum_name) else { + if eval_reflection_class_constant_metadata(&enum_name, &case_name, context, values)? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &enum_name, &case_name, context, values, + ); + } + if !eval_reflection_class_like_exists(&enum_name, context) + && eval_reflection_class_like_or_runtime_exists(&enum_name, context, values)? + { + return Ok(None); + } + return eval_reflection_missing_class_constant_exception( + &enum_name, &case_name, context, values, + ); + }; + let declaring_class_name = enum_decl.name().to_string(); + let has_case = enum_decl.case(&case_name).is_some(); + let is_backed = enum_decl.backing_type().is_some(); + if !has_case { + if eval_reflection_class_constant_metadata( + &declaring_class_name, + &case_name, + context, + values, + )? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + return eval_reflection_missing_class_constant_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return eval_throw_reflection_exception( + &format!("Enum case {}::{} is not a backed case", declaring_class_name, case_name), + context, + values, + ); + } + eval_reflection_enum_case_object_result( + owner_kind, + &declaring_class_name, + &case_name, + context, + values, + ) + .map(Some) +} + +/// Throws the catchable ReflectionException for a constant that is not an enum case. +pub(super) fn eval_reflection_not_enum_case_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + eval_throw_reflection_exception( + &format!("Constant {}::{} is not a case", reflected_name, constant_name), + context, + values, + ) +} + +/// Builds one eval-backed enum-case reflection owner object. +pub(super) fn eval_reflection_enum_case_object_result( + owner_kind: u64, + enum_name: &str, + case_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (declaring_class_name, attributes, is_backed) = { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case = enum_decl.case(case_name).ok_or(EvalStatus::RuntimeFatal)?; + ( + enum_decl.name().to_string(), + case.attributes().to_vec(), + enum_decl.backing_type().is_some(), + ) + }; + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return Err(EvalStatus::RuntimeFatal); + } + let case_value = context + .enum_case(&declaring_class_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let backing_value = if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + Some( + context + .enum_case_value(&declaring_class_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?, + ) + } else { + None + }; + let flags = eval_reflection_member_flags(EvalVisibility::Public, false, false, false, false) + | EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + let modifiers = eval_reflection_class_constant_modifiers(EvalVisibility::Public, false); + eval_reflection_owner_object( + owner_kind, + case_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(case_value), + backing_value, + context, + values, + ) +} + +/// Selects the concrete enum-case reflector class for one enum. +pub(super) fn eval_reflection_enum_case_owner_kind( + enum_name: &str, + context: &ElephcEvalContext, +) -> Result { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + Ok(if enum_decl.backing_type().is_some() { + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + } else { + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + }) +} + +/// Builds `ReflectionNamedType` metadata for an enum backing type. +pub(super) fn eval_reflection_enum_backing_type_metadata( + backing_type: EvalEnumBackingType, +) -> EvalReflectionParameterTypeMetadata { + let name = match backing_type { + EvalEnumBackingType::Int => "int", + EvalEnumBackingType::String => "string", + }; + EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + name, false, + )), + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/flags.rs b/crates/elephc-magician/src/interpreter/reflection/flags.rs new file mode 100644 index 0000000000..9b97c031e9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/flags.rs @@ -0,0 +1,157 @@ +//! Purpose: +//! Packs Reflection runtime flags and parses common constructor arguments. +//! +//! Called from: +//! - Reflection owner factories and metadata materializers. +//! +//! Key details: +//! - Bit layouts mirror the runtime owner factory contract and remain centralized here. + +use super::*; + +/// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. +pub(super) fn eval_reflection_member_flags( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, +) -> u64 { + let mut flags = 0; + if is_static { + flags |= EVAL_REFLECTION_MEMBER_FLAG_STATIC; + } + match visibility { + EvalVisibility::Public => flags |= EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, + EvalVisibility::Protected => flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + EvalVisibility::Private => flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, + } + if is_final { + flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } + if is_abstract { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT; + } + if is_readonly { + flags |= EVAL_REFLECTION_MEMBER_FLAG_READONLY; + } + flags +} + +/// Packs callable-only ReflectionFunctionAbstract predicate flags. +pub(super) fn eval_reflection_callable_flags(attributes: &[EvalAttribute]) -> u64 { + if eval_reflection_attributes_include_deprecated(attributes) { + EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED + } else { + 0 + } +} + +/// Returns whether an attribute list contains PHP's global `#[Deprecated]` marker. +pub(super) fn eval_reflection_attributes_include_deprecated(attributes: &[EvalAttribute]) -> bool { + attributes + .iter() + .any(|attribute| attribute.name().eq_ignore_ascii_case("Deprecated")) +} + +/// Packs ReflectionParameter predicate flags for the runtime parameter factory. +pub(super) fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) -> u64 { + let mut flags = 0; + if parameter.is_optional { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL; + } + if parameter.is_variadic { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC; + } + if parameter.is_passed_by_reference { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_BY_REF; + } + if parameter.is_promoted { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED; + } + if parameter.has_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; + } + if parameter.default_value.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE; + } + if parameter.allows_null { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL; + } + if parameter.default_value_constant_name.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT; + } + if parameter.is_array_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE; + } + if parameter.is_callable_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE; + } + flags +} + +/// Packs ReflectionNamedType predicate flags for the runtime type factory. +pub(super) fn eval_reflection_named_type_flags(type_metadata: &EvalReflectionNamedTypeMetadata) -> u64 { + let mut flags = 0; + if type_metadata.allows_null { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL; + } + if type_metadata.is_builtin { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN; + } + flags +} + +/// Packs ReflectionUnionType predicate flags for the runtime type factory. +pub(super) fn eval_reflection_union_type_flags(type_metadata: &EvalReflectionUnionTypeMetadata) -> u64 { + if type_metadata.allows_null { + EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL + } else { + 0 + } +} + +/// Converts a ReflectionFunction argument into a function or eval-closure name. +pub(super) fn eval_reflection_function_name_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let identity = values.object_identity(value)?; + if let Some(name) = context.closure_object_name(identity) { + return Ok(name.to_string()); + } + } + eval_reflection_string_arg(value, values) +} + +/// Converts one reflection constructor argument to a Rust UTF-8 string. +pub(super) fn eval_reflection_string_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Maps a PHP reflection owner class name to the helper owner kind. +pub(super) fn reflection_owner_kind(class_name: &str) -> Option { + match class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionobject" => Some(EVAL_REFLECTION_OWNER_OBJECT), + "reflectionenum" => Some(EVAL_REFLECTION_OWNER_ENUM), + "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), + "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), + "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), + "reflectionparameter" => Some(EVAL_REFLECTION_OWNER_PARAMETER), + "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), + "reflectionenumunitcase" => Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE), + "reflectionenumbackedcase" => Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/formatting.rs b/crates/elephc-magician/src/interpreter/reflection/formatting.rs new file mode 100644 index 0000000000..b0a93d6153 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/formatting.rs @@ -0,0 +1,565 @@ +//! Purpose: +//! Formats eval-backed Reflection owners using PHP-compatible string layouts. +//! This module owns presentation only; metadata lookup remains in the parent. +//! +//! Called from: +//! - `crate::interpreter::reflection` for Reflection `__toString()` methods. +//! +//! Key details: +//! - Class, callable, property, constant, parameter, and type formatting share +//! the same visibility, modifier, and default-value conventions. + +use super::*; + +/// Formats one reflected class-like symbol similarly to PHP's `__toString()` output. +pub(super) fn eval_reflection_class_to_string( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_class_to_string_metadata(reflected_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_lines = + eval_reflection_class_constant_string_lines(&metadata.resolved_name, context, values)?; + let property_lines = + eval_reflection_class_property_string_lines(&metadata, context, values)?; + let method_lines = eval_reflection_class_method_string_lines(&metadata, context, values)?; + + let mut rendered = format!("{} {{\n", eval_reflection_class_to_string_header(&metadata)); + eval_reflection_class_append_string_section(&mut rendered, "Constants", &constant_lines); + eval_reflection_class_append_string_section(&mut rendered, "Properties", &property_lines); + eval_reflection_class_append_string_section(&mut rendered, "Methods", &method_lines); + rendered.push_str("}\n"); + Ok(rendered) +} + +/// Returns eval or AOT class metadata for a ReflectionClass string dump. +pub(super) fn eval_reflection_class_to_string_metadata( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(mut metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + metadata.interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + metadata.flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + return Ok(Some(metadata)); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(runtime_class_name, values)? + else { + return Ok(None); + }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let native_interface_property_names = + eval_reflection_native_interface_property_names(runtime_class_name, context); + let property_names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )? + } else { + native_interface_property_names + }; + let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + Ok(Some(EvalReflectionClassMetadata { + resolved_name: runtime_class_name.to_string(), + source_location: None, + attributes: context.native_class_attributes(runtime_class_name), + flags, + modifiers, + interface_names, + trait_names, + method_names, + property_names, + parent_class_name, + })) +} + +/// Returns the PHP-like header line for `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_to_string_header(metadata: &EvalReflectionClassMetadata) -> String { + let origin = if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERNAL != 0 { + "" + } else { + "" + }; + let kind = eval_reflection_class_to_string_kind(metadata.flags); + let mut parts = Vec::new(); + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + parts.push(String::from("interface")); + parts.push(metadata.resolved_name.clone()); + if !metadata.interface_names.is_empty() { + parts.push(String::from("extends")); + parts.push(metadata.interface_names.join(", ")); + } + } else if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + parts.push(String::from("trait")); + parts.push(metadata.resolved_name.clone()); + } else { + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + parts.push(String::from("abstract")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0 { + parts.push(String::from("final")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0 { + parts.push(String::from("readonly")); + } + parts.push(String::from("class")); + parts.push(metadata.resolved_name.clone()); + if let Some(parent_class_name) = metadata.parent_class_name.as_ref() { + parts.push(String::from("extends")); + parts.push(parent_class_name.clone()); + } + if !metadata.interface_names.is_empty() { + parts.push(String::from("implements")); + parts.push(metadata.interface_names.join(", ")); + } + } + format!("{kind} [ {origin} {} ]", parts.join(" ")) +} + +/// Returns the ReflectionClass string header owner kind label. +pub(super) fn eval_reflection_class_to_string_kind(flags: u64) -> &'static str { + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + "Interface" + } else if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + "Trait" + } else { + "Class" + } +} + +/// Formats all constants visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_constant_string_lines( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let constant_names = eval_reflection_constant_names(reflected_name, context, values)?; + let mut lines = Vec::with_capacity(constant_names.len()); + for constant_name in constant_names { + let line = eval_reflection_class_constant_to_string( + reflected_name, + &constant_name, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + context, + values, + )?; + lines.push(line.trim_end_matches('\n').to_string()); + } + Ok(lines) +} + +/// Formats all properties visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_property_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.property_names.len()); + for property_name in &metadata.property_names { + let Some(member) = eval_reflection_reflected_property_metadata( + &metadata.resolved_name, + property_name, + context, + values, + )? + else { + continue; + }; + lines.push(eval_reflection_property_to_string(property_name, &member)); + } + Ok(lines) +} + +/// Formats all methods visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_method_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.method_names.len()); + for method_name in &metadata.method_names { + let member = + if let Some(member) = + eval_reflection_method_metadata(&metadata.resolved_name, method_name, context) + { + Some(member) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + &metadata.resolved_name, + method_name, + context, + values, + )? + }; + let Some(member) = member else { + continue; + }; + lines.push(eval_reflection_method_summary_to_string(method_name, &member)); + } + Ok(lines) +} + +/// Appends one named section to a ReflectionClass string dump. +pub(super) fn eval_reflection_class_append_string_section( + rendered: &mut String, + label: &str, + lines: &[String], +) { + rendered.push_str(&format!(" - {label} [{}] {{\n", lines.len())); + for line in lines { + rendered.push_str(" "); + rendered.push_str(line); + rendered.push('\n'); + } + rendered.push_str(" }\n"); +} + +/// Formats one reflected method line for `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_method_summary_to_string( + method_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + if member.is_static { + parts.push(String::from("static")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + parts.push(String::from("method")); + parts.push(method_name.to_string()); + format!("Method [ {} ]", parts.join(" ")) +} + +/// Formats one reflected function or method similarly to PHP's `__toString()` output. +pub(super) fn eval_reflection_function_method_to_string( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + let mut rendered = format!( + "{} {{\n - Parameters [{}] {{\n", + eval_reflection_function_method_header(target), + eval_reflection_function_method_parameters(target).len() + ); + for parameter in eval_reflection_function_method_parameters(target) { + rendered.push_str(" "); + rendered.push_str(&eval_reflection_function_method_parameter_to_string(parameter)); + rendered.push('\n'); + } + rendered.push_str(" }\n"); + if let Some(return_type) = eval_reflection_function_method_return_type(target) { + rendered.push_str(" - Return [ "); + rendered.push_str(&eval_reflection_type_metadata_to_string(return_type)); + rendered.push_str(" ]\n"); + } + rendered.push_str("}\n"); + rendered +} + +/// Returns the PHP-like header line for a reflected function or method. +pub(super) fn eval_reflection_function_method_header( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + format!( + "Function [ function {} ]", + name.trim_start_matches('\\') + ) + } + EvalReflectionFunctionMethodTarget::Method { + name, + visibility, + is_static, + is_final, + is_abstract, + .. + } => { + let mut parts = Vec::new(); + if *is_abstract { + parts.push(String::from("abstract")); + } + if *is_final { + parts.push(String::from("final")); + } + if *is_static { + parts.push(String::from("static")); + } + if let Some(visibility) = visibility { + parts.push(eval_reflection_visibility_label(*visibility).to_string()); + } + parts.push(String::from("method")); + parts.push(name.clone()); + format!("Method [ {} ]", parts.join(" ")) + } + } +} + +/// Returns retained parameters for a reflected function or method target. +pub(super) fn eval_reflection_function_method_parameters( + target: &EvalReflectionFunctionMethodTarget, +) -> &[EvalReflectionParameterMetadata] { + match target { + EvalReflectionFunctionMethodTarget::Function { parameters, .. } + | EvalReflectionFunctionMethodTarget::Method { parameters, .. } => parameters, + } +} + +/// Formats one parameter line for function-like `__toString()` output. +pub(super) fn eval_reflection_function_method_parameter_to_string( + parameter: &EvalReflectionParameterMetadata, +) -> String { + let requiredness = if parameter.is_optional { + "optional" + } else { + "required" + }; + let mut signature_parts = Vec::new(); + if let Some(type_metadata) = parameter.type_metadata.as_ref() { + signature_parts.push(eval_reflection_type_metadata_to_string(type_metadata)); + } + let mut variable = String::new(); + if parameter.is_passed_by_reference { + variable.push('&'); + } + if parameter.is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(¶meter.name); + signature_parts.push(variable); + let default = parameter + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + format!( + "Parameter #{} [ <{}> {}{} ]", + parameter.position, + requiredness, + signature_parts.join(" "), + default + ) +} + +/// Formats one reflected property similarly to PHP's `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_property_to_string( + property_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + if member.is_dynamic { + return format!("Property [ public ${property_name} ]\n"); + } + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + if member.is_static { + parts.push(String::from("static")); + } + if member.is_readonly { + parts.push(String::from("readonly")); + } + if let Some(type_name) = member + .type_metadata + .as_ref() + .map(eval_reflection_type_metadata_to_string) + { + parts.push(type_name); + } + parts.push(format!("${property_name}")); + + let default = if member.modifiers & 512 != 0 { + String::new() + } else { + member + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default() + }; + format!("Property [ {}{} ]", parts.join(" "), default) +} + +/// Formats one class constant or enum case like PHP's `ReflectionClassConstant::__toString()`. +pub(super) fn eval_reflection_class_constant_to_string( + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, _, visibility, is_final, is_enum_case) = + eval_reflection_class_constant_metadata(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_reflection_constant_value(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut parts = Vec::new(); + if is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(visibility).to_string()); + parts.push(eval_reflection_class_constant_type_label( + declaring_class, + value, + is_enum_case + || matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ), + values, + )?); + parts.push(constant_name.to_string()); + let value = eval_reflection_class_constant_display_value(value, values)?; + Ok(format!("Constant [ {} ] {{ {} }}\n", parts.join(" "), value)) +} + +/// Returns the type label PHP prints for a reflected class constant value. +pub(super) fn eval_reflection_class_constant_type_label( + declaring_class: &str, + value: RuntimeCellHandle, + is_enum_case: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if is_enum_case { + return Ok(declaring_class.trim_start_matches('\\').to_string()); + } + Ok(match values.type_tag(value)? { + EVAL_TAG_INT => String::from("int"), + EVAL_TAG_STRING => String::from("string"), + EVAL_TAG_FLOAT => String::from("float"), + EVAL_TAG_BOOL => String::from("bool"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("array"), + EVAL_TAG_OBJECT => String::from("object"), + EVAL_TAG_NULL => String::from("null"), + _ => String::from("mixed"), + }) +} + +/// Returns the value display PHP prints inside ReflectionClassConstant braces. +pub(super) fn eval_reflection_class_constant_display_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + EVAL_TAG_NULL => String::new(), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + +/// Returns PHP's lowercase label for one reflected visibility. +pub(super) fn eval_reflection_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + +/// Formats retained ReflectionType metadata for `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_type_metadata_to_string( + type_metadata: &EvalReflectionParameterTypeMetadata, +) -> String { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named) => { + if named.allows_null && named.name != "mixed" { + format!("?{}", named.name) + } else { + named.name.clone() + } + } + EvalReflectionParameterTypeKind::Union(union) => { + let mut names = union + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>(); + if union.allows_null && names.iter().all(|name| name != "null") { + names.push(String::from("null")); + } + names.join("|") + } + EvalReflectionParameterTypeKind::Intersection(intersection) => intersection + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>() + .join("&"), + } +} + +/// Formats retained literal defaults for `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_default_expr_to_string(default: &EvalExpr) -> Option { + match default { + EvalExpr::Const(EvalConst::Null) => Some(String::from("NULL")), + EvalExpr::Const(EvalConst::Bool(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Int(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Float(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::String(value)) => Some(format!("'{value}'")), + EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr, + } => eval_reflection_default_expr_to_string(expr), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_reflection_default_expr_to_string(expr).map(|value| format!("-{value}")), + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{class_name}::{constant}")), + _ => None, + } +} + +/// Returns whether eval retained this property as virtual rather than backed. +pub(super) fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { + property.is_virtual() +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. +pub(super) fn eval_reflection_method_modifiers_from_flags(flags: u64) -> u64 { + let mut modifiers = 0; + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0 { + modifiers |= 1; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0 { + modifiers |= 2; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0 { + modifiers |= 4; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0 { + modifiers |= 16; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0 { + modifiers |= 32; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0 { + modifiers |= 64; + } + modifiers +} diff --git a/crates/elephc-magician/src/interpreter/reflection/function_construction.rs b/crates/elephc-magician/src/interpreter/reflection/function_construction.rs new file mode 100644 index 0000000000..3bfd265481 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/function_construction.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Constructs `ReflectionFunction` owners for eval, closure, and native targets. +//! +//! Called from: +//! - `crate::interpreter::reflection::eval_reflection_owner_new_object()`. +//! +//! Key details: +//! - Closure targets and native parameter/default metadata are attached once here. + +use super::*; + +/// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. +pub(super) fn eval_reflection_function_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; + let closure_target = eval_reflection_function_closure_target_arg(args[0], context, values)?; + let requested_name = match closure_target.as_ref() { + Some(target) => eval_reflection_function_closure_target_name(target), + None => eval_reflection_function_name_arg(args[0], context, values)?, + }; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(closure) = context.closure(&requested_name).cloned() { + let function = closure.function(); + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return eval_reflection_function_object_result( + &requested_name, + function.attributes(), + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if let Some(function) = context.function(&lookup_name).cloned() { + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return eval_reflection_function_object_result( + function.name(), + function.attributes(), + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let required_parameter_count = function.required_param_count(); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); + return eval_reflection_function_object_result( + reflected_name, + &[], + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if closure_target.is_some() { + return eval_reflection_function_object_result( + &requested_name, + &[], + &[], + 0, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + Ok(None) +} + +/// Returns the retained callable target when a ReflectionFunction argument is a Closure object. +pub(super) fn eval_reflection_function_closure_target_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let identity = values.object_identity(value)?; + Ok(context.closure_object_target(identity).cloned()) +} + +/// Returns the function-like name exposed for a Closure-backed ReflectionFunction. +pub(super) fn eval_reflection_function_closure_target_name(target: &EvalClosureObjectTarget) -> String { + match target { + EvalClosureObjectTarget::Named(name) + | EvalClosureObjectTarget::BoundNamed { name, .. } => name.clone(), + EvalClosureObjectTarget::InvokableObject { .. } => String::from("__invoke"), + EvalClosureObjectTarget::ObjectMethod { method, .. } + | EvalClosureObjectTarget::StaticMethod { method, .. } => method.clone(), + } +} + +/// Attaches original Closure target metadata to a synthetic ReflectionFunction object. +pub(super) fn eval_reflection_attach_function_closure_target( + object: RuntimeCellHandle, + closure_target: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(closure_target) = closure_target else { + return Ok(object); + }; + let identity = values.object_identity(object)?; + context.register_eval_reflection_function_closure_target(identity, closure_target); + Ok(object) +} + +/// Returns parameter names for a registered native function, filling missing bridge names. +pub(super) fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> Vec { + (0..function.param_count()) + .map(|index| { + function + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Builds ReflectionParameter metadata for one registered native AOT function. +pub(super) fn eval_reflection_native_function_parameters( + function_name: &str, + function: &NativeFunction, +) -> Vec { + let parameter_names = eval_reflection_native_function_parameter_names(function); + let parameter_count = parameter_names.len(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_types = eval_reflection_native_function_parameter_types(function); + let parameter_defaults = eval_reflection_native_function_parameter_defaults(function); + let parameter_is_by_ref = (0..parameter_count) + .map(|index| function.param_by_ref(index)) + .collect::>(); + let parameter_is_variadic = (0..parameter_count) + .map(|index| function.param_variadic(index)) + .collect::>(); + eval_reflection_function_parameters( + function_name, + ¶meter_names, + Vec::new(), + ¶meter_attributes, + ¶meter_types, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + ) +} + +/// Converts registered native function parameter types into reflection metadata input. +pub(super) fn eval_reflection_native_function_parameter_types( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| function.param_type(index).cloned()) + .collect() +} + +/// Converts registered native function defaults into eval constant expressions. +pub(super) fn eval_reflection_native_function_parameter_defaults( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| { + function + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + +/// Builds one `ReflectionFunction` object from retained eval function metadata. +pub(super) fn eval_reflection_function_object_result( + function_name: &str, + attributes: &[EvalAttribute], + parameters: &[EvalReflectionParameterMetadata], + required_parameter_count: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_FUNCTION, + function_name, + attributes, + &[], + &[], + &[], + &[], + None, + parameters, + None, + None, + None, + None, + eval_reflection_callable_flags(attributes), + required_parameter_count as u64, + 0, + None, + None, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs new file mode 100644 index 0000000000..ff251b00d2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs @@ -0,0 +1,934 @@ +//! Purpose: +//! Resolves metadata shared by `ReflectionFunction` and `ReflectionMethod`. +//! It covers callable identity, source data, closures, statics, and prototypes. +//! +//! Called from: +//! - `crate::interpreter::reflection` while answering callable Reflection APIs. +//! +//! Key details: +//! - Eval, native, closure, and AOT method targets converge on one metadata shape. +//! - Prototype traversal preserves parent and interface declaration order. + +use super::*; + +/// Returns function or method metadata registered for a synthetic reflection owner object. +pub(super) fn eval_reflection_function_method_target( + identity: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(name) = context.eval_reflection_function_name(identity) { + let closure_target = context + .eval_reflection_function_closure_target(identity) + .cloned(); + if let Some(closure) = context.closure(name) { + let function = closure.function(); + let is_variadic = function.parameter_is_variadic().iter().any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + closure_captures: closure.captures().to_vec(), + parameters, + source_location, + closure_target, + is_variadic, + is_static: closure.is_static(), + is_closure: true, + is_deprecated, + return_type_metadata, + })); + } + let lookup_name = name.to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name) { + let is_variadic = function + .parameter_is_variadic() + .iter() + .any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + closure_captures: Vec::new(), + parameters, + source_location, + closure_target: closure_target.clone(), + is_variadic, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated, + return_type_metadata, + })); + } + if let Some(function) = context.native_function(&lookup_name) { + let parameters = eval_reflection_native_function_parameters(name, &function); + let is_variadic = + (0..function.param_count()).any(|index| function.param_variadic(index)); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key: None, + static_variables: Vec::new(), + closure_captures: Vec::new(), + parameters, + source_location: None, + closure_target: closure_target.clone(), + is_variadic, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated: false, + return_type_metadata, + })); + } + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key: None, + static_variables: Vec::new(), + closure_captures: Vec::new(), + parameters: Vec::new(), + source_location: None, + closure_target: closure_target.clone(), + is_variadic: false, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated: false, + return_type_metadata: None, + })); + } + let Some((declaring_class, method_name)) = context.eval_reflection_method(identity) else { + return Ok(None); + }; + let method_metadata = if let Some(method_metadata) = + eval_reflection_method_metadata(declaring_class, method_name, context) + { + Some(method_metadata) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + declaring_class, + method_name, + context, + values, + )? + }; + let ( + parameters, + source_file, + source_location, + visibility, + is_variadic, + is_static, + is_final, + is_abstract, + is_deprecated, + return_type_metadata, + ) = match method_metadata { + Some(method) => { + let is_variadic = method + .parameters + .iter() + .any(|parameter| parameter.is_variadic); + let is_deprecated = + eval_reflection_attributes_include_deprecated(&method.attributes); + ( + method.parameters, + method.source_file, + method.source_location, + Some(method.visibility), + is_variadic, + method.is_static, + method.is_final, + method.is_abstract, + is_deprecated, + method.return_type_metadata, + ) + } + None => ( + Vec::new(), + None, + None, + None, + false, + false, + false, + false, + false, + None, + ), + }; + let static_method = + eval_reflection_eval_method_static_target(declaring_class, method_name, context); + let declaring_class = static_method + .as_ref() + .map(|(declaring_class, _)| declaring_class.clone()); + let static_key = static_method + .as_ref() + .map(|(declaring_class, method)| eval_method_static_local_key(declaring_class, method.name())); + let static_variables = static_method + .as_ref() + .map(|(_, method)| static_var_initializers(method.body())) + .unwrap_or_default(); + Ok(Some(EvalReflectionFunctionMethodTarget::Method { + declaring_class, + name: method_name.to_string(), + static_key, + static_variables, + parameters, + source_file, + source_location, + visibility, + is_variadic, + is_static, + is_final, + is_abstract, + is_deprecated, + return_type_metadata, + })) +} + +/// Returns an eval method body that can contribute ReflectionMethod static locals. +pub(super) fn eval_reflection_eval_method_static_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_method(declaring_class, method_name); + } + let trait_decl = context.trait_decl(declaring_class)?; + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| (trait_decl.name().to_string(), method.clone())) +} + +/// Builds the static-local storage key shared by method execution and reflection. +pub(super) fn eval_method_static_local_key(class_name: &str, method_name: &str) -> String { + format!("{}::{}", class_name.trim_start_matches('\\'), method_name) +} + +/// Builds the associative `getStaticVariables()` result for eval-backed reflection. +pub(super) fn eval_reflection_function_method_static_variables_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (Some(static_key), static_variables, declaring_class) = + eval_reflection_function_method_static_target(target) + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(static_variables.len())?; + for variable in static_variables { + let key = values.string(&variable.name)?; + let value = eval_reflection_static_local_value( + static_key, + variable, + declaring_class, + context, + values, + )?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns static-local storage details retained for a reflected eval function or method. +pub(super) fn eval_reflection_function_method_static_target( + target: &EvalReflectionFunctionMethodTarget, +) -> ( + Option<&str>, + &[EvalStaticVarInitializer], + Option<&str>, +) { + match target { + EvalReflectionFunctionMethodTarget::Function { + static_key, + static_variables, + .. + } => (static_key.as_deref(), static_variables, None), + EvalReflectionFunctionMethodTarget::Method { + declaring_class, + static_key, + static_variables, + .. + } => ( + static_key.as_deref(), + static_variables, + declaring_class.as_deref(), + ), + } +} + +/// Returns the retained current static value or initializes it for reflection. +pub(super) fn eval_reflection_static_local_value( + static_key: &str, + variable: &EvalStaticVarInitializer, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = context.static_local(static_key, &variable.name) { + return values.retain(value); + } + let value = eval_reflection_static_local_initializer_value( + static_key, + &variable.init, + declaring_class, + context, + values, + )?; + if let Some(replaced) = + context.set_static_local(static_key.to_string(), variable.name.clone(), value) + { + values.release(replaced)?; + } + values.retain(value) +} + +/// Evaluates a static-local initializer with PHP magic class/function context. +pub(super) fn eval_reflection_static_local_initializer_value( + static_key: &str, + init: &EvalExpr, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(declaring_class) = declaring_class { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + } + context.push_function(static_key.to_string()); + let mut scope = ElephcEvalScope::new(); + let result = eval_expr(init, context, &mut scope, values); + for cell in scope.drain_owned_cells() { + values.release(cell)?; + } + context.pop_function(); + if declaring_class.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + result +} + +/// Validates that a synthetic reflection metadata call received no arguments. +pub(super) fn eval_reflection_bind_no_args(evaluated_args: Vec) -> Result<(), EvalStatus> { + let _ = bind_evaluated_function_args(&[], evaluated_args)?; + Ok(()) +} + +/// Returns a no-argument reflection metadata predicate result that is always false. +pub(super) fn eval_reflection_false_metadata_result( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(false).map(Some) +} + +/// Returns source file or line metadata for eval-backed reflection objects. +pub(super) fn eval_reflection_source_location_result( + method_key: &str, + source_file: Option<&str>, + source_location: Option, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + let Some(source_location) = source_location else { + return values.bool_value(false).map(Some); + }; + match method_key { + "getfilename" => { + let eval_file; + let file = if let Some(source_file) = source_file { + source_file + } else { + eval_file = context.eval_file_magic(); + &eval_file + }; + values.string(file).map(Some) + } + "getstartline" => values.int(source_location.start_line()).map(Some), + "getendline" => values.int(source_location.end_line()).map(Some), + _ => Ok(None), + } +} + +/// Returns PHP's short name for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_short_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_short_name(name) + } + EvalReflectionFunctionMethodTarget::Method { name, .. } => name.clone(), + } +} + +/// Returns eval-fragment source metadata for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_source_location( + target: &EvalReflectionFunctionMethodTarget, +) -> (Option<&str>, Option) { + match target { + EvalReflectionFunctionMethodTarget::Function { + source_location, .. + } => (None, *source_location), + EvalReflectionFunctionMethodTarget::Method { + source_file, + source_location, .. + } => (source_file.as_deref(), *source_location), + } +} + +/// Returns PHP's namespace name for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_namespace_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_namespace_name(name) + } + EvalReflectionFunctionMethodTarget::Method { .. } => String::new(), + } +} + +/// Returns whether the reflected function or method has a variadic parameter. +pub(super) fn eval_reflection_function_method_is_variadic( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_variadic, .. } + | EvalReflectionFunctionMethodTarget::Method { is_variadic, .. } => *is_variadic, + } +} + +/// Returns whether the reflected function-like target is an eval closure literal. +pub(super) fn eval_reflection_function_method_is_closure( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_closure, .. } => *is_closure, + EvalReflectionFunctionMethodTarget::Method { .. } => false, + } +} + +/// Returns whether the reflected function-like target is static. +pub(super) fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMethodTarget) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_static, .. } + | EvalReflectionFunctionMethodTarget::Method { is_static, .. } => *is_static, + } +} + +/// Returns whether retained Closure target metadata represents a static callable. +pub(super) fn eval_reflection_closure_target_is_static(target: Option<&EvalClosureObjectTarget>) -> bool { + matches!(target, Some(EvalClosureObjectTarget::StaticMethod { .. })) +} + +/// Returns whether the reflected function-like target carries `#[Deprecated]`. +pub(super) fn eval_reflection_function_method_is_deprecated( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_deprecated, .. } + | EvalReflectionFunctionMethodTarget::Method { is_deprecated, .. } => *is_deprecated, + } +} + +/// Builds `ReflectionFunction::getClosureUsedVariables()` for eval closure targets. +pub(super) fn eval_reflection_function_closure_used_variables_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let EvalReflectionFunctionMethodTarget::Function { + is_closure: true, + closure_captures, + .. + } = target + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(closure_captures.len())?; + for capture in closure_captures { + let key = values.string(capture.name())?; + let value = values.retain(capture.value())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds `ReflectionFunction::getClosureThis()` from retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_this_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(target) = eval_reflection_function_closure_target(target) else { + return values.null(); + }; + match target { + EvalClosureObjectTarget::BoundNamed { + bound_this: Some(object), + .. + } + | EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => values.retain(*object), + EvalClosureObjectTarget::Named(_) + | EvalClosureObjectTarget::BoundNamed { + bound_this: None, .. + } + | EvalClosureObjectTarget::StaticMethod { .. } => values.null(), + } +} + +/// Builds `ReflectionFunction::getClosureScopeClass()` from retained Closure metadata. +pub(super) fn eval_reflection_function_closure_scope_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_scope_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Builds `ReflectionFunction::getClosureCalledClass()` from retained Closure metadata. +pub(super) fn eval_reflection_function_closure_called_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_called_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Returns the retained callable target for a Closure-backed ReflectionFunction. +pub(super) fn eval_reflection_function_closure_target( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalClosureObjectTarget> { + match target { + EvalReflectionFunctionMethodTarget::Function { closure_target, .. } => { + closure_target.as_ref() + } + EvalReflectionFunctionMethodTarget::Method { .. } => None, + } +} + +/// Resolves the PHP closure scope class name for retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_scope_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => { + if context.closure(name).is_none() { + return Ok(bound_this.map(|_| String::from("Closure"))); + } + if let Some(bound_scope) = bound_scope { + return Ok(Some(bound_scope.clone())); + } + match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(None), + } + } + EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::StaticMethod { class_name, .. } => { + Ok(Some(class_name.trim_start_matches('\\').to_string())) + } + } +} + +/// Resolves the PHP closure called class name for retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_called_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + bound_this, + bound_scope, + .. + } => match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(bound_scope.clone()), + }, + EvalClosureObjectTarget::InvokableObject { object } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + called_class, + .. + } => match called_class { + Some(called_class) => Ok(Some(called_class.clone())), + None => eval_closure_bound_object_class_name(*object, context, values).map(Some), + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + called_class, + .. + } => Ok(Some( + called_class + .as_deref() + .unwrap_or(class_name) + .trim_start_matches('\\') + .to_string(), + )), + } +} + +/// Materializes a nullable ReflectionClass result for Closure scope metadata. +pub(super) fn eval_reflection_function_closure_class_object_result( + class_name: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = class_name else { + return values.null(); + }; + eval_reflection_full_class_object_result(&class_name, context, values) +} + +/// Returns the retained return type metadata for a reflected function or method. +pub(super) fn eval_reflection_function_method_return_type( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalReflectionParameterTypeMetadata> { + match target { + EvalReflectionFunctionMethodTarget::Function { + return_type_metadata, + .. + } + | EvalReflectionFunctionMethodTarget::Method { + return_type_metadata, + .. + } => return_type_metadata.as_ref(), + } +} + +/// Returns the final namespace segment-free name component from a PHP symbol name. +pub(super) fn eval_reflection_short_name(name: &str) -> String { + let name = name.trim_start_matches('\\'); + name.rsplit_once('\\').map_or_else( + || name.to_string(), + |(_, short_name)| short_name.to_string(), + ) +} + +/// Returns the namespace prefix from a PHP function name, or an empty string. +pub(super) fn eval_reflection_namespace_name(name: &str) -> String { + name.trim_start_matches('\\') + .rsplit_once('\\') + .map_or_else(String::new, |(namespace_name, _)| { + namespace_name.to_string() + }) +} + +/// Builds ReflectionMethod metadata for a resolved eval or AOT prototype target. +pub(super) fn eval_reflection_prototype_method_metadata( + prototype_class: &str, + prototype_method: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(metadata) = + eval_reflection_method_metadata(prototype_class, prototype_method, context) + { + return Ok(Some(metadata)); + } + eval_reflection_aot_method_metadata_with_signature_if_exists( + prototype_class, + prototype_method, + context, + values, + ) +} + +/// Finds the PHP ReflectionMethod prototype target for an eval or AOT method. +pub(super) fn eval_reflection_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + let want_static = eval_reflection_method_metadata(declaring_class, method_name, context) + .map_or(false, |metadata| metadata.is_static); + if let Some(prototype) = + eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) + { + return Ok(Some(prototype)); + } + if let Some(prototype) = eval_reflection_eval_aot_parent_method_prototype_target( + declaring_class, + method_name, + context, + want_static, + values, + )? { + return Ok(Some(prototype)); + } + if let Some(prototype) = + eval_reflection_interface_method_prototype_target(declaring_class, method_name, context) + { + return Ok(Some(prototype)); + } + return eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class, + method_name, + context, + want_static, + values, + ); + } + eval_reflection_aot_method_prototype_target(declaring_class, method_name, values) +} + +/// Finds the PHP ReflectionMethod prototype target for a generated/AOT method. +pub(super) fn eval_reflection_aot_method_prototype_target( + declaring_class: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(declaring_class, method_name)? else { + return Ok(None); + }; + if let Some(prototype) = + eval_reflection_aot_parent_method_prototype_target(declaring_class, method_name, flags, values)? + { + return Ok(Some(prototype)); + } + eval_reflection_aot_interface_method_prototype_target( + declaring_class, + method_name, + flags, + values, + ) +} + +/// Finds the nearest generated/AOT parent-class method that can act as prototype. +pub(super) fn eval_reflection_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let mut current = eval_reflection_aot_parent_class_name(declaring_class, values)?; + let mut seen = std::collections::HashSet::new(); + while let Some(parent_class) = current { + if !seen.insert(parent_class.to_ascii_lowercase()) { + break; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + current = eval_reflection_aot_parent_class_name(&parent_class, values)?; + } + Ok(None) +} + +/// Finds the first generated/AOT interface method that can act as prototype. +pub(super) fn eval_reflection_aot_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + for interface_name in eval_reflection_aot_class_interface_names(declaring_class, values)? { + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + +/// Returns one generated/AOT method prototype candidate if staticness and visibility match. +pub(super) fn eval_reflection_aot_method_candidate( + class_name: &str, + method_name: &str, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; + if is_static != want_static || is_private { + return Ok(None); + } + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + Ok(Some((declaring_class, method_name.to_ascii_lowercase()))) +} + +/// Finds the nearest parent-class method prototype for an eval-declared override. +pub(super) fn eval_reflection_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + for parent_class in context.class_parent_names(declaring_class) { + if let Some((prototype_class, prototype_method)) = + context.class_own_method(&parent_class, method_name) + { + return Some((prototype_class, prototype_method.name().to_string())); + } + } + None +} + +/// Finds the nearest generated/AOT parent-class method prototype for an eval method. +pub(super) fn eval_reflection_eval_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent_class) = context.class_native_parent_name(declaring_class) else { + return Ok(None); + }; + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values) +} + +/// Finds the interface method prototype for an eval-declared class method. +pub(super) fn eval_reflection_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + let mut seen = std::collections::HashSet::new(); + for interface_name in context.class_interface_names(declaring_class) { + if let Some(prototype) = eval_reflection_interface_declared_method_target( + &interface_name, + method_name, + context, + &mut seen, + ) { + return Some(prototype); + } + } + None +} + +/// Finds an AOT interface method prototype for an eval-declared class method. +pub(super) fn eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + for interface_name in eval_reflection_eval_class_interface_names( + declaring_class, + context, + values, + )? { + if context.has_interface(&interface_name) { + continue; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + +/// Finds the interface that actually declares a method in an interface hierarchy. +pub(super) fn eval_reflection_interface_declared_method_target( + interface_name: &str, + method_name: &str, + context: &ElephcEvalContext, + seen: &mut std::collections::HashSet, +) -> Option<(String, String)> { + let interface = context.interface(interface_name)?; + if !seen.insert(interface.name().to_ascii_lowercase()) { + return None; + } + if let Some(method) = interface + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + { + return Some((interface.name().to_string(), method.name().to_string())); + } + for parent in interface.parents() { + if let Some(prototype) = + eval_reflection_interface_declared_method_target(parent, method_name, context, seen) + { + return Some(prototype); + } + } + None +} diff --git a/crates/elephc-magician/src/interpreter/reflection/invocation.rs b/crates/elephc-magician/src/interpreter/reflection/invocation.rs new file mode 100644 index 0000000000..97ad08a120 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/invocation.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Invokes reflected eval, closure, native, and AOT functions or methods. +//! +//! Called from: +//! - Callable Reflection APIs after PHP argument normalization. +//! +//! Key details: +//! - Forwarded named arguments and declaring-class scope are preserved across dispatch paths. + +use super::*; + +/// Binds `ReflectionMethod::invoke()` arguments and preserves forwarded named args. +pub(super) fn eval_reflection_method_invoke_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut object = None; + let mut method_args = Vec::new(); + for arg in evaluated_args { + if matches!(arg.name.as_deref(), Some("object")) { + if object.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + object = Some(arg.value); + } else if object.is_none() && arg.name.is_none() { + object = Some(arg.value); + } else { + method_args.push(eval_reflection_method_forwarded_value_arg(arg)); + } + } + object + .map(|object| (object, method_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a variadic `invoke()` argument into a by-value forwarded method argument. +pub(super) fn eval_reflection_method_forwarded_value_arg(arg: EvaluatedCallArg) -> EvaluatedCallArg { + EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + } +} + +/// Binds `ReflectionMethod::invokeArgs()` and expands its PHP argument array. +pub(super) fn eval_reflection_method_invoke_args_array( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("args")], + evaluated_args, + )?; + let method_args = eval_array_call_arg_values(args[1], context, values)?; + Ok((args[0], method_args)) +} + +/// Binds `ReflectionFunction::invokeArgs()` and expands its PHP argument array. +pub(super) fn eval_reflection_function_invoke_args_array( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], context, values) +} + +/// Dispatches one reflected function invocation through eval or registered native functions. +pub(super) fn eval_reflection_function_invoke_dispatch( + function_name: &str, + function_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(closure) = context.closure(function_name).cloned() { + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); + } + let function_key = function_name.to_ascii_lowercase(); + if let Some(function) = context.function(&function_key).cloned() { + return eval_dynamic_function_with_evaluated_args_and_ref_mode( + &function, + function.parameter_is_by_ref(), + function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, + context, + values, + ); + } + eval_callable_with_call_array_args(&function_key, function_args, context, values) +} + +/// Dispatches one reflected method invocation through eval or AOT bridges. +pub(super) fn eval_reflection_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let lookup_method_name = eval_reflection_property_hook_synthetic_method_name(method_name) + .unwrap_or_else(|| method_name.to_string()); + if let Some((method_class, method)) = context.class_method(declaring_class, &lookup_method_name) + { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let callable_name = format!( + "{}::{}", + method_class.trim_start_matches('\\'), + method.name() + ); + if method.is_static() { + return eval_dynamic_static_method_with_values_and_ref_mode( + &method_class, + &method_class, + &method, + method.parameter_is_by_ref(), + method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ); + } + let called_class = + eval_reflection_method_instance_called_class(declaring_class, object, context, values)?; + return eval_dynamic_method_with_values_and_ref_mode( + &method_class, + &called_class, + &method, + object, + method.parameter_is_by_ref(), + method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ); + } + if eval_enum_static_builtin_applies(declaring_class, &lookup_method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + declaring_class, + &lookup_method_name, + method_args, + context, + values, + ); + } + eval_reflection_aot_method_invoke_dispatch( + declaring_class, + method_name, + object, + method_args, + context, + values, + ) +} + +/// Returns the runtime class name for an eval object used as a reflected receiver. +pub(super) fn eval_reflection_method_instance_called_class( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)?; + let Some(object_class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.class_is_a(&object_class_name, declaring_class, false) { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + Ok(object_class_name) +} + +/// Invokes one reflected generated/AOT method when it fits the bridge slice. +pub(super) fn eval_reflection_aot_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let member = + eval_reflection_aot_method_metadata_if_exists(declaring_class, method_name, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + if member.is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if member.is_static { + return eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + declaring_class, + method_name, + method_args, + Some(declaring_class), + Some(declaring_class), + context, + values, + ) + }); + } + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if !is_instance { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + let called_class = eval_reflection_object_class_name(object, context, values)?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + declaring_class, + method_name, + method_args, + Some(declaring_class), + Some(&called_class), + context, + values, + ) + }) +} + +/// Runs a reflected AOT invocation with the declaring class as visibility scope. +pub(super) fn eval_reflection_with_declaring_class_scope( + declaring_class: &str, + context: &mut ElephcEvalContext, + action: impl FnOnce(&mut ElephcEvalContext) -> Result, +) -> Result { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + let result = action(context); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_api.rs b/crates/elephc-magician/src/interpreter/reflection/member_api.rs new file mode 100644 index 0000000000..cfd41e0883 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_api.rs @@ -0,0 +1,558 @@ +//! Purpose: +//! Implements Reflection APIs for properties, constants, and enum cases. +//! +//! Called from: +//! - `crate::interpreter::statements` for non-callable reflected members. +//! +//! Key details: +//! - Property access, hooks, laziness, raw values, and owner-specific formatting dispatch here. + +use super::*; + +/// Handles eval-backed `ReflectionProperty` hook-inspection calls. +pub(in crate::interpreter) fn eval_reflection_property_hooks_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let Some((property_class, property)) = + eval_reflection_property_for_hooks(&declaring_class, &property_name, context) + else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hashooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + let has_hooks = !eval_reflection_property_hook_kinds(&property).is_empty(); + values.bool_value(has_hooks).map(Some) + } + "hashook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + values + .bool_value(eval_reflection_property_has_hook(&property, hook)) + .map(Some) + } + "gethook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + if !eval_reflection_property_has_hook(&property, hook) { + return values.null().map(Some); + } + eval_reflection_property_hook_method_object( + &property_class, + &property, + hook, + context, + values, + ) + .map(Some) + } + "gethooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_property_hook_method_array(&property_class, &property, context, values) + .map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionProperty::getValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_get_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + return eval_reflection_static_property_value( + &declaring_class, + &property_name, + context, + values, + )? + .map(Some) + .ok_or(EvalStatus::RuntimeFatal); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .map(Some) +} + +/// Handles eval-backed `ReflectionProperty::setValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_set_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + return values.null().map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + let value = value.unwrap_or(object_or_value); + if eval_reflection_class_like_exists(&declaring_class, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, value) + { + values.release(replaced)?; + } + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(declaring_class.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_set(declaring_class, &property_name, value) + })?; + if !updated { + return Err(EvalStatus::RuntimeFatal); + } + } + return values.null().map(Some); + } + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } + values.null().map(Some) +} + +/// Handles `ReflectionProperty::isInitialized()` calls for eval and generated/AOT properties. +pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInitialized") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + .and_then(|initialized| values.bool_value(initialized)) + .map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + let initialized = if eval_reflection_class_like_exists(declaring_class, context) { + context + .static_property(declaring_class, &property_name) + .is_some() + } else { + eval_reflection_aot_static_property_is_initialized( + declaring_class, + &property_name, + context, + values, + )? + }; + return values.bool_value(initialized).map(Some); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .and_then(|initialized| values.bool_value(initialized)) + .map(Some) +} + +/// Handles `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. +pub(in crate::interpreter) fn eval_reflection_property_lazy_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("isLazy") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return values.bool_value(false).map(Some); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_property_validate_object(&declaring_class, object, context, values)?; + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + } + return values.bool_value(false).map(Some); + } + if method_name.eq_ignore_ascii_case("skipLazyInitialization") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return Err(EvalStatus::RuntimeFatal); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + let (_, property) = eval_reflection_instance_property_target( + &declaring_class, + &property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + } + return values.null().map(Some); + } + Ok(None) +} + +/// Handles eval-backed `ReflectionProperty::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_property_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let member = eval_reflection_dynamic_property_metadata(&declaring_class); + let text = eval_reflection_property_to_string(&property_name, &member); + return values.string(&text).map(Some); + } + let member = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let text = eval_reflection_property_to_string(&property_name, &member); + values.string(&text).map(Some) +} + +/// Handles eval-backed `ReflectionClassConstant` and enum-case `__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_class_constant_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, constant_name, owner_kind)) = + context + .eval_reflection_class_constant(identity) + .map(|(declaring_class, constant_name, owner_kind)| { + ( + declaring_class.to_string(), + constant_name.to_string(), + owner_kind, + ) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let text = eval_reflection_class_constant_to_string( + &declaring_class, + &constant_name, + owner_kind, + context, + values, + )?; + values.string(&text).map(Some) +} + +/// Handles `ReflectionEnumUnitCase::getEnum()` and `ReflectionEnumBackedCase::getEnum()`. +pub(in crate::interpreter) fn eval_reflection_enum_case_get_enum_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getEnum") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some((declaring_class, _, owner_kind)) = context + .eval_reflection_class_constant(identity) + .map(|(class, constant, owner_kind)| (class.to_string(), constant.to_string(), owner_kind)) + else { + return Ok(None); + }; + if !matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return Ok(None); + } + eval_reflection_enum_object_result(&declaring_class, context, values).map(Some) +} + +/// Handles `ReflectionProperty::getRawValue()` and raw write calls. +pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("getRawValue") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } + return if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_raw_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .map(Some); + } + if method_name.eq_ignore_ascii_case("setRawValue") + || method_name.eq_ignore_ascii_case("setRawValueWithoutLazyInitialization") + { + let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + return values.null().map(Some); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_raw_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } + return values.null().map(Some); + } + Ok(None) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_construction.rs b/crates/elephc-magician/src/interpreter/reflection/member_construction.rs new file mode 100644 index 0000000000..94f28f77de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_construction.rs @@ -0,0 +1,1058 @@ +//! Purpose: +//! Constructs ReflectionMethod and ReflectionProperty owners and their AOT metadata. +//! +//! Called from: +//! - `crate::interpreter::reflection` and reflected static method dispatch. +//! +//! Key details: +//! - Eval, object, dynamic-property, native, and AOT targets converge here. +//! - Native callable defaults are converted into eval expressions without execution. + +use super::*; + +/// Handles eval-backed `ReflectionMethod::createFromMethodName()` static calls. +pub(in crate::interpreter) fn eval_reflection_method_create_from_method_name_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("ReflectionMethod") + || !method_name.eq_ignore_ascii_case("createFromMethodName") + { + return Ok(None); + } + let args = bind_evaluated_function_args(&[String::from("method")], evaluated_args)?; + let target = eval_reflection_string_arg(args[0], values)?; + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name", + context, + values, + ); + }; + eval_reflection_method_object_result_or_throw( + &class_name, + &method_name, + context, + values, + ) +} + +/// Splits PHP's `ClassName::methodName` reflection-method target string. +pub(super) fn eval_reflection_method_target_parts(target: &str) -> Option<(String, String)> { + let Some((class_name, method_name)) = target.rsplit_once("::") else { + return None; + }; + if class_name.is_empty() || method_name.is_empty() { + return None; + } + Some((class_name.to_string(), method_name.to_string())) +} + +/// Extracts the deprecated one-argument `ReflectionMethod("Class::method")` target. +pub(super) fn eval_reflection_method_single_target_arg( + evaluated_args: Vec, +) -> Result { + let mut args = evaluated_args.into_iter(); + let Some(arg) = args.next() else { + return Err(EvalStatus::RuntimeFatal); + }; + if args.next().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(name) = arg.name.as_deref() { + if !matches!(name, "class_name" | "objectOrMethod") { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(arg.value) +} + +/// Builds a `ReflectionMethod` object when the reflected method exists in eval or AOT metadata. +pub(super) fn eval_reflection_method_object_result_if_exists( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + requested_method_name, + context, + values, + )? { + let method_name = requested_method_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &method_name, + &method, + context, + values, + ) + .map(Some); + } + return Ok(None); + } + let method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + requested_method_name, + context, + ); + let Some(method_name) = method_name else { + return Ok(None); + }; + let Some(method) = eval_reflection_method_metadata(&reflected_name, &method_name, context) + else { + return Ok(None); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &method_name, + &method, + context, + values, + ) + .map(Some) +} + +/// Builds a `ReflectionMethod` object or throws PHP's catchable reflection error. +pub(super) fn eval_reflection_method_object_result_or_throw( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_method_object_result_if_exists( + class_name, + requested_method_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!( + "Method {}::{}() does not exist", + reflected_name, requested_method_name + ), + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. +pub(super) fn eval_reflection_method_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if evaluated_args.len() == 1 { + let target = eval_reflection_method_single_target_arg(evaluated_args)?; + let target = eval_reflection_string_arg(target, values)?; + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::__construct(): Argument #1 ($objectOrMethod) must be a valid method name", + context, + values, + ); + }; + return eval_reflection_method_object_result_or_throw( + &class_name, + &method_name, + context, + values, + ); + } + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("method_name")], + evaluated_args, + )?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let requested_method_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_method_object_result_or_throw( + &class_name, + &requested_method_name, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionProperty` object when the reflected property exists in eval. +pub(super) fn eval_reflection_property_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("property_name")], + evaluated_args, + )?; + let property_name = eval_reflection_string_arg(args[1], values)?; + if values.type_tag(args[0])? == EVAL_TAG_OBJECT { + return eval_reflection_property_new_for_object(args[0], &property_name, context, values); + } + let class_name = eval_reflection_string_arg(args[0], values)?; + eval_reflection_property_object_result_or_throw(&class_name, &property_name, context, values) +} + +/// Builds a `ReflectionProperty` object when the reflected property exists. +pub(super) fn eval_reflection_property_object_result_if_exists( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + property_name, + context, + ) + { + let property = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + let Some(property) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + property_name, + context, + values, + )? + else { + return Ok(None); + }; + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + let Some(property) = eval_reflection_property_metadata(&reflected_name, property_name, context) + else { + return Ok(None); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some) +} + +/// Builds a `ReflectionProperty` object or throws PHP's catchable reflection error. +pub(super) fn eval_reflection_property_object_result_or_throw( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_property_object_result_if_exists( + class_name, + property_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &reflected_name, + property_name, + ); + eval_throw_reflection_exception(&message, context, values) +} + +/// Builds a ReflectionProperty from an object argument, including dynamic properties. +pub(super) fn eval_reflection_property_new_for_object( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = eval_reflection_object_class_name(object, context, values)?; + if let Some(property) = eval_reflection_property_metadata(&class_name, property_name, context) { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + if !eval_reflection_object_dynamic_property_exists(object, property_name, values)? { + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &class_name, + property_name, + ); + return eval_throw_reflection_exception(&message, context, values); + } + let property = eval_reflection_dynamic_property_metadata(&class_name); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some) +} + +/// Returns the class name for an object passed to a Reflection constructor. +pub(super) fn eval_reflection_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Returns whether one object has a public dynamic property by exact PHP name. +pub(super) fn eval_reflection_object_dynamic_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if property_name.contains('\0') { + return Ok(false); + } + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Returns the object captured by a `ReflectionObject` instance, when present. +pub(super) fn eval_reflection_object_reflected_object( + reflection_object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(reflection_object, "ReflectionObject", values)? { + return Ok(None); + } + let object = eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_get(reflection_object, "__object") + })?; + if values.type_tag(object)? == EVAL_TAG_OBJECT { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Appends dynamic public properties to `ReflectionObject::getProperties()` results. +pub(super) fn eval_reflection_object_dynamic_property_array_result( + reflection_object: RuntimeCellHandle, + owner_kind: u64, + reflected_name: &str, + filter: Option, + mut result: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if owner_kind != EVAL_REFLECTION_OWNER_PROPERTY { + return Ok(result); + } + let Some(object) = eval_reflection_object_reflected_object(reflection_object, context, values)? + else { + return Ok(result); + }; + let property_names = + eval_reflection_object_dynamic_property_names(object, reflected_name, context, values); + values.release(object)?; + let property_names = property_names?; + for name in property_names { + let member = eval_reflection_dynamic_property_metadata(reflected_name); + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let member_object = eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &name, + &member, + context, + values, + )?; + let next_index = values.array_len(result)? as i64; + let key = values.int(next_index)?; + result = values.array_set(result, key, member_object)?; + } + Ok(result) +} + +/// Enumerates public dynamic property names on the object behind `ReflectionObject`. +pub(super) fn eval_reflection_object_dynamic_property_names( + object: RuntimeCellHandle, + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let property_name = + String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if !eval_reflection_dynamic_property_name_is_visible( + reflected_name, + &property_name, + context, + ) { + continue; + } + if !names.iter().any(|name| name == &property_name) { + names.push(property_name); + } + } + Ok(names) +} + +/// Returns true when an object-storage name represents a public dynamic property. +pub(super) fn eval_reflection_dynamic_property_name_is_visible( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> bool { + !property_name.contains('\0') + && eval_reflection_member_name( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_name, + context, + ) + .is_none() +} + +/// Builds PHP reflection metadata for a public dynamic object property. +pub(super) fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: true, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + None, + false, + false, + false, + false, + false, + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata: None, + default_value: None, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + +/// Returns generated AOT ReflectionMethod metadata when the runtime table has a matching row. +pub(super) fn eval_reflection_aot_method_metadata_if_exists( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some(eval_reflection_aot_method_metadata( + &declaring_class_name, + method_name, + flags, + Vec::new(), + None, + None, + None, + ))) +} + +/// Returns generated AOT ReflectionMethod metadata with registered signature details. +pub(super) fn eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let mut signature = + eval_reflection_aot_method_signature(&declaring_class_name, method_name, flags, context); + if signature.is_none() && declaring_class_name != runtime_class_name { + signature = + eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); + } + let attributes = eval_reflection_aot_method_attributes( + runtime_class_name, + &declaring_class_name, + method_name, + context, + ); + let (source_file, source_location) = + eval_reflection_aot_method_source_metadata(flags, values)?; + Ok(Some(eval_reflection_aot_method_metadata( + &declaring_class_name, + method_name, + flags, + attributes, + source_file, + source_location, + signature.as_ref(), + ))) +} + +/// Returns AOT source-file and line metadata encoded in ReflectionMethod flags. +pub(super) fn eval_reflection_aot_method_source_metadata( + flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(source_location) = eval_reflection_aot_method_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionMethod source lines packed into high flag bits. +pub(super) fn eval_reflection_aot_method_source_location_from_flags( + flags: u64, +) -> Option { + let start_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + let end_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + +/// Returns generated/AOT method dispatch metadata for interpreter-only runtime decisions. +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + Ok( + eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)?.map( + |member| { + ( + member + .declaring_class_name + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()), + member.visibility, + member.is_static, + member.is_abstract, + ) + }, + ), + ) +} + +/// Converts AOT method flag metadata into the eval ReflectionMethod shape. +pub(super) fn eval_reflection_aot_method_metadata( + class_name: &str, + method_name: &str, + flags: u64, + attributes: Vec, + source_file: Option, + source_location: Option, + signature: Option<&NativeCallableSignature>, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let required_parameter_count = + signature.map_or(0, NativeCallableSignature::required_param_count); + let parameters = signature.map_or_else(Vec::new, |signature| { + eval_reflection_native_callable_parameters(class_name, method_name, flags, signature) + }); + let return_type_metadata = signature + .and_then(NativeCallableSignature::return_type) + .and_then(eval_reflection_parameter_type_metadata); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file, + source_location, + attributes, + visibility, + is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + is_final: flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + is_abstract: flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers_from_flags(flags), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } +} + +/// Returns registered generated/AOT method attributes for one reflected method. +pub(super) fn eval_reflection_aot_method_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_method_attributes(declaring_class_name, method_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_method_attributes(runtime_class_name, method_name) +} + +/// Selects the registered native signature for an AOT method-like member. +pub(super) fn eval_reflection_aot_method_signature( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, +) -> Option { + if method_name.eq_ignore_ascii_case("__construct") { + return context.native_constructor_signature(class_name); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0 { + context.native_static_method_signature(class_name, method_name) + } else { + context.native_method_signature(class_name, method_name) + } +} + +/// Builds ReflectionParameter metadata for one registered native AOT signature. +pub(super) fn eval_reflection_native_callable_parameters( + declaring_class_name: &str, + method_name: &str, + flags: u64, + signature: &NativeCallableSignature, +) -> Vec { + let names = eval_reflection_native_callable_parameter_names(signature); + let parameter_count = names.len(); + let parameter_types = eval_reflection_native_callable_parameter_types(signature); + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let defaults = eval_reflection_native_callable_parameter_defaults(signature); + let by_ref_flags = (0..parameter_count) + .map(|index| signature.param_by_ref(index)) + .collect::>(); + let variadic_flags = (0..parameter_count) + .map(|index| signature.param_variadic(index)) + .collect::>(); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method_name.to_ascii_lowercase(), + declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), + magic_scope: None, + attributes: Vec::new(), + flags, + required_parameter_count: signature.required_param_count(), + }; + eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class_name.trim_start_matches('\\')), + Some(&declaring_function), + &names, + &has_type_flags, + ¶meter_types, + ¶meter_attributes, + &defaults, + &by_ref_flags, + &variadic_flags, + &[], + ) +} + +/// Returns declared parameter type metadata for a registered native callable. +pub(super) fn eval_reflection_native_callable_parameter_types( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| signature.param_type(index).cloned()) + .collect() +} + +/// Returns parameter names for a registered native callable, filling missing bridge names. +pub(super) fn eval_reflection_native_callable_parameter_names( + signature: &NativeCallableSignature, +) -> Vec { + (0..signature.param_count()) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Converts registered scalar native defaults into eval constant expressions. +pub(super) fn eval_reflection_native_callable_parameter_defaults( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| { + signature + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + +/// Converts one registered native default into an eval constant expression. +pub(super) fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) -> EvalExpr { + match default { + NativeCallableDefault::Null => EvalExpr::Const(EvalConst::Null), + NativeCallableDefault::Bool(value) => EvalExpr::Const(EvalConst::Bool(*value)), + NativeCallableDefault::Int(value) => EvalExpr::Const(EvalConst::Int(*value)), + NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), + NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), + NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), + NativeCallableDefault::Array(elements) => EvalExpr::Array( + elements + .iter() + .map(eval_reflection_native_callable_default_array_element) + .collect(), + ), + NativeCallableDefault::Object { class_name, args } => EvalExpr::NewObject { + class_name: class_name.clone(), + args: args + .iter() + .map(eval_reflection_native_callable_default_arg) + .collect(), + }, + } +} + +/// Converts one native array-default element into an eval array literal element. +pub(super) fn eval_reflection_native_callable_default_array_element( + element: &NativeCallableArrayDefaultElement, +) -> EvalArrayElement { + let value = eval_reflection_native_callable_default_expr(&element.value); + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::Int(*key)), + value, + }, + Some(NativeCallableArrayDefaultKey::String(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String(key.clone())), + value, + }, + None => EvalArrayElement::Value(value), + } +} + +/// Converts one native object-default constructor argument into an eval call arg. +pub(super) fn eval_reflection_native_callable_default_arg(arg: &NativeCallableObjectDefaultArg) -> EvalCallArg { + let value = eval_reflection_native_callable_default_expr(&arg.value); + match &arg.name { + Some(name) => EvalCallArg::named(name, value), + None => EvalCallArg::positional(value), + } +} + +/// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. +pub(super) fn eval_reflection_aot_property_metadata_if_exists( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let type_metadata = eval_reflection_aot_property_type_metadata( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + let default_value = eval_reflection_aot_property_default_value( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + let attributes = eval_reflection_aot_property_attributes( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + Ok(Some(eval_reflection_aot_property_metadata( + &declaring_class_name, + flags, + attributes, + type_metadata, + default_value, + ))) +} + +/// Returns generated/AOT property access metadata for an exact class/property pair. +pub(in crate::interpreter) fn eval_reflection_aot_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some(eval_reflection_aot_property_access_metadata_from_flags( + declaring_class, + flags, + ))) +} + +/// Returns generated/AOT static property metadata from a class or native parent chain. +pub(in crate::interpreter) fn eval_reflection_aot_static_property_access_metadata( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = + eval_reflection_aot_property_access_metadata(¤t, property_name, values)? + { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + +/// Converts AOT ReflectionProperty flags into access-check metadata. +pub(super) fn eval_reflection_aot_property_access_metadata_from_flags( + declaring_class: String, + flags: u64, +) -> (String, EvalVisibility, EvalVisibility, bool) { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let write_visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + visibility + }; + ( + declaring_class, + visibility, + write_visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + ) +} + +/// Returns registered generated/AOT property type metadata for one reflected property. +pub(super) fn eval_reflection_aot_property_type_metadata( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_type(declaring_class_name, property_name) + .or_else(|| context.native_property_type(runtime_class_name, property_name)) + .as_ref() + .and_then(eval_reflection_parameter_type_metadata) +} + +/// Returns registered generated/AOT property default metadata for one reflected property. +pub(super) fn eval_reflection_aot_property_default_value( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_default(declaring_class_name, property_name) + .or_else(|| context.native_property_default(runtime_class_name, property_name)) + .as_ref() + .map(eval_reflection_native_callable_default_expr) +} + +/// Returns registered generated/AOT property attributes for one reflected property. +pub(super) fn eval_reflection_aot_property_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_property_attributes(declaring_class_name, property_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_property_attributes(runtime_class_name, property_name) +} + +/// Converts AOT property flag metadata into the eval ReflectionProperty shape. +pub(super) fn eval_reflection_aot_property_metadata( + class_name: &str, + flags: u64, + attributes: Vec, + type_metadata: Option, + default_value: Option, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_final = flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0; + let is_abstract = flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0; + let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let is_virtual = flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL != 0; + let mut modifiers = eval_reflection_property_modifiers( + visibility, + None, + is_static, + is_final, + is_abstract, + is_readonly, + is_virtual, + ); + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + modifiers |= 32 | 4096; + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + modifiers |= 2048; + } + let settable_type_metadata = type_metadata.clone(); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, + source_location: None, + attributes, + visibility, + is_static, + is_final, + is_abstract, + is_readonly, + is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, + is_dynamic: false, + modifiers, + type_metadata, + settable_type_metadata, + return_type_metadata: None, + default_value, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs new file mode 100644 index 0000000000..1a15641eb5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs @@ -0,0 +1,637 @@ +//! Purpose: +//! Builds reflected method and property metadata for eval and native class-like targets. +//! +//! Called from: +//! - Reflection owner construction, class member APIs, and property access. +//! +//! Key details: +//! - Trait composition, enum synthetic methods, defaults, and visibility are resolved here. + +use super::*; + +/// Returns method metadata for a method-like member on an eval class-like symbol. +pub(super) fn eval_reflection_method_metadata( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option { + if context.has_class(class_name) || context.has_enum(class_name) { + if let Some((declaring_class, method)) = context.class_method(class_name, method_name) { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(declaring_class.clone()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + &declaring_class, + &method, + None, + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let promoted_parameter_names = eval_reflection_promoted_parameter_names( + &declaring_class, + method.name(), + context, + ); + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class.as_str()), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &promoted_parameter_names, + ); + return Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + }); + } + return eval_reflection_enum_synthetic_method_metadata(class_name, method_name, context); + } + if context.has_interface(class_name) { + return context + .interface_method_requirements(class_name) + .into_iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + EvalVisibility::Public, + method.is_static(), + false, + true, + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(class_name.to_string()), + magic_scope: Some(eval_reflection_method_parameter_magic_scope( + class_name, + method.name(), + &format!("{}::{}", class_name.trim_start_matches('\\'), method.name()), + None, + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(class_name), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &[], + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.to_string()), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: method.is_static(), + is_final: false, + is_abstract: true, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + EvalVisibility::Public, + method.is_static(), + false, + true, + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } + }); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(trait_decl.name().to_string()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + trait_decl.name(), + method, + Some(trait_decl.name()), + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let promoted_parameter_names = + eval_reflection_promoted_trait_parameter_names(trait_decl, method.name()); + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(trait_decl.name()), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &promoted_parameter_names, + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } + }) + }) +} + +/// Builds ReflectionMethod metadata for PHP's enum-provided synthetic methods. +pub(super) fn eval_reflection_enum_synthetic_method_metadata( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option { + let synthetic_name = eval_enum_static_builtin_applies(class_name, method_name, context)?; + let enum_decl = context.enum_decl(class_name)?; + let declaring_class_name = enum_decl.name().trim_start_matches('\\').to_string(); + let flags = eval_reflection_member_flags(EvalVisibility::Public, true, false, false, false); + let (parameter_names, parameter_types, return_type_metadata) = match synthetic_name { + "cases" => ( + Vec::new(), + Vec::new(), + Some(eval_reflection_parameter_type_metadata(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Array], + false, + ))?), + ), + "from" | "tryFrom" => { + let return_type = EvalParameterType::new( + vec![EvalParameterTypeVariant::Class(String::from("static"))], + synthetic_name == "tryFrom", + ); + ( + vec![String::from("value")], + vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String, EvalParameterTypeVariant::Int], + false, + ))], + Some(eval_reflection_parameter_type_metadata(&return_type)?), + ) + } + _ => return None, + }; + let parameter_count = parameter_names.len(); + let parameter_has_types = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_defaults = vec![None; parameter_count]; + let parameter_is_by_ref = vec![false; parameter_count]; + let parameter_is_variadic = vec![false; parameter_count]; + let required_parameter_count = + eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: synthetic_name.to_string(), + declaring_class_name: Some(declaring_class_name.clone()), + magic_scope: None, + attributes: Vec::new(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(&declaring_class_name), + Some(&declaring_function), + ¶meter_names, + ¶meter_has_types, + ¶meter_types, + ¶meter_attributes, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + &[], + ); + Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class_name), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: true, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers(EvalVisibility::Public, true, false, false), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + }) +} + +/// Returns property metadata for a property-like member on an eval class-like symbol. +pub(super) fn eval_reflection_property_metadata( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + if context.has_class(class_name) || context.has_enum(class_name) { + return context.class_property(class_name, property_name).map( + |(declaring_class, property)| { + let default_value = eval_reflection_property_default_value(&property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.set_visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(&property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value, + default_value_trait_origin: property.trait_origin().map(str::to_string), + required_parameter_count: 0, + parameters: Vec::new(), + } + }, + ); + } + if context.has_interface(class_name) { + return context + .interface_property_requirements(class_name) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| { + eval_reflection_interface_property_metadata(class_name.to_string(), &property) + }); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement(class_name, property_name, context) + { + return Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + )); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| { + let default_value = eval_reflection_property_default_value(property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.set_visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value, + default_value_trait_origin: property + .trait_origin() + .map(str::to_string) + .or_else(|| Some(trait_decl.name().to_string())), + required_parameter_count: 0, + parameters: Vec::new(), + } + }) + }) +} + +/// Returns property metadata for a property contract declared on an interface. +pub(super) fn eval_reflection_interface_property_metadata( + declaring_class: String, + property: &EvalInterfaceProperty, +) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: true, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + property.set_visibility(), + false, + false, + true, + false, + true, + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value: None, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + +/// Returns property names that can contribute to `ReflectionClass::getDefaultProperties()`. +pub(super) fn eval_reflection_default_property_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(reflected_name) + || context.has_enum(reflected_name) + || context.has_trait(reflected_name) + || context.has_interface(reflected_name) + { + return Ok(eval_reflection_eval_property_names(reflected_name, context)); + } + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values) +} + +/// Returns eval or generated/AOT property metadata for default-property materialization. +pub(super) fn eval_reflection_default_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { + return Ok(Some(member)); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + reflected_name, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } + eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) +} + +/// Returns eval or generated/AOT metadata for a materialized `ReflectionProperty`. +pub(super) fn eval_reflection_reflected_property_metadata( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(declaring_class, property_name, context) { + return Ok(Some(member)); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } + eval_reflection_aot_property_metadata_if_exists(declaring_class, property_name, context, values) +} + +/// Returns eval-declared property names for reflection APIs that do not use AOT lists. +pub(super) fn eval_reflection_eval_property_names( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if context.has_trait(reflected_name) { + return context.trait_property_names(reflected_name); + } + if context.has_interface(reflected_name) { + return context.interface_property_names(reflected_name); + } + context.class_property_names(reflected_name) +} + +/// Returns property names that can contribute to `ReflectionClass::getStaticProperties()`. +pub(super) fn eval_reflection_static_property_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_property_names(reflected_name, context) + .into_iter() + .filter(|name| { + eval_reflection_property_metadata(reflected_name, name, context) + .is_some_and(|property| property.is_static) + }) + .collect()); + } + let names = + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values)?; + let mut result = Vec::new(); + for name in names { + if eval_reflection_aot_property_metadata_if_exists(reflected_name, &name, context, values)? + .is_some_and(|property| property.is_static) + { + result.push(name); + } + } + Ok(result) +} + +/// Returns eval or generated/AOT property metadata for static-property reflection. +pub(super) fn eval_reflection_static_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_reflected_property_metadata(reflected_name, property_name, context, values) +} + +/// Returns the current eval or generated/AOT static property value. +pub(super) fn eval_reflection_static_property_value( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(member) = + eval_reflection_static_property_metadata(reflected_name, property_name, context, values)? + else { + return Ok(None); + }; + if !member.is_static { + return Ok(None); + } + if eval_reflection_class_like_exists(reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(value) = context.static_property(declaring_class, property_name) { + return Ok(Some(value)); + } + return member + .default_value + .as_ref() + .map(|default| eval_reflection_member_default_value(&member, default, context, values)) + .transpose(); + } + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name); + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_get(reflected_name, property_name) + }) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs b/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs new file mode 100644 index 0000000000..b19efdccd6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs @@ -0,0 +1,1139 @@ +//! Purpose: +//! Materializes common Reflection owner fields and nested member/type objects. +//! +//! Called from: +//! - Reflection owner-specific constructors after resolving metadata. +//! +//! Key details: +//! - Temporary arrays and object handles are transferred through runtime ownership APIs. +//! - Full and shallow class owners avoid recursive metadata expansion. + +use super::*; + +/// Materializes one Reflection owner object and transfers the temporary attribute array. +pub(super) fn eval_reflection_owner_object( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], + method_names: &[String], + property_names: &[String], + parent_class_name: Option<&str>, + parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: Option, + backing_value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object_with_members( + owner_kind, + reflected_name, + attributes, + interface_names, + trait_names, + method_names, + property_names, + parent_class_name, + parameter_metadata, + type_metadata, + settable_type_metadata, + default_value, + default_value_trait_origin, + flags, + modifiers, + method_modifiers, + constant_value, + backing_value, + true, + context, + values, + ) +} + +/// Materializes one Reflection owner object with optional nested class member objects. +pub(super) fn eval_reflection_owner_object_with_members( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], + method_names: &[String], + property_names: &[String], + parent_class_name: Option<&str>, + parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: Option, + backing_value: Option, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = eval_reflection_attribute_array_result( + attributes, + eval_reflection_attribute_target(owner_kind), + context, + values, + )?; + let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; + let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; + let method_names_array = eval_reflection_string_array_result(method_names, values)?; + let property_names_array = eval_reflection_string_array_result(property_names, values)?; + let class_metadata_owner = eval_reflection_owner_uses_class_metadata(owner_kind); + let is_eval_class = class_metadata_owner + && eval_reflection_class_like_exists(reflected_name, context); + let method_objects = if class_metadata_owner && include_class_members { + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { + eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match type_metadata { + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, + None => values.null()?, + } + } else { + values.array_new(0)? + }; + let property_objects = if class_metadata_owner && include_class_members { + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match default_value { + Some(default) => eval_reflection_class_like_default_value( + parent_class_name, + default_value_trait_origin, + default, + context, + values, + )?, + None => values.null()?, + } + } else { + values.array_new(0)? + }; + let parent_class = eval_reflection_related_class_result( + owner_kind, + parent_class_name, + include_class_members, + context, + values, + )?; + let constructor = eval_reflection_constructor_object_result( + owner_kind, + reflected_name, + include_class_members, + context, + values, + )?; + let (constant_value_cell, release_constant_value) = if owner_kind + == EVAL_REFLECTION_OWNER_PROPERTY + { + match settable_type_metadata { + Some(type_metadata) => ( + eval_reflection_type_object_result(type_metadata, values)?, + true, + ), + None => (values.null()?, true), + } + } else { + match constant_value { + Some(value) => (value, false), + None => (values.null()?, true), + } + }; + let (backing_value_cell, release_backing_value) = match backing_value { + Some(value) => (value, false), + None => (values.null()?, true), + }; + let object = values.reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names_array, + trait_names_array, + method_names_array, + property_names_array, + method_objects, + property_objects, + parent_class, + flags, + modifiers, + method_modifiers, + constant_value_cell, + backing_value_cell, + constructor, + )?; + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM + ) { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_method(identity, declaring_class, reflected_name); + } + } else if owner_kind == EVAL_REFLECTION_OWNER_FUNCTION { + let identity = values.object_identity(object)?; + context.register_eval_reflection_function(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(declaring_class) = parent_class_name { + if flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC != 0 { + let identity = values.object_identity(object)?; + context.register_eval_dynamic_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } else { + let identity = values.object_identity(object)?; + context.register_eval_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } + } + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class_constant( + identity, + declaring_class, + reflected_name, + owner_kind, + ); + } + } + values.release(attrs)?; + values.release(interface_names_array)?; + values.release(trait_names_array)?; + values.release(method_names_array)?; + values.release(property_names_array)?; + values.release(method_objects)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constructor)?; + if release_constant_value { + values.release(constant_value_cell)?; + } + if release_backing_value { + values.release(backing_value_cell)?; + } + Ok(object) +} + +/// Builds the `ReflectionClass|false` value stored in parent or declaring-class slots. +pub(super) fn eval_reflection_related_class_result( + owner_kind: u64, + related_class_name: Option<&str>, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(related_class_name) = related_class_name else { + return values.bool_value(false); + }; + if eval_reflection_owner_uses_class_metadata(owner_kind) && include_class_members { + return eval_reflection_full_class_object_result(related_class_name, context, values); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return eval_reflection_shallow_class_object_result(related_class_name, context, values); + } + values.bool_value(false) +} + +/// Builds a full `ReflectionClass` object for parent-class metadata. +pub(super) fn eval_reflection_full_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + EVAL_REFLECTION_OWNER_CLASS, + context, + values, + ); + } + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + let runtime_class_name = class_name.trim_start_matches('\\'); + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )?; + let interface_names = + eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + let attributes = context.native_class_attributes(runtime_class_name); + return eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + runtime_class_name, + &attributes, + &interface_names, + &trait_names, + &method_names, + &property_names, + parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Builds a shallow `ReflectionClass` object for member declaring-class metadata. +pub(super) fn eval_reflection_shallow_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(class_name, values)?; + let attributes = context.native_class_attributes(class_name); + return eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + class_name.trim_start_matches('\\'), + &attributes, + &interface_names, + &trait_names, + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + false, + context, + values, + ); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + false, + context, + values, + ) +} + +/// Returns the generated/AOT parent class name for a reflected class, if any. +pub(super) fn eval_reflection_aot_parent_class_name( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let class_cell = values.string(runtime_class_name)?; + let parent_cell = match values.parent_class_name(class_cell) { + Ok(parent_cell) => parent_cell, + Err(err) => { + values.release(class_cell)?; + return Err(err); + } + }; + values.release(class_cell)?; + let parent_bytes = match values.string_bytes(parent_cell) { + Ok(parent_bytes) => parent_bytes, + Err(err) => { + values.release(parent_cell)?; + return Err(err); + } + }; + values.release(parent_cell)?; + let parent_name = String::from_utf8(parent_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if parent_name.is_empty() { + Ok(None) + } else { + Ok(Some(parent_name)) + } +} + +/// Builds an indexed PHP string array for ReflectionClass metadata names. +pub(super) fn eval_reflection_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.string_array_new(names.len())?; + for name in names { + result = values.string_array_push(result, name)?; + } + Ok(result) +} + +/// Builds a string-keyed PHP associative array from owned string pairs. +pub(super) fn eval_reflection_string_assoc_result( + pairs: Vec<(String, String)>, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(pairs.len())?; + for (key, value) in pairs { + let key = values.string(&key)?; + let value = values.string(&value)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds a name-keyed PHP array of full ReflectionClass objects. +pub(super) fn eval_reflection_class_object_map_result( + names: &[String], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(name)?; + let object = eval_reflection_full_class_object_result(name, context, values)?; + result = values.array_set(result, key, object)?; + } + Ok(result) +} + +/// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. +pub(super) fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { + match owner_kind { + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM => { + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS + } + EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, + EVAL_REFLECTION_OWNER_METHOD => EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD, + EVAL_REFLECTION_OWNER_PROPERTY => EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT, + _ => 0, + } +} + +/// Returns whether a synthetic owner stores `ReflectionClass`-style metadata. +pub(super) fn eval_reflection_owner_uses_class_metadata(owner_kind: u64) -> bool { + matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT + ) +} + +/// Builds an indexed array of populated ReflectionParameter objects. +pub(super) fn eval_reflection_parameter_object_array_result( + parameters: &[EvalReflectionParameterMetadata], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(parameters.len())?; + for parameter in parameters { + let parameter_object = eval_reflection_parameter_object_result(parameter, context, values)?; + let key = values.int(parameter.position as i64)?; + result = values.array_set(result, key, parameter_object)?; + } + Ok(result) +} + +/// Materializes one ReflectionParameter object through the shared reflection helper. +pub(super) fn eval_reflection_parameter_object_result( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = eval_reflection_attribute_array_result( + ¶meter.attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER, + context, + values, + )?; + let declaring_function = match parameter.declaring_function.as_ref() { + Some(metadata) => { + eval_reflection_declaring_function_object_result(metadata, context, values)? + } + None => values.null()?, + }; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let parent_class = match parameter.declaring_class_name.as_deref() { + Some(declaring_class_name) => { + eval_reflection_shallow_class_object_result(declaring_class_name, context, values)? + } + None => values.null()?, + }; + let type_value = match parameter.type_metadata.as_ref() { + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, + None => values.null()?, + }; + let class_value = eval_reflection_parameter_class_value(parameter, context, values)?; + let default_value = eval_reflection_parameter_default_value(parameter, context, values)?; + let default_value_constant_name = match parameter.default_value_constant_name.as_deref() { + Some(name) => values.string(name)?, + None => values.null()?, + }; + let constructor = values.null()?; + let flags = eval_reflection_parameter_flags(parameter); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_PARAMETER, + ¶meter.name, + attrs, + declaring_function, + trait_names, + method_names, + property_names, + type_value, + default_value, + parent_class, + flags, + parameter.position as u64, + 0, + default_value_constant_name, + class_value, + constructor, + )?; + values.release(attrs)?; + values.release(declaring_function)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; + values.release(type_value)?; + values.release(default_value)?; + values.release(parent_class)?; + values.release(default_value_constant_name)?; + values.release(class_value)?; + values.release(constructor)?; + Ok(object) +} + +/// Materializes the legacy ReflectionParameter::getClass() value for known named object types. +pub(super) fn eval_reflection_parameter_class_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_reflection_parameter_class_name(parameter) { + Some(class_name) => eval_reflection_shallow_class_object_result(class_name, context, values), + None => values.null(), + } +} + +/// Returns the retained object type name used by ReflectionParameter::getClass(). +pub(super) fn eval_reflection_parameter_class_name( + parameter: &EvalReflectionParameterMetadata, +) -> Option<&str> { + match ¶meter.type_metadata.as_ref()?.kind { + EvalReflectionParameterTypeKind::Named(named_type) if !named_type.is_builtin => { + Some(named_type.name.as_str()) + } + _ => None, + } +} + +/// Materializes one ReflectionParameter default using declaring class and magic scopes. +pub(super) fn eval_reflection_parameter_default_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = parameter.default_value.as_ref() else { + return values.null(); + }; + if let Some(class_name) = parameter.declaring_class_name.as_deref() { + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(class_name.to_string()); + } + let magic_scope = parameter + .declaring_function + .as_ref() + .and_then(|function| function.magic_scope.as_ref()); + if let Some(magic_scope) = magic_scope { + context.push_callable_magic_scope( + &magic_scope.function_name, + &magic_scope.method_name, + magic_scope.class_name.as_deref(), + magic_scope.trait_name.as_deref(), + ); + } + let result = eval_method_parameter_default(default, context, values); + if magic_scope.is_some() { + context.pop_magic_scope(); + } + if parameter.declaring_class_name.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + result +} + +/// Evaluates one reflected property default with its declaring class-like magic scope. +pub(super) fn eval_reflection_member_default_value( + member: &EvalReflectionMemberMetadata, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_class_like_default_value( + member.declaring_class_name.as_deref(), + member.default_value_trait_origin.as_deref(), + default, + context, + values, + ) +} + +/// Evaluates one class-like default expression with PHP `__CLASS__` and `__TRAIT__`. +pub(super) fn eval_reflection_class_like_default_value( + declaring_class: Option<&str>, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = declaring_class else { + return eval_method_parameter_default(default, context, values); + }; + let trait_name = + trait_origin.or_else(|| context.has_trait(declaring_class).then_some(declaring_class)); + context.push_class_like_member_magic_scope(declaring_class, trait_name); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + result +} + +/// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. +pub(super) fn eval_reflection_declaring_function_object_result( + metadata: &EvalReflectionDeclaringFunctionMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let owner_kind = if metadata.declaring_class_name.is_some() { + EVAL_REFLECTION_OWNER_METHOD + } else { + EVAL_REFLECTION_OWNER_FUNCTION + }; + let method_modifiers = if metadata.declaring_class_name.is_some() { + eval_reflection_method_modifiers_from_flags(metadata.flags) + } else { + 0 + }; + eval_reflection_owner_object( + owner_kind, + &metadata.name, + &metadata.attributes, + &[], + &[], + &[], + &[], + metadata.declaring_class_name.as_deref(), + &[], + None, + None, + None, + None, + metadata.flags, + metadata.required_parameter_count as u64, + method_modifiers, + None, + None, + context, + values, + ) +} + +/// Materializes one parameter ReflectionType object through the shared reflection helper. +pub(super) fn eval_reflection_type_object_result( + type_metadata: &EvalReflectionParameterTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => { + eval_reflection_named_type_object_result(named_type, values) + } + EvalReflectionParameterTypeKind::Union(union_type) => { + eval_reflection_union_type_object_result(union_type, values) + } + EvalReflectionParameterTypeKind::Intersection(intersection_type) => { + eval_reflection_intersection_type_object_result(intersection_type, values) + } + } +} + +/// Materializes one ReflectionNamedType object through the shared reflection helper. +pub(super) fn eval_reflection_named_type_object_result( + type_metadata: &EvalReflectionNamedTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let flags = eval_reflection_named_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_NAMED_TYPE, + &type_metadata.name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + parent_class, + flags, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Materializes one ReflectionUnionType object through the shared reflection helper. +pub(super) fn eval_reflection_union_type_object_result( + type_metadata: &EvalReflectionUnionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let flags = eval_reflection_union_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_UNION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + flags, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Materializes one ReflectionIntersectionType object through the shared reflection helper. +pub(super) fn eval_reflection_intersection_type_object_result( + type_metadata: &EvalReflectionIntersectionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + 0, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Builds an indexed array of populated ReflectionNamedType objects. +pub(super) fn eval_reflection_named_type_object_array_result( + types: &[EvalReflectionNamedTypeMetadata], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(types.len())?; + for (position, type_metadata) in types.iter().enumerate() { + let type_object = eval_reflection_named_type_object_result(type_metadata, values)?; + let key = values.int(position as i64)?; + result = values.array_set(result, key, type_object)?; + } + Ok(result) +} + +/// Builds the `ReflectionMethod|null` value stored in ReflectionClass::__constructor. +pub(super) fn eval_reflection_constructor_object_result( + owner_kind: u64, + class_name: &str, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_reflection_owner_uses_class_metadata(owner_kind) || !include_class_members { + return values.null(); + } + let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { + if !eval_reflection_class_like_exists(class_name, context) { + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, + "__construct", + context, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ); + } + } + return values.null(); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ) +} + +/// Materializes one eval-backed ReflectionMethod or ReflectionProperty object. +pub(super) fn eval_reflection_member_object_result( + owner_kind: u64, + reflected_name: &str, + member: &EvalReflectionMemberMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut flags = eval_reflection_member_flags( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + member.is_readonly, + ); + if member.default_value.is_some() { + flags |= EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE; + } + if member.is_promoted { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROMOTED; + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 4096) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET | EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && (member.modifiers & 2048) != 0 + && !member.is_readonly + { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET; + } + if member.is_dynamic { + flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + flags |= eval_reflection_callable_flags(&member.attributes); + } + let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.required_parameter_count as u64 + } else { + member.modifiers + }; + let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.modifiers + } else { + 0 + }; + eval_reflection_owner_object( + owner_kind, + reflected_name, + &member.attributes, + &[], + &[], + &[], + &[], + member.declaring_class_name.as_deref(), + &member.parameters, + member.type_metadata.as_ref(), + member.settable_type_metadata.as_ref(), + member.default_value.as_ref(), + member.default_value_trait_origin.as_deref(), + flags, + owner_modifiers, + method_modifiers, + None, + None, + context, + values, + ) +} + +/// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. +pub(super) fn eval_reflection_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + filter: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let Some(member) = eval_reflection_member_metadata(owner_kind, class_name, name, context) + else { + continue; + }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let member_object = + eval_reflection_member_object_result(owner_kind, name, &member, context, values)?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} + +/// Builds an indexed array of AOT ReflectionMethod or ReflectionProperty objects for a class. +pub(super) fn eval_reflection_aot_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + filter: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let member = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, name, context, values, + )? + } else { + eval_reflection_native_interface_property_requirement(class_name, name, context) + .map(|(declaring_class, property)| { + eval_reflection_interface_property_metadata(declaring_class, &property) + }) + .or(eval_reflection_aot_property_metadata_if_exists( + class_name, name, context, values, + )?) + }; + let Some(member) = member else { + continue; + }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let reflected_name = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + name.to_ascii_lowercase() + } else { + name.clone() + }; + let member_object = eval_reflection_member_object_result( + owner_kind, + &reflected_name, + &member, + context, + values, + )?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs b/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs new file mode 100644 index 0000000000..7ae21c4cec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs @@ -0,0 +1,280 @@ +//! Purpose: +//! Resolves ReflectionParameter constructor selectors and materializes parameters. +//! +//! Called from: +//! - `crate::interpreter::reflection::eval_reflection_owner_new_object()`. +//! +//! Key details: +//! - Function and method targets accept PHP-compatible name or position selectors. + +use super::*; + +/// Builds an eval-backed `ReflectionParameter` object for a function or method parameter. +pub(super) fn eval_reflection_parameter_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("function"), String::from("param")], + evaluated_args, + )?; + let selector = eval_reflection_parameter_selector(args[1], values)?; + let Some(parameter) = + eval_reflection_parameter_constructor_metadata(args[0], selector.clone(), context, values)? + else { + return eval_reflection_parameter_constructor_error(args[0], &selector, context, values); + }; + eval_reflection_parameter_object_result(¶meter, context, values).map(Some) +} + +/// Throws the PHP constructor error for eval-backed `ReflectionParameter` misses. +pub(super) fn eval_reflection_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_constructor_error(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_constructor_error( + target, selector, context, values, + ); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Throws the PHP constructor error for eval-backed function parameter misses. +pub(super) fn eval_reflection_function_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if context.function(&lookup_name).is_some() || context.native_function(&lookup_name).is_some() { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws the PHP constructor error for eval-backed method parameter misses. +pub(super) fn eval_reflection_method_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if eval_reflection_class_like_exists(&reflected_name, context) { + let Some(reflected_method_name) = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + &method_name, + context, + ) else { + return eval_throw_reflection_exception( + &format!("Method {}::{}() does not exist", reflected_name, method_name), + context, + values, + ); + }; + if eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + return Err(EvalStatus::RuntimeFatal); + } + if eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &method_name, + context, + values, + )? + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws PHP's selector-specific ReflectionParameter constructor error. +pub(super) fn eval_reflection_parameter_selector_error( + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match selector { + EvalReflectionParameterSelector::Name(_) => eval_throw_reflection_exception( + "The parameter specified by its name could not be found", + context, + values, + ), + EvalReflectionParameterSelector::Position(position) if *position < 0 => { + eval_throw_value_error( + "ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0", + context, + values, + ) + } + EvalReflectionParameterSelector::Position(_) => eval_throw_reflection_exception( + "The parameter specified by its offset could not be found", + context, + values, + ), + } +} + +/// Resolves `ReflectionParameter` constructor target metadata. +pub(super) fn eval_reflection_parameter_constructor_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_metadata(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_metadata(target, selector, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds selected parameter metadata for an eval or native free function. +pub(super) fn eval_reflection_function_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name).cloned() { + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + Ok(None) +} + +/// Builds selected parameter metadata for an eval or generated/AOT method target. +pub(super) fn eval_reflection_method_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let member = if eval_reflection_class_like_exists(&reflected_name, context) { + let reflected_method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + &method_name, + context, + ); + let Some(reflected_method_name) = reflected_method_name else { + return Ok(None); + }; + let Some(method) = + eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + else { + return Ok(None); + }; + method + } else { + let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &method_name, + context, + values, + )? + else { + return Ok(None); + }; + member + }; + Ok(eval_reflection_parameter_for_selector( + member.parameters, + selector, + )) +} + +/// Converts a `ReflectionParameter` selector runtime value to a supported selector. +pub(super) fn eval_reflection_parameter_selector( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_STRING => { + eval_reflection_string_arg(value, values).map(EvalReflectionParameterSelector::Name) + } + EVAL_TAG_INT => { + eval_int_value(value, values).map(EvalReflectionParameterSelector::Position) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Selects a parameter by PHP name or zero-based position. +pub(super) fn eval_reflection_parameter_for_selector( + parameters: Vec, + selector: EvalReflectionParameterSelector, +) -> Option { + match selector { + EvalReflectionParameterSelector::Name(name) => parameters + .into_iter() + .find(|parameter| parameter.name == name), + EvalReflectionParameterSelector::Position(position) if position >= 0 => { + parameters.into_iter().nth(position as usize) + } + EvalReflectionParameterSelector::Position(_) => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs new file mode 100644 index 0000000000..dac7ae2cf7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs @@ -0,0 +1,348 @@ +//! Purpose: +//! Builds reflected parameter lists, defaults, magic scopes, and type metadata. +//! +//! Called from: +//! - Function, method, property-hook, and ReflectionParameter construction paths. +//! +//! Key details: +//! - Promoted properties, nullable composites, variadics, and defaults remain aligned by index. + +use super::*; + +/// Returns PHP's required parameter count for a reflected method signature. +pub(super) fn eval_reflection_required_parameter_count( + defaults: &[Option], + variadic_flags: &[bool], +) -> usize { + let fixed_count = variadic_flags + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(defaults.len()); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Builds PHP magic scope metadata for a reflected function parameter default. +pub(super) fn eval_reflection_function_parameter_magic_scope( + function_name: &str, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: function_name.to_string(), + class_name: None, + trait_name: None, + } +} + +/// Builds PHP magic scope metadata for a reflected method parameter default. +pub(super) fn eval_reflection_method_parameter_magic_scope( + class_name: &str, + function_name: &str, + method_name: &str, + trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: Some(class_name.trim_start_matches('\\').to_string()), + trait_name: trait_name.map(|trait_name| trait_name.trim_start_matches('\\').to_string()), + } +} + +/// Builds PHP magic scope metadata for an eval method's reflected parameter default. +pub(super) fn eval_reflection_eval_method_parameter_magic_scope( + class_name: &str, + method: &EvalClassMethod, + fallback_trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + eval_reflection_method_parameter_magic_scope( + class_name, + method.magic_function_name(), + &method.magic_method_name(class_name), + method + .trait_origin() + .or(fallback_trait_name), + ) +} + +/// Builds parameter reflection metadata from eval parameter names and type flags. +pub(super) fn eval_reflection_parameters_from_names_and_type_flags( + declaring_class_name: Option<&str>, + declaring_function: Option<&EvalReflectionDeclaringFunctionMetadata>, + names: &[String], + has_type_flags: &[bool], + parameter_types: &[Option], + parameter_attributes: &[Vec], + defaults: &[Option], + by_ref_flags: &[bool], + variadic_flags: &[bool], + promoted_parameter_names: &[String], +) -> Vec { + names + .iter() + .enumerate() + .map(|(position, name)| { + let has_type = has_type_flags.get(position).copied().unwrap_or(false); + let default_value = defaults.get(position).and_then(Clone::clone); + let default_value_constant_name = default_value + .as_ref() + .and_then(eval_reflection_default_constant_name); + let type_metadata = parameter_types + .get(position) + .and_then(Option::as_ref) + .and_then(eval_reflection_parameter_type_metadata) + .filter(|_| has_type); + let is_array_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + EvalReflectionParameterMetadata { + name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.cloned(), + attributes: parameter_attributes + .get(position) + .cloned() + .unwrap_or_default(), + position, + is_optional: defaults.get(position).is_some_and(Option::is_some) + || variadic_flags.get(position).copied().unwrap_or(false), + is_variadic: variadic_flags.get(position).copied().unwrap_or(false), + is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), + has_type, + allows_null: eval_reflection_parameter_allows_null( + has_type, + type_metadata.as_ref(), + default_value.as_ref(), + ), + is_array_type, + is_callable_type, + type_metadata, + default_value, + default_value_constant_name, + } + }) + .collect() +} + +/// Returns whether retained parameter metadata is one named type with the requested name. +pub(super) fn eval_reflection_parameter_has_named_type( + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + expected_name: &str, +) -> bool { + matches!( + type_metadata, + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named) + }) if named.name.eq_ignore_ascii_case(expected_name) + ) +} + +/// Returns PHP's ReflectionParameter default-constant name for retained eval defaults. +pub(super) fn eval_reflection_default_constant_name(default: &EvalExpr) -> Option { + match default { + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{}::{}", class_name, constant)), + _ => None, + } +} + +/// Builds ReflectionParameter metadata for eval-declared or native free functions. +pub(super) fn eval_reflection_function_parameters( + function_name: &str, + names: &[String], + function_attributes: Vec, + parameter_attributes: &[Vec], + parameter_types: &[Option], + defaults: &[Option], + by_ref_flags: &[bool], + variadic_flags: &[bool], +) -> Vec { + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let flags = eval_reflection_callable_flags(&function_attributes); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: function_name.to_string(), + declaring_class_name: None, + magic_scope: Some(eval_reflection_function_parameter_magic_scope(function_name)), + attributes: function_attributes, + flags, + required_parameter_count: eval_reflection_required_parameter_count( + defaults, + variadic_flags, + ), + }; + eval_reflection_parameters_from_names_and_type_flags( + None, + Some(&declaring_function), + names, + &has_type_flags, + parameter_types, + parameter_attributes, + defaults, + by_ref_flags, + variadic_flags, + &[], + ) +} + +/// Returns promoted constructor parameter names for one eval class method. +pub(super) fn eval_reflection_promoted_parameter_names( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if !method_name.eq_ignore_ascii_case("__construct") { + return Vec::new(); + } + context + .class(class_name) + .map(eval_reflection_promoted_property_names) + .unwrap_or_default() +} + +/// Returns promoted constructor parameter names for one eval trait method. +pub(super) fn eval_reflection_promoted_trait_parameter_names( + trait_decl: &EvalTrait, + method_name: &str, +) -> Vec { + if method_name.eq_ignore_ascii_case("__construct") { + eval_reflection_promoted_property_names_from_slice(trait_decl.properties()) + } else { + Vec::new() + } +} + +/// Returns property names marked as constructor-promoted in one eval class. +pub(super) fn eval_reflection_promoted_property_names(class: &EvalClass) -> Vec { + eval_reflection_promoted_property_names_from_slice(class.properties()) +} + +/// Returns property names marked as constructor-promoted in one property list. +pub(super) fn eval_reflection_promoted_property_names_from_slice( + properties: &[EvalClassProperty], +) -> Vec { + properties + .iter() + .filter(|property| property.is_promoted()) + .map(|property| property.name().to_string()) + .collect() +} + +/// Converts eval parameter type metadata into the supported ReflectionType subset. +pub(super) fn eval_reflection_parameter_type_metadata( + parameter_type: &EvalParameterType, +) -> Option { + let variants = parameter_type.variants(); + if variants.is_empty() { + return None; + } + let allows_null = parameter_type.allows_null(); + let mut types = variants + .iter() + .map(|variant| eval_reflection_named_type_variant_metadata(variant, false)) + .collect::>>()?; + if types.len() == 1 { + let mut named = types.pop()?; + named.allows_null = allows_null; + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named), + }); + } + if parameter_type.is_intersection() { + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Intersection( + EvalReflectionIntersectionTypeMetadata { types }, + ), + }); + } + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Union(EvalReflectionUnionTypeMetadata { + types, + allows_null, + }), + }) +} + +/// Returns PHP's `ReflectionParameter::allowsNull()` value for retained metadata. +pub(super) fn eval_reflection_parameter_allows_null( + has_type: bool, + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, +) -> bool { + !has_type + || default_value.is_some_and(|default| matches!(default, EvalExpr::Const(EvalConst::Null))) + || type_metadata.is_some_and(eval_reflection_type_allows_null) +} + +/// Returns whether one retained ReflectionType metadata value accepts null. +pub(super) fn eval_reflection_type_allows_null(type_metadata: &EvalReflectionParameterTypeMetadata) -> bool { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => named_type.allows_null, + EvalReflectionParameterTypeKind::Union(union_type) => union_type.allows_null, + EvalReflectionParameterTypeKind::Intersection(_) => false, + } +} + +/// Converts one eval parameter type variant into `ReflectionNamedType` metadata. +pub(super) fn eval_reflection_named_type_variant_metadata( + variant: &EvalParameterTypeVariant, + allows_null: bool, +) -> Option { + match variant { + EvalParameterTypeVariant::Array => { + Some(eval_reflection_builtin_named_type("array", allows_null)) + } + EvalParameterTypeVariant::Bool => { + Some(eval_reflection_builtin_named_type("bool", allows_null)) + } + EvalParameterTypeVariant::Callable => { + Some(eval_reflection_builtin_named_type("callable", allows_null)) + } + EvalParameterTypeVariant::Class(name) => Some(EvalReflectionNamedTypeMetadata { + name: name.clone(), + allows_null, + is_builtin: false, + }), + EvalParameterTypeVariant::Float => { + Some(eval_reflection_builtin_named_type("float", allows_null)) + } + EvalParameterTypeVariant::Int => { + Some(eval_reflection_builtin_named_type("int", allows_null)) + } + EvalParameterTypeVariant::Iterable => { + Some(eval_reflection_builtin_named_type("iterable", allows_null)) + } + EvalParameterTypeVariant::Mixed => Some(eval_reflection_builtin_named_type("mixed", true)), + EvalParameterTypeVariant::Never => Some(eval_reflection_builtin_named_type("never", false)), + EvalParameterTypeVariant::Object => { + Some(eval_reflection_builtin_named_type("object", allows_null)) + } + EvalParameterTypeVariant::String => { + Some(eval_reflection_builtin_named_type("string", allows_null)) + } + EvalParameterTypeVariant::Void => Some(eval_reflection_builtin_named_type("void", false)), + } +} + +/// Builds metadata for one builtin eval `ReflectionNamedType`. +pub(super) fn eval_reflection_builtin_named_type( + name: &str, + allows_null: bool, +) -> EvalReflectionNamedTypeMetadata { + EvalReflectionNamedTypeMetadata { + name: name.to_string(), + allows_null, + is_builtin: true, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/property_access.rs b/crates/elephc-magician/src/interpreter/reflection/property_access.rs new file mode 100644 index 0000000000..b7862a05a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/property_access.rs @@ -0,0 +1,421 @@ +//! Purpose: +//! Reads, writes, and validates reflected instance, static, and dynamic properties. +//! +//! Called from: +//! - ReflectionProperty value, raw-value, and initialized-state APIs. +//! +//! Key details: +//! - Eval and AOT storage paths enforce declaring-class and object compatibility. + +use super::*; + +/// Reads one eval instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.has_get_hook() + && !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + validate_eval_reflection_property_write(declaring_class, &property, context)?; + if property.has_set_hook() { + if !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Reads one generated/AOT instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_aot_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_get(object, property_name) + }) +} + +/// Writes one generated/AOT instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_aot_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_set(object, property_name, value) + }) +} + +/// Checks one generated/AOT instance property initialization marker through ReflectionProperty. +pub(super) fn eval_reflection_aot_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_is_initialized(object, property_name) + }) +} + +/// Checks one generated/AOT static property initialization marker through ReflectionProperty. +pub(super) fn eval_reflection_aot_static_property_is_initialized( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_is_initialized(declaring_class, property_name) + }) +} + +/// Verifies a generated/AOT ReflectionProperty instance target is compatible. +pub(super) fn eval_reflection_aot_instance_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if is_instance { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether one eval instance property is initialized for ReflectionProperty. +pub(super) fn eval_reflection_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Ok(true); + } + let identity = values.object_identity(object)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + Ok(context.dynamic_property_is_initialized(identity, &storage_property_name)) +} + +/// Reads one eval instance property through ReflectionProperty raw-storage semantics. +pub(super) fn eval_reflection_instance_property_get_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty raw-storage semantics. +pub(super) fn eval_reflection_instance_property_set_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_reflection_property_write(declaring_class, &property, context)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Reads a public dynamic property through ReflectionProperty semantics. +pub(super) fn eval_reflection_dynamic_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_get(object, property_name) +} + +/// Writes a public dynamic property through ReflectionProperty semantics. +pub(super) fn eval_reflection_dynamic_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_set(object, property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, property_name); + Ok(()) +} + +/// Returns whether a public dynamic property currently exists on the target object. +pub(super) fn eval_reflection_dynamic_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + eval_reflection_object_dynamic_property_exists(object, property_name, values) +} + +/// Validates the object argument used by dynamic ReflectionProperty operations. +pub(super) fn eval_reflection_dynamic_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object_class_name = eval_reflection_object_class_name(object, context, values)?; + if eval_reflection_class_like_exists(declaring_class, context) { + if context.class_is_a(&object_class_name, declaring_class, false) { + return Ok(()); + } + } else if object_class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(declaring_class.trim_start_matches('\\')) + { + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Validates the object argument shared by non-mutating ReflectionProperty instance APIs. +pub(super) fn eval_reflection_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Resolves and validates the object/property pair targeted by ReflectionProperty. +pub(super) fn eval_reflection_instance_property_target( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, EvalClassProperty), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + let (_, property) = context + .class_own_property(declaring_class, property_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if property.is_static() || property.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((object_class_name, property)) +} + +/// Rejects writes to eval properties ReflectionProperty is not allowed to mutate. +pub(super) fn validate_eval_reflection_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Throws PHP's `ReflectionException` for invalid static-property writes. +pub(super) fn eval_reflection_static_property_missing_for_set( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_throw_reflection_exception( + &format!( + "Class {} does not have a property named {}", + reflected_name, property_name + ), + context, + values, + ) +} + +/// Returns ReflectionProperty default metadata for concrete eval properties. +pub(super) fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { + if let Some(default) = property.default() { + return Some(default.clone()); + } + if property.is_abstract() || property.property_type().is_some() { + return None; + } + Some(EvalExpr::Const(EvalConst::Null)) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs b/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs new file mode 100644 index 0000000000..5c8aa97435 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs @@ -0,0 +1,393 @@ +//! Purpose: +//! Normalizes ReflectionProperty arguments and materializes property hook metadata. +//! +//! Called from: +//! - Property Reflection APIs before runtime reads, writes, or hook inspection. +//! +//! Key details: +//! - Static defaults, raw-value arguments, and synthetic get/set hook methods converge here. + +use super::*; + +/// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. +pub(super) fn eval_reflection_static_property_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("name"), String::from("default")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let property_name = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((property_name, bound_args[1])) +} + +/// Binds the optional `ReflectionProperty::getValue()` object argument. +pub(super) fn eval_reflection_property_get_value_arg( + evaluated_args: Vec, +) -> Result, EvalStatus> { + let params = [String::from("object")]; + let mut bound_arg = None; + for arg in evaluated_args { + if let Some(name) = arg.name { + if params.iter().all(|param| param != &name) || bound_arg.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_arg = Some(arg.value); + } else if bound_arg.is_none() { + bound_arg = Some(arg.value); + } else { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(bound_arg) +} + +/// Binds `ReflectionProperty::setValue()` arguments while allowing PHP's static shorthand. +pub(super) fn eval_reflection_property_set_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("objectOrValue"), String::from("value")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let object_or_value = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((object_or_value, bound_args[1])) +} + +/// Binds the required object argument for `ReflectionProperty::getRawValue()`. +pub(super) fn eval_reflection_property_raw_value_arg( + evaluated_args: Vec, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + Ok(args[0]) +} + +/// Binds the object and value arguments for `ReflectionProperty::setRawValue()`. +pub(super) fn eval_reflection_property_set_raw_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("value")], + evaluated_args, + )?; + Ok((args[0], args[1])) +} + +/// Returns the eval property metadata eligible for ReflectionProperty hook APIs. +pub(super) fn eval_reflection_property_for_hooks( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_property(declaring_class, property_name); + } + context + .trait_decl(declaring_class) + .and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (trait_decl.name().to_string(), property.clone())) + }) + .or_else(|| { + context + .interface_property_requirements(declaring_class) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract( + property.requires_get(), + property.requires_set(), + ); + (declaring_class.to_string(), property) + }) + }) + .or_else(|| { + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + .map(|(owner, property)| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + (owner, property) + }) + }) +} + +/// Returns one generated/AOT interface property contract registered for eval reflection. +pub(super) fn eval_reflection_native_interface_property_requirement( + interface_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalInterfaceProperty)> { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .find(|(_, property)| property.name() == property_name) +} + +/// Returns generated/AOT interface property names registered for eval reflection. +pub(super) fn eval_reflection_native_interface_property_names( + interface_name: &str, + context: &ElephcEvalContext, +) -> Vec { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .map(|(_, property)| property.name().to_string()) + .collect() +} + +/// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. +pub(super) fn eval_reflection_property_hook_arg( + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("type")], evaluated_args)?; + eval_reflection_property_hook_type(args[0], context, values) +} + +/// Converts one synthetic `PropertyHookType` object into an eval reflection hook kind. +pub(super) fn eval_reflection_property_hook_type( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(value)?; + if !context.dynamic_object_is_class(identity, "PropertyHookType") { + return Err(EvalStatus::RuntimeFatal); + } + let hook_value = values.property_get(value, "value")?; + match eval_reflection_string_arg(hook_value, values)?.as_str() { + "get" => Ok(EvalReflectionPropertyHook::Get), + "set" => Ok(EvalReflectionPropertyHook::Set), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns concrete hook kinds declared on one eval property. +pub(super) fn eval_reflection_property_hook_kinds( + property: &EvalClassProperty, +) -> Vec { + let mut hooks = Vec::new(); + if property.has_get_hook() || property.requires_get_hook() { + hooks.push(EvalReflectionPropertyHook::Get); + } + if property.has_set_hook() || property.requires_set_hook() { + hooks.push(EvalReflectionPropertyHook::Set); + } + hooks +} + +/// Returns whether one eval property exposes the requested concrete hook. +pub(super) fn eval_reflection_property_has_hook( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> bool { + match hook { + EvalReflectionPropertyHook::Get => property.has_get_hook() || property.requires_get_hook(), + EvalReflectionPropertyHook::Set => property.has_set_hook() || property.requires_set_hook(), + } +} + +/// Builds PHP's string-keyed ReflectionMethod map returned by `getHooks()`. +pub(super) fn eval_reflection_property_hook_method_array( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let hooks = eval_reflection_property_hook_kinds(property); + let mut result = values.assoc_new(hooks.len())?; + for hook in hooks { + let key = values.string(hook.key())?; + let method = eval_reflection_property_hook_method_object( + declaring_class, + property, + hook, + context, + values, + )?; + result = values.array_set(result, key, method)?; + } + Ok(result) +} + +/// Materializes a ReflectionMethod object for one concrete property hook. +pub(super) fn eval_reflection_property_hook_method_object( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_property_hook_method_metadata(declaring_class, property, hook); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &hook.reflected_method_name(property.name()), + &metadata, + context, + values, + ) +} + +/// Builds ReflectionMethod metadata for one eval property hook accessor. +pub(super) fn eval_reflection_property_hook_method_metadata( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> EvalReflectionMemberMetadata { + let parameters = eval_reflection_property_hook_parameters(declaring_class, property, hook); + let required_parameter_count = parameters.len(); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class.to_string()), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: property.visibility(), + is_static: false, + is_final: false, + is_abstract: property.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + property.visibility(), + false, + false, + property.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata: eval_reflection_property_hook_return_type(property, hook), + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } +} + +/// Builds the synthetic setter parameter metadata exposed by PHP hook reflection. +pub(super) fn eval_reflection_property_hook_parameters( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Vec { + if !matches!(hook, EvalReflectionPropertyHook::Set) { + return Vec::new(); + } + let type_metadata = property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata); + let has_type = type_metadata.is_some(); + let is_array_type = eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: hook.reflected_method_name(property.name()), + declaring_class_name: Some(declaring_class.to_string()), + magic_scope: None, + attributes: Vec::new(), + flags: eval_reflection_member_flags(property.visibility(), false, false, false, false), + required_parameter_count: 1, + }; + vec![EvalReflectionParameterMetadata { + name: "value".to_string(), + declaring_class_name: Some(declaring_class.to_string()), + declaring_function: Some(declaring_function), + attributes: Vec::new(), + position: 0, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + is_promoted: false, + has_type, + allows_null: type_metadata + .as_ref() + .is_some_and(eval_reflection_type_allows_null), + is_array_type, + is_callable_type, + type_metadata, + default_value: None, + default_value_constant_name: None, + }] +} + +/// Returns the ReflectionMethod return type metadata for a property hook. +pub(super) fn eval_reflection_property_hook_return_type( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Option { + match hook { + EvalReflectionPropertyHook::Get => property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + EvalReflectionPropertyHook::Set => Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + "void", false, + )), + }), + } +} + +/// Maps PHP-visible property-hook method names back to eval's synthetic method names. +pub(super) fn eval_reflection_property_hook_synthetic_method_name(method_name: &str) -> Option { + let body = method_name.strip_prefix('$')?; + let (property_name, hook_name) = body.rsplit_once("::")?; + match hook_name { + "get" => Some(EvalReflectionPropertyHook::Get.synthetic_method_name(property_name)), + "set" => Some(EvalReflectionPropertyHook::Set.synthetic_method_name(property_name)), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/return_type_compat.rs b/crates/elephc-magician/src/interpreter/return_type_compat.rs new file mode 100644 index 0000000000..464d241938 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/return_type_compat.rs @@ -0,0 +1,629 @@ +//! Purpose: +//! Validates type compatibility for eval-declared method and property signatures. +//! This keeps method parameter contravariance, return covariance, and property +//! type invariance checks out of the statement dispatcher. +//! +//! Called from: +//! - `crate::interpreter::statements` while registering eval class-like declarations. +//! +//! Key details: +//! - `self`, `parent`, and `static` are resolved relative to the declaration owner. +//! - Parameter checks reverse the return-type relation because PHP parameters are contravariant. +//! - Pending class declarations are checked before they are registered in the eval context. + +use super::*; + +/// Returns whether an implementation can accept every required declared parameter type. +pub(super) fn method_parameter_type_signature_accepts( + implementation_types: &[Option], + implementation_variadics: &[bool], + implementation_owner: &str, + required_types: &[Option], + required_variadics: &[bool], + required_owner: &str, + required_param_count: usize, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + (0..required_param_count).all(|position| { + let implementation_type = method_signature_effective_parameter_type( + implementation_types, + implementation_variadics, + position, + ); + let required_type = method_signature_effective_parameter_type( + required_types, + required_variadics, + position, + ); + eval_parameter_type_accepts( + implementation_type, + implementation_owner, + required_type, + required_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether a method preserves the required declared return type contract. +pub(super) fn method_return_type_signature_accepts( + implementation_type: Option<&EvalParameterType>, + implementation_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let Some(required_type) = required_type else { + return true; + }; + implementation_type.is_some_and(|implementation_type| { + eval_return_type_accepts( + required_type, + required_owner, + implementation_type, + implementation_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether a redeclared property preserves PHP's invariant type contract. +pub(super) fn property_type_signature_matches( + property_type: Option<&EvalParameterType>, + property_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (property_type, required_type) { + (None, None) => true, + (Some(property_type), Some(required_type)) => eval_property_type_matches( + property_type, + property_owner, + required_type, + required_owner, + pending_class, + context, + ), + _ => false, + } +} + +/// Returns the parameter type that applies at one source-order argument position. +fn method_signature_effective_parameter_type<'a>( + parameter_types: &'a [Option], + variadics: &[bool], + position: usize, +) -> Option<&'a EvalParameterType> { + if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { + if position >= variadic_index { + return parameter_types + .get(variadic_index) + .and_then(Option::as_ref); + } + } + parameter_types.get(position).and_then(Option::as_ref) +} + +/// Returns whether two property type declarations are PHP-equivalent. +fn eval_property_type_matches( + property_type: &EvalParameterType, + property_owner: &str, + required_type: &EvalParameterType, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + property_type.allows_null() == required_type.allows_null() + && property_type.is_intersection() == required_type.is_intersection() + && eval_property_type_variants_match( + property_type.variants(), + property_owner, + required_type.variants(), + required_owner, + pending_class, + context, + ) +} + +/// Returns whether two property type variant sets match, ignoring source order. +fn eval_property_type_variants_match( + property_variants: &[EvalParameterTypeVariant], + property_owner: &str, + required_variants: &[EvalParameterTypeVariant], + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property_variants.len() != required_variants.len() { + return false; + } + let mut matched = vec![false; property_variants.len()]; + required_variants.iter().all(|required_variant| { + property_variants + .iter() + .enumerate() + .find(|(index, property_variant)| { + !matched[*index] + && eval_property_type_variant_matches( + property_variant, + property_owner, + required_variant, + required_owner, + pending_class, + context, + ) + }) + .is_some_and(|(index, _)| { + matched[index] = true; + true + }) + }) +} + +/// Returns whether two property type atoms match PHP's invariant redeclaration rule. +fn eval_property_type_variant_matches( + property_variant: &EvalParameterTypeVariant, + property_owner: &str, + required_variant: &EvalParameterTypeVariant, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (property_variant, required_variant) { + ( + EvalParameterTypeVariant::Class(property_name), + EvalParameterTypeVariant::Class(required_name), + ) => eval_property_class_type_matches( + property_name, + property_owner, + required_name, + required_owner, + pending_class, + context, + ), + _ => property_variant == required_variant, + } +} + +/// Returns whether two class-name property type atoms name the same PHP type. +fn eval_property_class_type_matches( + property_name: &str, + property_owner: &str, + required_name: &str, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(required_name.trim_start_matches('\\')) + { + return true; + } + let Some(property_resolved) = + eval_resolve_return_class_type_name(property_name, property_owner, pending_class, context) + else { + return false; + }; + eval_resolve_return_class_type_name(required_name, required_owner, pending_class, context) + .is_some_and(|required_resolved| property_resolved.eq_ignore_ascii_case(&required_resolved)) +} + +/// Returns whether an implementation parameter type is a PHP contravariant supertype. +fn eval_parameter_type_accepts( + implementation_type: Option<&EvalParameterType>, + implementation_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (implementation_type, required_type) { + (None, _) => true, + (Some(implementation_type), None) => eval_type_is_mixed(implementation_type), + (Some(implementation_type), Some(required_type)) => eval_return_type_accepts( + implementation_type, + implementation_owner, + required_type, + required_owner, + pending_class, + context, + ), + } +} + +/// Returns whether `actual_type` is a covariant subtype of `expected_type`. +fn eval_return_type_accepts( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if eval_return_type_is_never(actual_type) { + return true; + } + if eval_return_type_is_never(expected_type) { + return false; + } + if eval_return_type_is_void(expected_type) { + return eval_return_type_is_void(actual_type); + } + if eval_return_type_is_void(actual_type) { + return false; + } + if eval_return_type_allows_null(actual_type) && !eval_return_type_allows_null(expected_type) { + return false; + } + if actual_type.variants().is_empty() { + return eval_return_type_allows_null(expected_type); + } + if expected_type.variants().is_empty() { + return false; + } + if actual_type.is_intersection() { + return eval_return_type_accepts_actual_intersection( + expected_type, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ); + } + actual_type.variants().iter().all(|actual_variant| { + eval_return_type_accepts_actual_variant( + expected_type, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether a return type can produce PHP null, including standalone `mixed`. +fn eval_return_type_allows_null(return_type: &EvalParameterType) -> bool { + return_type.allows_null() + || eval_type_is_mixed(return_type) +} + +/// Returns whether one retained type is PHP's standalone `mixed` atom. +fn eval_type_is_mixed(parameter_type: &EvalParameterType) -> bool { + !parameter_type.is_intersection() + && matches!(parameter_type.variants(), [EvalParameterTypeVariant::Mixed]) +} + +/// Returns whether a return type is exactly PHP `never`. +fn eval_return_type_is_never(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Never]) +} + +/// Returns whether a return type is exactly PHP `void`. +fn eval_return_type_is_void(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Void]) +} + +/// Returns whether an expected type accepts an actual intersection return type. +fn eval_return_type_accepts_actual_intersection( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_type.is_intersection() { + return expected_type.variants().iter().all(|expected_variant| { + eval_return_variant_accepts_actual_intersection( + expected_variant, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ) + }); + } + expected_type.variants().iter().any(|expected_variant| { + eval_return_variant_accepts_actual_intersection( + expected_variant, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether an expected type accepts one actual non-intersection atom. +fn eval_return_type_accepts_actual_variant( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_variant: &EvalParameterTypeVariant, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_type.is_intersection() { + return expected_type.variants().iter().all(|expected_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }); + } + expected_type.variants().iter().any(|expected_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one expected atom accepts an actual intersection return type. +fn eval_return_variant_accepts_actual_intersection( + expected_variant: &EvalParameterTypeVariant, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + actual_type.variants().iter().any(|actual_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one expected non-null atom accepts one actual non-null atom. +fn eval_return_variant_accepts_actual_variant( + expected_variant: &EvalParameterTypeVariant, + expected_owner: &str, + actual_variant: &EvalParameterTypeVariant, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (expected_variant, actual_variant) { + (_, EvalParameterTypeVariant::Never) => true, + (EvalParameterTypeVariant::Never, _) => false, + (EvalParameterTypeVariant::Void, _) | (_, EvalParameterTypeVariant::Void) => false, + (EvalParameterTypeVariant::Mixed, _) => true, + (EvalParameterTypeVariant::Object, EvalParameterTypeVariant::Object) + | (EvalParameterTypeVariant::Array, EvalParameterTypeVariant::Array) + | (EvalParameterTypeVariant::Bool, EvalParameterTypeVariant::Bool) + | (EvalParameterTypeVariant::Callable, EvalParameterTypeVariant::Callable) + | (EvalParameterTypeVariant::Float, EvalParameterTypeVariant::Float) + | (EvalParameterTypeVariant::Int, EvalParameterTypeVariant::Int) + | (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Iterable) + | (EvalParameterTypeVariant::String, EvalParameterTypeVariant::String) => true, + (EvalParameterTypeVariant::Object, EvalParameterTypeVariant::Class(_)) => true, + (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Array) => true, + (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Class(actual_name)) => { + eval_return_class_type_is_a( + actual_name, + actual_owner, + "Traversable", + actual_owner, + pending_class, + context, + ) || eval_return_class_type_is_a( + actual_name, + actual_owner, + "Iterator", + actual_owner, + pending_class, + context, + ) + } + ( + EvalParameterTypeVariant::Class(expected_name), + EvalParameterTypeVariant::Class(actual_name), + ) => eval_return_class_type_accepts( + expected_name, + expected_owner, + actual_name, + actual_owner, + pending_class, + context, + ), + _ => false, + } +} + +/// Returns whether one declared class-like return atom is covariant with another. +fn eval_return_class_type_accepts( + expected_name: &str, + expected_owner: &str, + actual_name: &str, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_name.eq_ignore_ascii_case("static") { + return actual_name.eq_ignore_ascii_case("static"); + } + if actual_name.eq_ignore_ascii_case("static") { + return eval_return_class_type_is_a( + actual_owner, + actual_owner, + expected_name, + expected_owner, + pending_class, + context, + ); + } + let Some(actual_resolved) = + eval_resolve_return_class_type_name(actual_name, actual_owner, pending_class, context) + else { + return false; + }; + eval_return_class_type_is_a( + &actual_resolved, + actual_owner, + expected_name, + expected_owner, + pending_class, + context, + ) || eval_resolve_return_class_type_name(expected_name, expected_owner, pending_class, context) + .is_some_and(|expected_resolved| actual_resolved.eq_ignore_ascii_case(&expected_resolved)) +} + +/// Returns whether an actual class-like return type satisfies an expected class-like target. +fn eval_return_class_type_is_a( + actual_name: &str, + actual_owner: &str, + expected_name: &str, + expected_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let Some(actual_resolved) = + eval_resolve_return_class_type_name(actual_name, actual_owner, pending_class, context) + else { + return false; + }; + let Some(expected_resolved) = + eval_resolve_return_class_type_name(expected_name, expected_owner, pending_class, context) + else { + return false; + }; + if actual_resolved.eq_ignore_ascii_case(&expected_resolved) { + return true; + } + if pending_class.is_some_and(|class| class.name().eq_ignore_ascii_case(&actual_resolved)) { + return pending_class_return_type_is_a(pending_class, &expected_resolved, context); + } + if context.has_class(&actual_resolved) { + return context.class_is_a(&actual_resolved, &expected_resolved, false); + } + if context.has_interface(&actual_resolved) { + return context + .interface_parent_names(&actual_resolved) + .iter() + .any(|parent| parent.eq_ignore_ascii_case(&expected_resolved)); + } + false +} + +/// Returns whether the pending class declaration satisfies one expected class-like type. +fn pending_class_return_type_is_a( + pending_class: Option<&EvalClass>, + expected_name: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(class) = pending_class else { + return false; + }; + if class.name().eq_ignore_ascii_case(expected_name) { + return true; + } + if class.parent().is_some_and(|parent| { + parent.eq_ignore_ascii_case(expected_name) + || context.class_is_a(parent, expected_name, false) + }) { + return true; + } + pending_class_return_interface_names(class, context) + .iter() + .any(|interface| interface.eq_ignore_ascii_case(expected_name)) +} + +/// Returns direct and inherited interface names for a pending eval class declaration. +fn pending_class_return_interface_names( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = class.parent() { + for interface in context.class_interface_names(parent) { + push_unique_return_class_name(&interface, &mut interfaces, &mut seen); + } + } + for interface in class.interfaces() { + push_unique_return_class_name(interface, &mut interfaces, &mut seen); + for parent in context.interface_parent_names(interface) { + push_unique_return_class_name(&parent, &mut interfaces, &mut seen); + } + } + interfaces +} + +/// Adds one class-like name once using PHP case-insensitive matching. +fn push_unique_return_class_name( + name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let name = name.trim_start_matches('\\'); + if seen.insert(name.to_ascii_lowercase()) { + names.push(name.to_string()); + } +} + +/// Resolves `self`/`parent`/`static` and aliases in a declared return type atom. +fn eval_resolve_return_class_type_name( + type_name: &str, + owner_name: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> Option { + let owner_name = owner_name.trim_start_matches('\\'); + match type_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "self" | "static" => Some(owner_name.to_string()), + "parent" => { + if let Some(class) = pending_class.filter(|class| { + class + .name() + .trim_start_matches('\\') + .eq_ignore_ascii_case(owner_name) + }) { + return class + .parent() + .map(|parent| parent.trim_start_matches('\\').to_string()); + } + context + .class(owner_name) + .and_then(EvalClass::parent) + .map(|parent| parent.trim_start_matches('\\').to_string()) + } + _ => Some( + context + .resolve_class_like_name(type_name) + .unwrap_or_else(|| type_name.trim_start_matches('\\').to_string()), + ), + } +} diff --git a/crates/elephc-magician/src/interpreter/return_values.rs b/crates/elephc-magician/src/interpreter/return_values.rs new file mode 100644 index 0000000000..c9b1a447b4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/return_values.rs @@ -0,0 +1,373 @@ +//! Purpose: +//! Enforces declared eval function and method return values at runtime. +//! This keeps return-value checks separate from argument binding and statement dispatch. +//! +//! Called from: +//! - `crate::interpreter::dynamic_functions` +//! - `crate::interpreter::statements` +//! +//! Key details: +//! - `self` resolves to the declaring owner, while `static` resolves to the called class. +//! - Return values use weak scalar coercions like parameter binding, with dedicated handling for `void` and `never`. + +use super::*; + +/// Applies one declared function or method return type to a completed control result. +pub(in crate::interpreter) fn eval_declared_return_control_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + control: EvalControl, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match control { + EvalControl::None => eval_declared_implicit_return_value(return_type, values), + EvalControl::ReturnVoid => eval_declared_void_return_value(return_type, values), + EvalControl::Return(result) => eval_declared_explicit_return_value( + return_type, + return_owner, + called_class_name, + result, + context, + values, + ), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Applies a registered native/AOT return type to an already materialized result. +pub(in crate::interpreter) fn eval_declared_native_return_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return Ok(value); + }; + if eval_declared_return_type_is_void(return_type) { + return if values.type_tag(value)? == EVAL_TAG_NULL { + Ok(value) + } else { + Err(EvalStatus::RuntimeFatal) + }; + } + if eval_declared_return_type_is_never(return_type) { + return Err(EvalStatus::RuntimeFatal); + } + eval_declared_return_value( + return_type, + return_owner, + called_class_name, + value, + context, + values, + ) +} + +/// Materializes an implicit return according to the declared return type. +fn eval_declared_implicit_return_value( + return_type: Option<&EvalParameterType>, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return values.null(); + }; + if eval_declared_return_type_is_void(return_type) { + return values.null(); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Materializes `return;` according to the declared return type. +fn eval_declared_void_return_value( + return_type: Option<&EvalParameterType>, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return values.null(); + }; + if eval_declared_return_type_is_void(return_type) { + return values.null(); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Validates or coerces an explicit returned value according to a declared return type. +fn eval_declared_explicit_return_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return Ok(value); + }; + if eval_declared_return_type_is_void(return_type) + || eval_declared_return_type_is_never(return_type) + { + return Err(EvalStatus::RuntimeFatal); + } + eval_declared_return_value( + return_type, + return_owner, + called_class_name, + value, + context, + values, + ) +} + +/// Applies a non-void declared return type to one returned runtime value. +fn eval_declared_return_value( + return_type: &EvalParameterType, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_declared_return_type_accepts_exact( + return_type, + return_owner, + called_class_name, + value, + context, + values, + )? { + return Ok(value); + } + if return_type.is_intersection() { + return Err(EvalStatus::RuntimeFatal); + } + for variant in return_type.variants() { + if let Some(coerced) = + eval_method_parameter_scalar_coercion(variant, value, context, values)? + { + return Ok(coerced); + } + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns whether a value already satisfies one declared return type. +fn eval_declared_return_type_accepts_exact( + return_type: &EvalParameterType, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + if tag == EVAL_TAG_NULL && eval_declared_return_type_allows_null(return_type) { + return Ok(true); + } + if return_type.is_intersection() { + for variant in return_type.variants() { + if !eval_declared_return_variant_accepts_exact( + variant, + return_owner, + called_class_name, + value, + tag, + context, + values, + )? { + return Ok(false); + } + } + return Ok(true); + } + for variant in return_type.variants() { + if eval_declared_return_variant_accepts_exact( + variant, + return_owner, + called_class_name, + value, + tag, + context, + values, + )? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether one non-null return type atom accepts a runtime value exactly. +fn eval_declared_return_variant_accepts_exact( + variant: &EvalParameterTypeVariant, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + tag: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match variant { + EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), + EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), + EvalParameterTypeVariant::Callable => Ok(matches!( + tag, + EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT + )), + EvalParameterTypeVariant::Class(class_name) => eval_declared_return_class_accepts( + value, + tag, + class_name, + return_owner, + called_class_name, + context, + values, + ), + EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), + EvalParameterTypeVariant::Iterable => { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if eval_declared_return_class_accepts( + value, + tag, + "Traversable", + return_owner, + called_class_name, + context, + values, + )? { + return Ok(true); + } + eval_declared_return_class_accepts( + value, + tag, + "Iterator", + return_owner, + called_class_name, + context, + values, + ) + } + EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), + EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), + EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), + } +} + +/// Returns whether an object value satisfies one class-like declared return target. +fn eval_declared_return_class_accepts( + value: RuntimeCellHandle, + tag: u64, + class_name: &str, + return_owner: Option<&str>, + called_class_name: Option<&str>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + let target = eval_declared_return_runtime_class_name( + class_name, + return_owner, + called_class_name, + context, + )?; + let identity = values.object_identity(value)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(eval_declared_dynamic_object_is_a( + class.name(), + &target, + context, + )); + } + if values.object_is_a(value, &target, false)? { + return Ok(true); + } + if target.eq_ignore_ascii_case("Traversable") { + return Ok(values.object_is_a(value, "Iterator", false)? + || values.object_is_a(value, "IteratorAggregate", false)?); + } + Ok(false) +} + +/// Returns whether one eval-created object class satisfies a declared return target. +fn eval_declared_dynamic_object_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + if context.class_is_a(class_name, target, false) { + return true; + } + target.eq_ignore_ascii_case("Traversable") + && (context.class_is_a(class_name, "Iterator", false) + || context.class_is_a(class_name, "IteratorAggregate", false)) +} + +/// Resolves class keywords and aliases in a declared return type atom. +fn eval_declared_return_runtime_class_name( + class_name: &str, + return_owner: Option<&str>, + called_class_name: Option<&str>, + context: &ElephcEvalContext, +) -> Result { + match class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "self" => return_owner + .map(|owner| owner.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal), + "static" => called_class_name + .or(return_owner) + .map(|owner| owner.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let owner = return_owner.ok_or(EvalStatus::RuntimeFatal)?; + context + .class(owner) + .and_then(EvalClass::parent) + .map(|parent| parent.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => Ok(context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Returns whether a declared return type can accept PHP null. +fn eval_declared_return_type_allows_null(return_type: &EvalParameterType) -> bool { + return_type.allows_null() + || (!return_type.is_intersection() + && return_type + .variants() + .iter() + .any(|variant| matches!(variant, EvalParameterTypeVariant::Mixed))) +} + +/// Returns whether a declared return type is exactly PHP `never`. +fn eval_declared_return_type_is_never(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Never]) +} + +/// Returns whether a declared return type is exactly PHP `void`. +fn eval_declared_return_type_is_void(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Void]) +} diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs new file mode 100644 index 0000000000..a7fd59c7d4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -0,0 +1,607 @@ +//! Purpose: +//! Defines the runtime value operation contract used by the EvalIR interpreter. +//! The trait abstracts over opaque elephc runtime cells while eval drives PHP semantics. +//! +//! Called from: +//! - `crate::interpreter::execute_program_with_context()` +//! - Eval builtin and expression execution helpers. +//! +//! Key details: +//! - Implementors own allocation, retain/release, casting, arithmetic, and target runtime calls. +//! - Tag constants mirror boxed Mixed runtime tags consumed by eval-only helpers. + +use std::ffi::c_void; + +use crate::errors::EvalStatus; +use crate::eval_ir::EvalBinOp; +use crate::value::RuntimeCellHandle; + +/// Runtime value hooks required by the EvalIR interpreter. +pub trait RuntimeValueOps { + /// Creates a runtime indexed-array cell with room for at least `capacity` elements. + fn array_new(&mut self, capacity: usize) -> Result; + + /// Creates a runtime indexed-array cell specialized for direct string payload slots. + fn string_array_new(&mut self, capacity: usize) -> Result; + + /// Appends one string payload to a runtime string-array cell. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result; + + /// Creates a runtime associative-array cell with room for at least `capacity` elements. + fn assoc_new(&mut self, capacity: usize) -> Result; + + /// Reads one element from a runtime Mixed cell using PHP array-read semantics. + /// + /// Missing keys and non-array receivers return PHP null, matching the generated + /// `__rt_mixed_array_get` runtime helper. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result; + + /// Checks whether a normalized PHP array key exists without conflating null values with misses. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result; + + /// Returns the foreach-visible key at a zero-based iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result; + + /// Writes one element to a runtime array-like Mixed cell and returns the target cell. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result; + + /// Creates a shallow array copy before writeback paths perform PHP COW writes. + fn array_clone_shallow( + &mut self, + array: RuntimeCellHandle, + ) -> Result { + let len = self.array_len(array)?; + let mut result = match self.type_tag(array)? { + EVAL_TAG_ARRAY => self.array_new(len)?, + EVAL_TAG_ASSOC => self.assoc_new(len)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + for position in 0..len { + let key = self.array_iter_key(array, position)?; + let value = self.array_get(array, key)?; + result = self.array_set(result, key, value)?; + } + Ok(result) + } + + /// Reads a named property from a runtime object held in a boxed Mixed cell. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result; + + /// Checks whether a generated/AOT instance property is initialized. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result; + + /// Writes a named property on a runtime object held in a boxed Mixed cell. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus>; + + /// Reads a generated/AOT static property through the generated bridge. + fn static_property_get( + &mut self, + class_name: &str, + property: &str, + ) -> Result, EvalStatus>; + + /// Checks whether a generated/AOT static property is initialized. + fn static_property_is_initialized( + &mut self, + class_name: &str, + property: &str, + ) -> Result; + + /// Writes a generated/AOT static property through the generated bridge. + fn static_property_set( + &mut self, + class_name: &str, + property: &str, + value: RuntimeCellHandle, + ) -> Result; + + /// Reads a generated/AOT class-like constant through the generated bridge. + fn class_constant_get( + &mut self, + class_name: &str, + constant: &str, + ) -> Result, EvalStatus>; + + /// Creates a shallow clone of a runtime object held in a boxed Mixed cell. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result; + + /// Returns the number of public JSON-visible properties on a runtime object. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result; + + /// Returns the public property key at a zero-based JSON object iteration position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result; + + /// Calls a named method on a runtime object held in a boxed Mixed cell. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result; + + /// Calls a named static method through the generated AOT bridge. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result; + + /// Converts a generated native call result into an eval result or pending throwable status. + fn native_call_result( + &mut self, + result: RuntimeCellHandle, + ) -> Result { + if result.is_null() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(result) + } + } + + /// Materializes a synthetic `ReflectionAttribute` object through generated private-layout code. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result; + + /// Materializes a synthetic ReflectionClass/Method/Property object through generated private-layout code. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result; + + /// Returns generated AOT ReflectionMethod flags for a class/method pair. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus>; + + /// Returns the generated AOT declaring class for a class/method pair. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionMethod names visible for one class. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns the generated program source file used for AOT reflection metadata. + fn reflection_source_file(&mut self) -> Result, EvalStatus> { + Ok(None) + } + + /// Returns generated AOT ReflectionClass modifier flags for one class. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionProperty flags for a class/property pair. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus>; + + /// Returns the generated AOT declaring class for a class/property pair. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionProperty names visible for one class. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated AOT ReflectionClassConstant values without visibility checks. + fn reflection_constant_value( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionClassConstant flags for a class/constant pair. + fn reflection_constant_flags( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns the generated AOT declaring class for a class/constant pair. + fn reflection_constant_declaring_class( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionClassConstant names visible for one class. + fn reflection_constant_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated/AOT interface names visible for one reflected class-like symbol. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated/AOT trait names visible for one reflected class-like symbol. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated/AOT trait alias names visible for one reflected class-like symbol. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated/AOT trait alias sources visible for one reflected class-like symbol. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result; + + /// Creates a named runtime object without constructor arguments. + fn new_object(&mut self, class_name: &str) -> Result; + + /// Calls the runtime constructor for an object when the class declares one. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus>; + + /// Returns whether a runtime class table contains the requested class name. + fn class_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime interface table contains the requested interface name. + fn interface_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime trait table contains the requested trait name. + fn trait_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime enum table contains the requested enum name. + fn enum_exists(&mut self, name: &str) -> Result; + + /// Tests whether a boxed object cell satisfies a class/interface relation. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result; + + /// Returns the PHP-visible runtime class name for an object cell. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result; + + /// Returns the PHP-visible parent class name for an object or class-name cell. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result; + + /// Returns the visible element count for an array-like runtime cell. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result; + + /// Returns whether a runtime cell can be indexed like an array by eval writes. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; + + /// Returns whether a runtime cell holds PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result; + + /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; + + /// Creates an invoker-only by-reference marker for a staged Mixed slot. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result; + + /// Creates an invoker-only by-reference marker for a staged raw one-word slot. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut c_void, + source_tag: u64, + ) -> Result; + + /// Extracts the low raw payload word from a boxed Mixed cell. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result; + + /// Extracts the high raw payload word from a boxed Mixed cell. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result; + + /// Duplicates one raw string payload so a staged native by-ref slot owns it. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus>; + + /// Boxes one raw string payload as a Mixed string cell. + fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result; + + /// Releases one raw string payload owned by a staged native by-ref slot. + fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus>; + + /// Retains one raw heap payload word so a staged native by-ref slot owns it. + fn retain_raw_heap_word(&mut self, word: u64) -> Result; + + /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result; + + /// Boxes one raw heap payload word as a Mixed cell after inspecting its heap kind. + fn raw_heap_word_value(&mut self, word: u64) -> Result; + + /// Releases one raw heap payload word owned by a staged native by-ref slot. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus>; + + /// Returns the unboxed object payload pointer used for PHP object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; + + /// Returns the object identity that would be freed by releasing this owned cell, if any. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus>; + + /// Releases one owned runtime cell that is no longer held by the eval scope. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Retains one runtime cell so the eval caller receives an independent owner. + fn retain(&mut self, value: RuntimeCellHandle) -> Result; + + /// Emits or suppresses one PHP runtime warning through the target runtime. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus>; + + /// Creates a runtime null cell. + fn null(&mut self) -> Result; + + /// Creates a runtime bool cell. + fn bool_value(&mut self, value: bool) -> Result; + + /// Creates a runtime int cell. + fn int(&mut self, value: i64) -> Result; + + /// Creates a runtime resource cell with a zero-based native resource payload. + fn resource(&mut self, value: i64) -> Result; + + /// Creates a runtime float cell. + fn float(&mut self, value: f64) -> Result; + + /// Creates a runtime string cell. + fn string(&mut self, value: &str) -> Result; + + /// Creates a runtime byte-string cell from raw PHP string bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result; + + /// Casts one runtime cell to a boxed PHP integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. + fn abs(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `ceil()` for one runtime cell after PHP numeric conversion. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `floor()` for one runtime cell after PHP numeric conversion. + fn floor(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; + + /// Reverses a string value using PHP `strrev()` byte-string semantics. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result; + + /// Divides two runtime cells using PHP `fdiv()` semantics. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes the floating-point remainder using PHP `fmod()` semantics. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Adds two runtime cells using PHP addition semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Subtracts two runtime cells using PHP numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Multiplies two runtime cells using PHP numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Divides two runtime cells using PHP numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes modulo for two runtime cells using PHP integer modulo semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Raises one runtime cell to another using PHP exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Rounds one runtime cell using PHP `round()` semantics and optional precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result; + + /// Applies an integer bitwise or shift operation to two runtime cells. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Applies integer bitwise NOT to one runtime cell. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result; + + /// Concatenates two runtime cells using PHP string conversion semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Compares two runtime cells and returns a boxed PHP boolean cell. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Compares two runtime cells and returns a boxed PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Emits one runtime cell to stdout using PHP echo semantics. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Casts one runtime cell to a PHP string and copies its bytes for parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus>; + + /// Converts one runtime cell to PHP boolean truthiness. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result; +} + +pub(super) const EVAL_TAG_INT: u64 = 0; +pub(super) const EVAL_TAG_STRING: u64 = 1; +pub(super) const EVAL_TAG_FLOAT: u64 = 2; +pub(super) const EVAL_TAG_BOOL: u64 = 3; +pub(super) const EVAL_TAG_ARRAY: u64 = 4; +pub(super) const EVAL_TAG_ASSOC: u64 = 5; +pub(super) const EVAL_TAG_OBJECT: u64 = 6; +pub(super) const EVAL_TAG_MIXED: u64 = 7; +pub(super) const EVAL_TAG_NULL: u64 = 8; +pub(super) const EVAL_TAG_RESOURCE: u64 = 9; +pub(super) const EVAL_TAG_CALLABLE: u64 = 10; +pub(super) const EVAL_TAG_INVOKER_REF_CELL: u64 = 11; + +pub(super) const EVAL_REFLECTION_OWNER_CLASS: u64 = 0; +pub(super) const EVAL_REFLECTION_OWNER_METHOD: u64 = 1; +pub(super) const EVAL_REFLECTION_OWNER_PROPERTY: u64 = 2; +pub(super) const EVAL_REFLECTION_OWNER_CLASS_CONSTANT: u64 = 3; +pub(super) const EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE: u64 = 4; +pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; +pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; +pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; +pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; +pub(super) const EVAL_REFLECTION_OWNER_INTERSECTION_TYPE: u64 = 9; +pub(super) const EVAL_REFLECTION_OWNER_FUNCTION: u64 = 10; +pub(super) const EVAL_REFLECTION_OWNER_ENUM: u64 = 11; +pub(super) const EVAL_REFLECTION_OWNER_OBJECT: u64 = 12; diff --git a/crates/elephc-magician/src/interpreter/scope_cells.rs b/crates/elephc-magician/src/interpreter/scope_cells.rs new file mode 100644 index 0000000000..2b0542618e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/scope_cells.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Provides scope-cell read, write, unset, global-alias, and reference-alias helpers for eval execution. +//! +//! Called from: +//! - `crate::interpreter::statements` and `crate::interpreter::expressions`. +//! +//! Key details: +//! - Global aliases redirect through `ElephcEvalContext` while local aliases stay in the materialized eval scope. +//! - Replaced owned cells are released by callers through existing scope APIs. + +use super::*; + +/// Returns the eval-visible entry for a variable, following `global` aliases. +pub(in crate::interpreter) fn scope_entry( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + let Some(global_name) = scope.global_alias_target(name) else { + return scope.entry(name); + }; + let Some(global_scope) = context.global_scope_ptr() else { + return scope.entry(name); + }; + let current_scope = scope as *const ElephcEvalScope as *mut ElephcEvalScope; + if global_scope == current_scope { + return scope.entry(global_name); + } + unsafe { + global_scope + .as_ref() + .and_then(|scope| scope.entry(global_name)) + } +} + +/// Returns the eval-visible cell for a variable, following `global` aliases. +pub(in crate::interpreter) fn visible_scope_cell( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(ScopeEntry::cell) +} + +/// Stores a variable cell, redirecting `global` aliases to the global scope. +pub(in crate::interpreter) fn set_scope_cell( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, +) -> Result, EvalStatus> { + let name = name.into(); + if let Some(global_name) = scope.global_alias_target(&name).map(str::to_string) { + let Some(global_scope) = context.global_scope_ptr() else { + return Err(EvalStatus::RuntimeFatal); + }; + let current_scope = scope as *mut ElephcEvalScope; + if global_scope == current_scope { + return Ok(scope.set_respecting_references(global_name, cell, ownership)); + } + let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + return Ok(global_scope.set_respecting_references(global_name, cell, ownership)); + } + Ok(scope.set_respecting_references(name, cell, ownership)) +} + +/// Creates a PHP reference alias between two eval-visible variable names. +pub(in crate::interpreter) fn set_reference_alias( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + target: &str, + source: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(global_name) = scope.global_alias_target(source).map(str::to_string) { + scope.mark_global_alias_to(target.to_string(), global_name); + return Ok(Vec::new()); + } + let (cell, ownership) = scope_entry(context, scope, source) + .filter(|entry| entry.flags().is_visible()) + .map_or_else( + || values.null().map(|cell| (cell, ScopeCellOwnership::Owned)), + |entry| Ok((entry.cell(), entry.flags().ownership)), + )?; + Ok(scope.set_reference(target.to_string(), source.to_string(), cell, ownership)) +} + +/// Unsets a variable, removing only the local alias when the name is global. +pub(in crate::interpreter) fn unset_scope_cell( + scope: &mut ElephcEvalScope, + name: impl Into, +) -> Option { + let name = name.into(); + if scope.is_global_alias(&name) { + scope.clear_global_alias(&name); + } + scope.unset_respecting_references(name) +} + +/// Marks variables as aliases to the context global scope for later reads/writes. +pub(in crate::interpreter) fn execute_global_stmt( + vars: &[String], + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, +) -> Result<(), EvalStatus> { + if context.global_scope_ptr().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + for name in vars { + scope.mark_global_alias(name.clone()); + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs new file mode 100644 index 0000000000..f6b0e3cc4c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Dispatches EvalIR statements and propagates structured control flow. +//! Statement families and runtime subsystems live in focused child modules. +//! +//! Called from: +//! - `crate::interpreter::execute_program_outcome_with_context()` and dynamic function execution. +//! +//! Key details: +//! - Statement execution propagates `EvalControl` instead of flattening returns, throws, breaks, or continues. +//! - Scope writes flow through shared scope-cell helpers so global aliases and reference aliases stay coherent. + +mod abstract_requirements; +mod array_updates; +mod attributes_magic_validation; +mod callable_objects; +mod class_declarations; +mod class_resolution; +mod closure_binding; +mod dispatch; +mod dynamic_method_execution; +mod enum_declarations; +mod exceptions; +mod instance_property_access; +mod interface_contracts; +mod interface_member_validation; +mod loop_statements; +mod method_dispatch; +mod native_argument_binding; +mod native_constructor_defaults; +mod native_method_execution; +mod native_static_dispatch; +mod property_constant_validation; +mod property_validation; +mod reference_writeback; +mod reflection_instantiation; +mod static_method_dispatch; +mod static_property_access; +mod trait_declarations; + +use super::*; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, + NativeCallableObjectDefaultArg, NativeCallableSignature, + push_native_frame_called_class_override, +}; + +use abstract_requirements::*; +pub(crate) use array_updates::*; +use attributes_magic_validation::*; +pub(in crate::interpreter) use callable_objects::*; +pub(in crate::interpreter) use class_declarations::*; +pub(in crate::interpreter) use class_resolution::*; +use closure_binding::*; +pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use dynamic_method_execution::*; +pub(in crate::interpreter) use enum_declarations::*; +pub(in crate::interpreter) use exceptions::*; +pub(in crate::interpreter) use instance_property_access::*; +use interface_contracts::*; +use interface_member_validation::*; +pub(in crate::interpreter) use loop_statements::*; +pub(in crate::interpreter) use method_dispatch::*; +pub(in crate::interpreter) use native_argument_binding::*; +pub(in crate::interpreter) use native_constructor_defaults::*; +pub(in crate::interpreter) use native_method_execution::*; +pub(in crate::interpreter) use native_static_dispatch::*; +use property_constant_validation::*; +pub(in crate::interpreter) use property_validation::*; +pub(in crate::interpreter) use reference_writeback::*; +pub(in crate::interpreter) use reflection_instantiation::*; +pub(in crate::interpreter) use static_method_dispatch::*; +pub(in crate::interpreter) use static_property_access::*; +pub(in crate::interpreter) use trait_declarations::*; diff --git a/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs b/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs new file mode 100644 index 0000000000..b7be19cff5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs @@ -0,0 +1,508 @@ +//! Purpose: +//! Collects, merges, and applies inherited abstract method and property requirements. +//! +//! Called from: +//! - Concrete class validation after direct member compatibility checks. +//! +//! Key details: +//! - Eval and AOT parent contracts retain visibility, staticness, and property hook modes. + +use super::*; + +/// Validates that a concrete class has satisfied inherited abstract and interface requirements. +pub(super) fn validate_concrete_class_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !pending_class_abstract_method_requirements(class, context).is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if !pending_class_abstract_property_requirements(class, context)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) { + validate_class_implements_eval_interface(class, &interface, context)?; + } + } + Ok(()) +} + +/// Validates concrete class methods required by PHP builtin runtime interfaces. +pub(super) fn validate_concrete_class_builtin_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + if !class_has_builtin_interface_method(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates concrete class methods required by generated/AOT abstract parents. +pub(super) fn validate_concrete_class_aot_parent_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !pending_class_aot_parent_abstract_method_requirements(class, context, values)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if !pending_class_aot_parent_abstract_property_requirements(class, context, values)?.is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates concrete class methods required by generated/AOT runtime interfaces. +pub(super) fn validate_concrete_class_aot_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + if !class_has_aot_interface_method(class, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns inherited abstract methods that the pending class has not concretized. +pub(super) fn pending_class_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, &mut requirements); + } + apply_class_abstract_method_requirements(class, &mut requirements); + requirements.into_values().collect() +} + +/// Returns inherited abstract properties that the pending class has not concretized. +pub(super) fn pending_class_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, &mut requirements)?; + } + apply_class_abstract_property_requirements(class, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Returns generated/AOT abstract parent methods the pending class has not concretized. +pub(super) fn pending_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Returns generated/AOT abstract parent properties the pending class has not concretized. +pub(super) fn pending_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Collects abstract method requirements from one declared eval class ancestry chain. +pub(super) fn collect_class_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) { + let Some(class) = context.class(class_name) else { + return; + }; + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, requirements); + } + apply_class_abstract_method_requirements(class, requirements); +} + +/// Collects generated/AOT abstract method requirements through eval and AOT parents. +pub(super) fn collect_aot_parent_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_method_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract methods exposed by one generated/AOT class reflection row. +pub(super) fn collect_native_aot_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method_name in eval_aot_method_names(class_name, values)? { + let Some(flags) = values.reflection_method_flags(class_name, &method_name)? else { + continue; + }; + let key = method_name.to_ascii_lowercase(); + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { + requirements.remove(&key); + continue; + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + continue; + } + let requirement = + eval_aot_abstract_method_requirement(class_name, &method_name, flags, context, values)?; + requirements.insert(key, requirement); + } + Ok(()) +} + +/// Collects generated/AOT abstract property requirements through eval and AOT parents. +pub(super) fn collect_aot_parent_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_property_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract properties exposed by one generated/AOT class metadata row. +pub(super) fn collect_native_aot_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for (owner, property) in context.native_abstract_property_requirements(class_name) { + let Some(flags) = values.reflection_property_flags(class_name, property.name())? else { + continue; + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 + { + continue; + } + let visibility = eval_aot_property_visibility(flags); + let write_visibility = eval_aot_property_write_visibility(flags, visibility); + let set_visibility = (write_visibility != visibility).then_some(write_visibility); + let requirement = EvalClassProperty::with_visibility(property.name(), visibility, None) + .with_type(property.property_type().cloned()) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + requirements.insert( + property.name().to_string(), + EvalAotAbstractPropertyRequirement { + owner, + property: requirement, + }, + ); + } + Ok(()) +} + +/// Collects abstract property requirements from one declared eval class ancestry chain. +pub(super) fn collect_class_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let Some(class) = context.class(class_name) else { + return Ok(()); + }; + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, requirements)?; + } + apply_class_abstract_property_requirements(class, requirements) +} + +/// Applies one class's methods to the open abstract-method requirement set. +pub(super) fn apply_class_abstract_method_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + if method.is_abstract() { + requirements.insert(key, method.clone()); + } else { + requirements.remove(&key); + } + } +} + +/// Applies one eval class's methods to the open AOT abstract-method requirement set. +pub(super) fn apply_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + let Some(requirement) = requirements.get(&key) else { + continue; + }; + if !class_method_satisfies_aot_abstract_parent_requirement( + method, + class.name(), + requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if !method.is_abstract() { + requirements.remove(&key); + } + } + Ok(()) +} + +/// Applies one eval class's properties to the open AOT abstract-property requirement set. +pub(super) fn apply_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + let Some(requirement) = requirements.get(&key).map(|requirement| { + EvalAotAbstractPropertyRequirement { + owner: requirement.owner.clone(), + property: requirement.property.clone(), + } + }) else { + continue; + }; + if !class_property_satisfies_aot_abstract_parent_requirement( + property, + class.name(), + &requirement, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_abstract() { + requirements.insert( + key, + EvalAotAbstractPropertyRequirement { + owner: class.name().to_string(), + property: merge_abstract_property_contracts( + &requirement.property, + property, + ), + }, + ); + } else { + requirements.remove(&key); + } + } + Ok(()) +} + +/// Applies one class's properties to the open abstract-property requirement set. +pub(super) fn apply_class_abstract_property_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + if property.is_abstract() { + if let Some(existing) = requirements.get(&key) { + (property_contract_visibility_allows(existing, property) + && property_contract_write_visibility_allows(existing, property)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.insert(key, merge_abstract_property_contracts(existing, property)); + } else { + requirements.insert(key, property.clone()); + } + } else if let Some(requirement) = requirements.get(&key) { + class_property_satisfies_abstract_contract(property, requirement) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.remove(&key); + } + } + Ok(()) +} + +/// Merges inherited and redeclared abstract property hook requirements. +pub(super) fn merge_abstract_property_contracts( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> EvalClassProperty { + redeclared.clone().with_abstract_hook_contract( + inherited.requires_get_hook() || redeclared.requires_get_hook(), + inherited.requires_set_hook() || redeclared.requires_set_hook(), + ) +} + +/// Returns whether a redeclared property keeps compatible visibility. +pub(super) fn property_contract_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + property_visibility_rank(redeclared.visibility()) + >= property_visibility_rank(inherited.visibility()) +} + +/// Returns whether a redeclared property keeps compatible write visibility. +pub(super) fn property_contract_write_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + !inherited.requires_set_hook() + || property_visibility_rank(redeclared.write_visibility()) + >= property_visibility_rank(inherited.write_visibility()) +} + +/// Returns whether a concrete property satisfies an abstract hook contract. +pub(super) fn class_property_satisfies_abstract_contract( + property: &EvalClassProperty, + requirement: &EvalClassProperty, +) -> bool { + if property.is_abstract() + || property.is_static() + || property.property_type() != requirement.property_type() + || !property_contract_visibility_allows(requirement, property) + { + return false; + } + if requirement.requires_set_hook() { + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(requirement, property) + && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); + } + requirement.requires_get_hook() +} + +/// Returns whether one property satisfies a generated/AOT abstract parent contract. +pub(super) fn class_property_satisfies_aot_abstract_parent_requirement( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalAotAbstractPropertyRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let required = &requirement.property; + if property.is_static() != required.is_static() + || !property_contract_visibility_allows(required, property) + || !property_type_signature_matches( + property.property_type(), + property_owner, + required.property_type(), + &requirement.owner, + pending_class, + context, + ) + { + return false; + } + if property.is_abstract() { + return (!required.requires_get_hook() || property.requires_get_hook()) + && (!required.requires_set_hook() + || (property.requires_set_hook() + && property_contract_write_visibility_allows(required, property))); + } + if required.requires_get_hook() && !class_property_supports_interface_get(property) { + return false; + } + if required.requires_set_hook() { + return required.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(required, property) + && class_property_supports_interface_set(property); + } + required.requires_get_hook() +} + +/// Returns a comparable rank where larger means less restrictive property visibility. +pub(super) fn property_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/array_updates.rs b/crates/elephc-magician/src/interpreter/statements/array_updates.rs new file mode 100644 index 0000000000..836f096f28 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/array_updates.rs @@ -0,0 +1,590 @@ +//! Purpose: +//! Executes increment, unset, append, and indexed array mutation statements. +//! +//! Called from: +//! - `crate::interpreter::statements::execute_stmt()`. +//! +//! Key details: +//! - Object ArrayAccess and plain runtime arrays preserve reference and release semantics. + +use super::*; + +/// Applies member increment/decrement to a runtime value using PHP numeric semantics. +pub(super) fn eval_inc_dec_value( + current: RuntimeCellHandle, + increment: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let one = values.int(1)?; + if increment { + values.add(current, one) + } else { + values.sub(current, one) + } +} + +/// Reads, updates, and writes one object property after the receiver/name are evaluated. +pub(super) fn eval_property_inc_dec_result( + object: RuntimeCellHandle, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_property_get_result(object, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_property_set_result(object, property, value, context, values) +} + +/// Reads, updates, and writes one static property after the receiver/name are resolved. +pub(super) fn eval_static_property_inc_dec_result( + class_name: &str, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_static_property_get_result(class_name, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_static_property_set_result(class_name, property, value, context, values) +} + +/// Releases one eval-owned value after running an eval-declared dynamic destructor if needed. +pub(super) fn eval_release_value( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if let Some(identity) = values.final_object_identity_for_release(value)? { + eval_dynamic_destructor_for_release(identity, value, context, values)?; + } + values.release(value) +} + +/// Calls a dynamic eval `__destruct()` hook immediately before the runtime frees the object. +pub(super) fn eval_dynamic_destructor_for_release( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_dynamic_destructor_for_object_cell(identity, object, context, values).map(|_| ()) +} + +/// Calls a dynamic eval `__destruct()` hook for an already-boxed object cell. +pub(crate) fn eval_dynamic_destructor_for_object_cell( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Ok(false); + }; + let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { + return Ok(false); + }; + if !context.begin_dynamic_object_destructor(identity) { + return Ok(true); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + object, + Vec::new(), + context, + values, + ); + let release_result = match result { + Ok(result) => values.release(result), + Err(status) => Err(status), + }; + context.finish_dynamic_object_destructor(identity); + release_result.map(|_| true) +} + +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +pub(super) fn eval_array_unset_element_stmt( + array: &EvalExpr, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match array { + EvalExpr::LoadVar(name) => { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + let Some((array, ownership)) = existing else { + return Ok(()); + }; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { + values.release(replaced)?; + } + } + return Ok(()); + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let array = eval_property_get_result(object, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_property_get_result(object, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, &property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(class_name, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let array = eval_static_property_get_result(&class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_static_property_get_result(&class_name, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, &property, array, context, values)?; + } + return Ok(()); + } + _ => {} + } + let array = eval_expr(array, context, scope, values)?; + eval_array_access_unset_result(array, index, context, scope, values) +} + +/// Unsets one offset from an already-resolved array-like target and returns a replacement array. +pub(super) fn eval_array_unset_target_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(array)? == EVAL_TAG_OBJECT { + eval_array_access_unset_result(array, index, context, scope, values)?; + return Ok(None); + } + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = eval_array_set_index(index, context, scope, values)?; + eval_array_without_key_result(array, index, values).map(Some) +} + +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +pub(super) fn eval_array_access_unset_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(array, "offsetUnset", vec![index], context, values)?; + values.release(result)?; + Ok(()) +} + +/// Rebuilds an array without the strict-equal key requested by `unset($array[$key])`. +pub(super) fn eval_array_without_key_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + let mut result = if tag == EVAL_TAG_ASSOC { + values.assoc_new(len.saturating_sub(1))? + } else { + values.array_new(len.saturating_sub(1))? + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let equal = values.compare(EvalBinOp::StrictEq, key, index)?; + if values.truthy(equal)? { + continue; + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +pub(super) fn eval_array_append_var_stmt( + name: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_append_var_stmt( + name, value, existing, context, scope, values, + ); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_append_var_stmt(name, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[] = value` path with the existing array semantics. +pub(super) fn eval_non_object_array_append_var_stmt( + name: &str, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + let tag = values.type_tag(cell)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes `$var[index] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +pub(super) fn eval_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_set_var_stmt( + name, index, value, existing, context, scope, values, + ); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_set_var_stmt(name, index, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[index] = value` path with the existing array semantics. +pub(super) fn eval_non_object_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_set_index(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = eval_array_set_target_for_index(array, index, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes `$object->property[] = value`, dispatching ArrayAccess property values when needed. +pub(super) fn eval_property_array_append_result( + object: RuntimeCellHandle, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Executes `$object->property[index] = value` and compound indexed property writes. +pub(super) fn eval_property_array_set_result( + object: RuntimeCellHandle, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Computes the value written by a simple or compound property-array assignment. +pub(super) fn eval_property_array_set_value( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(op) = op else { + return eval_expr(value, context, scope, values); + }; + let current = eval_array_get_result(array, index, context, values)?; + let right = eval_expr(value, context, scope, values)?; + eval_binary_result(op, current, right, context, values) +} + +/// Executes `Class::$property[] = value`, including ArrayAccess static-property values. +pub(super) fn eval_static_property_array_append_result( + class_name: &str, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + +/// Executes `Class::$property[index] = value` and compound indexed static-property writes. +pub(super) fn eval_static_property_array_set_result( + class_name: &str, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + +/// Evaluates an array-set index and normalizes PHP integer-string keys. +pub(super) fn eval_array_set_index( + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(index); + } + let bytes = values.string_bytes(index)?; + match eval_numeric_string_array_key(&bytes) { + Some(key) => values.int(key), + None => Ok(index), + } +} + +/// Converts indexed arrays to associative arrays before writing a non-numeric string key. +pub(super) fn eval_array_set_target_for_index( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_ARRAY || values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(array); + } + let len = values.array_len(array)?; + let mut assoc = values.assoc_new(len + 1)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + assoc = values.array_set(assoc, key, value)?; + } + Ok(assoc) +} diff --git a/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs b/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs new file mode 100644 index 0000000000..ce6f02f4f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs @@ -0,0 +1,620 @@ +//! Purpose: +//! Validates class-like attributes, modifiers, override markers, and magic methods. +//! +//! Called from: +//! - Eval class, interface, trait, and enum declaration validation. +//! +//! Key details: +//! - AOT member flags and magic signature contracts are shared with later validators. + +use super::*; + +/// Bridge reflection flag for static generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; + +/// Bridge reflection flag for public generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; + +/// Bridge reflection flag for protected generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; + +/// Bridge reflection flag for private generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; + +/// Bridge reflection flag for final generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; + +/// Bridge reflection flag for abstract generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; + +/// Bridge reflection flag for readonly generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; + +/// Bridge reflection flag for protected-set generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; + +/// Bridge reflection flag for private-set generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; + +/// Method requirement discovered from generated/AOT interface metadata. +pub(super) struct EvalAotInterfaceMethodRequirement { + pub(super) owner: String, + pub(super) name: String, + pub(super) is_static: bool, + pub(super) signature: Option, +} + +/// Abstract method requirement discovered from generated/AOT parent metadata. +pub(super) struct EvalAotAbstractMethodRequirement { + pub(super) owner: String, + pub(super) is_static: bool, + pub(super) visibility: EvalVisibility, + pub(super) signature: Option, +} + +/// Abstract property requirement discovered from generated/AOT parent metadata. +pub(super) struct EvalAotAbstractPropertyRequirement { + pub(super) owner: String, + pub(super) property: EvalClassProperty, +} + +/// Rejects builtin attributes that cannot target an eval-declared class. +pub(super) fn validate_eval_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "Override") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects builtin attributes that cannot target eval-declared interfaces. +pub(super) fn validate_eval_interface_attribute_targets( + interface: &EvalInterface, +) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(interface.attributes())?; + for property in interface.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in interface.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Validates PHP's global `#[Override]` marker on eval-declared interface methods. +pub(super) fn validate_eval_interface_override_attributes( + interface: &EvalInterface, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let parent_requirements = eval_interface_parent_method_requirements(interface, context); + for method in interface.methods() { + if !eval_interface_method_has_global_builtin_attribute(method, "Override") { + continue; + } + if parent_requirements + .iter() + .any(|(_, requirement)| eval_interface_method_matches_requirement(method, requirement)) + { + continue; + } + if eval_aot_interface_parent_method_matches(interface, method, values)? { + continue; + } + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns method requirements inherited by one eval interface declaration. +pub(super) fn eval_interface_parent_method_requirements( + interface: &EvalInterface, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for parent in interface.parents() { + if context.has_interface(parent) { + requirements.extend(context.interface_method_requirements_with_owners(parent)); + } + requirements.extend(builtin_interface_method_requirements(parent)); + } + requirements +} + +/// Returns whether a generated/AOT parent interface exposes a matching method. +pub(super) fn eval_aot_interface_parent_method_matches( + interface: &EvalInterface, + method: &EvalInterfaceMethod, + values: &mut impl RuntimeValueOps, +) -> Result { + for parent in interface.parents() { + if !values.interface_exists(parent)? { + continue; + } + let parent = parent.trim_start_matches('\\'); + if let Some(flags) = values.reflection_method_flags(parent, method.name())? { + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + return Ok(parent_method_is_static == method.is_static()); + } + } + Ok(false) +} + +/// Returns whether an interface method matches one inherited requirement signature kind. +pub(super) fn eval_interface_method_matches_requirement( + method: &EvalInterfaceMethod, + requirement: &EvalInterfaceMethod, +) -> bool { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() +} + +/// Rejects builtin attributes that cannot target eval-declared traits. +pub(super) fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(trait_decl.attributes())?; + for property in trait_decl.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in trait_decl.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Rejects builtin attributes that cannot target eval-declared enums. +pub(super) fn validate_eval_enum_attribute_targets(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(enum_decl.attributes()) +} + +/// Rejects class-only or method-only builtin attributes on non-class declarations. +pub(super) fn validate_eval_non_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only or method-only builtin attributes on non-method members. +pub(super) fn validate_eval_non_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only builtin attributes on method declarations. +pub(super) fn validate_eval_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Returns whether the attribute list contains one global builtin attribute. +pub(super) fn eval_attributes_have_global_builtin_attribute( + attributes: &[EvalAttribute], + builtin: &str, +) -> bool { + attributes + .iter() + .any(|attribute| eval_attribute_is_global_builtin(attribute, builtin)) +} + +/// Returns whether one attribute names a global builtin attribute class. +pub(super) fn eval_attribute_is_global_builtin(attribute: &EvalAttribute, builtin: &str) -> bool { + attribute + .name() + .trim_start_matches('\\') + .eq_ignore_ascii_case(builtin) +} + +/// Validates PHP's global `#[Override]` marker on one eval-declared method. +pub(super) fn validate_eval_override_attribute( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !eval_method_has_global_builtin_attribute(method, "Override") { + return Ok(()); + } + if eval_method_overrides_parent(class, method, context) + || eval_method_overrides_aot_parent(class, method, context, values)? + || eval_method_implements_interface(class, method, context, values)? + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether a method has a global builtin marker attribute. +pub(super) fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: &str) -> bool { + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) +} + +/// Returns whether an interface method has a global builtin marker attribute. +pub(super) fn eval_interface_method_has_global_builtin_attribute( + method: &EvalInterfaceMethod, + builtin: &str, +) -> bool { + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) +} + +/// Returns whether one method overrides a non-private parent method. +pub(super) fn eval_method_overrides_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> bool { + class + .parent() + .and_then(|parent| context.class_method(parent, method.name())) + .is_some_and(|(_, parent_method)| { + parent_method.visibility() != EvalVisibility::Private + && parent_method.is_static() == method.is_static() + }) +} + +/// Returns whether one method overrides a visible generated/AOT parent method. +pub(super) fn eval_method_overrides_aot_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = pending_class_native_parent_name(class, context) else { + return Ok(false); + }; + if !values.class_exists(&parent)? { + return Ok(false); + } + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { + return Ok(false); + }; + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_method_is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; + Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) +} + +/// Returns the nearest generated/AOT parent for a class not yet registered in context. +pub(super) fn pending_class_native_parent_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + let mut current = class.parent()?.to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + let resolved = context + .resolve_class_name(¤t) + .unwrap_or_else(|| current.trim_start_matches('\\').to_string()); + if !seen.insert(resolved.to_ascii_lowercase()) { + return None; + } + let Some(parent_class) = context.class(&resolved) else { + return Some(resolved.trim_start_matches('\\').to_string()); + }; + current = parent_class.parent()?.to_string(); + } +} + +/// Returns whether one method implements a direct or inherited interface method. +pub(super) fn eval_method_implements_interface( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if pending_class_interface_names(class, context) + .iter() + .filter(|interface| context.has_interface(interface)) + .any(|interface| { + context + .interface_method_requirements_with_owners(interface) + .into_iter() + .any(|(_, requirement)| { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() + }) + }) + { + return Ok(true); + } + Ok(pending_class_aot_interface_method_requirements(class, context, values)? + .iter() + .any(|requirement| { + requirement.name.eq_ignore_ascii_case(method.name()) + && requirement.is_static == method.is_static() + })) +} + +/// Validates PHP magic-method contracts for one eval class-like method list. +pub(super) fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { + for method in methods { + validate_eval_magic_method(method)?; + } + Ok(()) +} + +/// Validates staticness, visibility, arity, and declared return type for one eval magic method. +pub(super) fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { + let name = method.name().to_ascii_lowercase(); + if validated_eval_magic_method_rejects_by_ref_params(&name) { + validate_magic_no_by_ref_params(method)?; + } + match name.as_str() { + "__tostring" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::String)?; + } + "__get" | "__isset" | "__unset" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + if method.name().eq_ignore_ascii_case("__isset") { + validate_magic_declared_return_type(method, MagicReturnType::Bool)?; + } else if method.name().eq_ignore_ascii_case("__unset") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + } + "__set" | "__call" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + if method.name().eq_ignore_ascii_case("__set") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; + } + } + "__callstatic" => { + validate_magic_static(method)?; + validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; + } + "__sleep" | "__serialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Array)?; + } + "__wakeup" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + "__unserialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + "__debuginfo" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::NullableArray)?; + } + "__set_state" => { + validate_magic_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; + } + "__invoke" => { + validate_magic_non_static(method)?; + } + "__clone" | "__destruct" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + if method.name().eq_ignore_ascii_case("__clone") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_no_declared_return_type(method)?; + } + } + "__construct" => { + if method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_magic_no_declared_return_type(method)?; + } + _ => {} + } + Ok(()) +} + +/// Returns whether PHP rejects by-reference parameters for this magic method. +pub(super) fn validated_eval_magic_method_rejects_by_ref_params(name: &str) -> bool { + is_validated_eval_magic_method(name) && !matches!(name, "__construct" | "__invoke") +} + +/// Returns whether eval knows PHP declaration-time rules for this magic method. +pub(super) fn is_validated_eval_magic_method(name: &str) -> bool { + matches!( + name, + "__tostring" + | "__get" + | "__isset" + | "__unset" + | "__set" + | "__call" + | "__callstatic" + | "__sleep" + | "__serialize" + | "__wakeup" + | "__unserialize" + | "__debuginfo" + | "__set_state" + | "__invoke" + | "__clone" + | "__destruct" + | "__construct" + ) +} + +/// Magic method return types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +pub(super) enum MagicReturnType { + Array, + Bool, + NullableArray, + String, + Void, +} + +/// Magic method parameter types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +pub(super) enum MagicParamType { + Array, + String, +} + +/// Rejects static declarations for magic methods that must be instance methods. +pub(super) fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects instance declarations for magic methods that must be static methods. +pub(super) fn validate_magic_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects non-public declarations for public-only PHP magic methods. +pub(super) fn validate_magic_public(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.visibility() == EvalVisibility::Public { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects magic methods whose arity differs from PHP's required shape. +pub(super) fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), EvalStatus> { + let has_variadic = method + .parameter_is_variadic() + .iter() + .any(|is_variadic| *is_variadic); + if method.params().len() == expected && !has_variadic { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects by-reference parameters on PHP magic methods. +pub(super) fn validate_magic_no_by_ref_params(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method + .parameter_is_by_ref() + .iter() + .any(|is_by_ref| *is_by_ref) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects incompatible explicit parameter types on PHP magic methods. +pub(super) fn validate_magic_declared_param_type( + method: &EvalClassMethod, + position: usize, + expected: MagicParamType, +) -> Result<(), EvalStatus> { + let Some(Some(parameter_type)) = method.parameter_types().get(position) else { + return Ok(()); + }; + if magic_param_type_matches(parameter_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether one retained eval parameter type is exactly the expected magic atom. +pub(super) fn magic_param_type_matches( + parameter_type: &EvalParameterType, + expected: MagicParamType, +) -> bool { + if parameter_type.allows_null() || parameter_type.is_intersection() { + return false; + } + let [variant] = parameter_type.variants() else { + return false; + }; + matches!( + (expected, variant), + (MagicParamType::Array, EvalParameterTypeVariant::Array) + | (MagicParamType::String, EvalParameterTypeVariant::String) + ) +} + +/// Rejects PHP magic methods that cannot declare any return type. +pub(super) fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.return_type().is_some() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects incompatible explicit return types on PHP magic methods. +pub(super) fn validate_magic_declared_return_type( + method: &EvalClassMethod, + expected: MagicReturnType, +) -> Result<(), EvalStatus> { + method.return_type().map_or(Ok(()), |return_type| { + if magic_return_type_matches(return_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } + }) +} + +/// Returns whether one retained eval return type is exactly the expected magic return atom. +pub(super) fn magic_return_type_matches( + return_type: &EvalParameterType, + expected: MagicReturnType, +) -> bool { + if return_type.is_intersection() { + return false; + } + if return_type.allows_null() && !matches!(expected, MagicReturnType::NullableArray) { + return false; + } + let [variant] = return_type.variants() else { + return false; + }; + matches!( + (expected, variant), + (MagicReturnType::Array, EvalParameterTypeVariant::Array) + | (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) + | (MagicReturnType::NullableArray, EvalParameterTypeVariant::Array) + | (MagicReturnType::String, EvalParameterTypeVariant::String) + | (MagicReturnType::Void, EvalParameterTypeVariant::Void) + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/callable_objects.rs b/crates/elephc-magician/src/interpreter/statements/callable_objects.rs new file mode 100644 index 0000000000..3d80bbe81e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/callable_objects.rs @@ -0,0 +1,242 @@ +//! Purpose: +//! Dispatches invokable objects and magic instance/static calls. +//! +//! Called from: +//! - Callable and method dispatch after normal method lookup. +//! +//! Key details: +//! - `__invoke`, `__call`, and `__callStatic` receive normalized argument arrays. + +use super::*; + +/// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. +pub(in crate::interpreter) fn eval_invokable_object_call_result( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, "__invoke", evaluated_args); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + return eval_native_method_with_evaluated_args_unchecked( + object, + &class_name, + "__invoke", + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") + else { + if let Some(native_class_name) = + eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + { + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + &native_class_name, + "__invoke", + evaluated_args, + Some(&native_class_name), + Some(&called_class_name), + context, + values, + ); + } + return eval_throw_object_not_callable_error(&called_class_name, context, values); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ) +} + +/// Rejects non-invokable eval-declared objects before dynamic-call arguments are evaluated. +pub(in crate::interpreter) fn eval_invokable_object_precheck( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(()); + }; + let called_class_name = class.name().to_string(); + let Some((_, method)) = context.class_method(&called_class_name, "__invoke") else { + if eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + .is_some() + { + return Ok(()); + } + return eval_throw_object_not_callable_error(&called_class_name, context, values); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the generated/AOT class that can dispatch an inherited `__invoke()` hook. +pub(in crate::interpreter) fn eval_dynamic_class_native_invokable_method_class( + called_class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, _, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(called_class_name, "__invoke", context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + Ok(Some(declaring_class)) +} + +/// Returns generated/AOT method metadata inherited by an eval-declared class. +pub(in crate::interpreter) fn eval_dynamic_class_native_method_metadata( + called_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values) +} + +/// Dispatches a missing or inaccessible eval instance method through `__call()`. +pub(in crate::interpreter) fn eval_magic_instance_method_call( + object: RuntimeCellHandle, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(called_class_name, "__call") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Ok(None); + } + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_dynamic_method_with_values( + &declaring_class, + called_class_name, + &method, + object, + magic_args, + context, + values, + ) + .map(Some) +} + +/// Dispatches a missing or inaccessible eval static method through `__callStatic()`. +pub(in crate::interpreter) fn eval_magic_static_method_call( + class_name: &str, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(class_name, "__callStatic") else { + return Ok(None); + }; + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_dynamic_static_method_with_values( + &declaring_class, + called_class_name, + &method, + magic_args, + context, + values, + ) + .map(Some) +} + +/// Builds the two synthetic arguments passed to `__call()` and `__callStatic()`. +pub(super) fn eval_magic_call_args( + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method = values.string(method_name)?; + let args = eval_magic_call_arg_array(evaluated_args, values)?; + Ok(positional_args(vec![method, args])) +} + +/// Materializes PHP's `$args` array for a magic method fallback. +pub(super) fn eval_magic_call_arg_array( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let contains_named = evaluated_args.iter().any(|arg| arg.name.is_some()); + let mut args = if contains_named { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + let mut next_positional = 0_i64; + for arg in evaluated_args { + let key = if let Some(name) = arg.name { + values.string(&name)? + } else { + let key = values.int(next_positional)?; + next_positional = next_positional + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + }; + args = values.array_set(args, key, arg.value)?; + } + Ok(args) +} diff --git a/crates/elephc-magician/src/interpreter/statements/class_declarations.rs b/crates/elephc-magician/src/interpreter/statements/class_declarations.rs new file mode 100644 index 0000000000..f4905b5229 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/class_declarations.rs @@ -0,0 +1,123 @@ +//! Purpose: +//! Registers eval class declarations and validates their direct parent. +//! +//! Called from: +//! - Statement dispatch for class and anonymous-class declarations. +//! +//! Key details: +//! - Registration occurs only after trait expansion and declaration validation succeed. + +use super::*; + +/// Registers an eval-declared class in the dynamic class table. +pub(in crate::interpreter) fn execute_class_decl_stmt( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = class.name().trim_start_matches('\\'); + if context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); + let class = &class; + validate_eval_class_modifiers(class, context, values)?; + let native_parent = validate_eval_class_parent(class, context, values)?; + for interface in class.interfaces() { + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_class_does_not_implement_throwable_interfaces(class, context)?; + validate_eval_class_does_not_implement_enum_interfaces(class, context)?; + validate_declared_class_interface_members(class, context)?; + validate_declared_class_builtin_interface_members(class, context)?; + validate_declared_class_aot_interface_members(class, context, values)?; + if !class.is_abstract() { + validate_concrete_class_requirements(class, context)?; + validate_concrete_class_builtin_interface_requirements(class, context)?; + validate_concrete_class_aot_parent_requirements(class, context, values)?; + validate_concrete_class_aot_interface_requirements(class, context, values)?; + } + if context.define_class(class.clone()) { + if let Some(parent) = native_parent.as_deref() { + if !context.define_native_class_parent(class.name(), parent) { + return Err(EvalStatus::RuntimeFatal); + } + } + initialize_eval_declared_constants( + class.name(), + class.constants(), + context, + scope, + values, + )?; + initialize_eval_static_properties(class, context, scope, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Validates an eval class parent and returns an AOT parent name when the parent is runtime-backed. +pub(super) fn validate_eval_class_parent( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(None); + }; + let parent = context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + if let Some(parent_class) = context.class(&parent) { + if parent_class.is_final() + || parent_class.is_readonly_class() != class.is_readonly_class() + || context.class_is_a(&parent, class.name(), false) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(None); + } + let Some((parent_is_final, parent_is_readonly)) = + eval_reflection_aot_class_inheritance_modifiers(&parent, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if parent_is_final + || parent_is_readonly != class.is_readonly_class() + || native_class_is_a(&parent, class.name(), context) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(parent)) +} + +/// Registers one eval anonymous class expression if this execution has not seen it yet. +pub(in crate::interpreter) fn ensure_eval_anonymous_class_decl( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !class.is_anonymous() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(existing) = context.class(class.name()) { + return if existing.is_anonymous() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + }; + } + execute_class_decl_stmt(class, context, scope, values) +} diff --git a/crates/elephc-magician/src/interpreter/statements/class_resolution.rs b/crates/elephc-magician/src/interpreter/statements/class_resolution.rs new file mode 100644 index 0000000000..b16c5f66de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/class_resolution.rs @@ -0,0 +1,512 @@ +//! Purpose: +//! Resolves class-like names and allocates or clones dynamic objects. +//! +//! Called from: +//! - New-object, clone, and static-member dispatch paths. +//! +//! Key details: +//! - self/parent/static scope, native parents, constructor modes, and clone hooks are resolved here. + +use super::*; + +/// Resolves a static method using private-method scope rules. +pub(in crate::interpreter) fn eval_dynamic_static_method_for_call( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if eval_classes_are_related(current_class, class_name, context) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(class_name, method_name) +} + +/// Resolves `self`, `parent`, and `static` for eval static member access. +pub(in crate::interpreter) fn resolve_eval_static_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "static" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(|parent| { + context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()) + }) + .or_else(|| context.native_class_parent(current).map(str::to_string)) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => context + .resolve_class_name(class_name) + .or_else(|| { + context + .has_class(class_name) + .then(|| class_name.to_string()) + }) + .ok_or(EvalStatus::RuntimeFatal), + } +} + +/// Resolved static method dispatch metadata preserving PHP late-static forwarding. +pub(in crate::interpreter) struct EvalStaticMethodReceiver { + pub(in crate::interpreter) dispatch_class: String, + pub(in crate::interpreter) called_class: String, +} + +/// Resolves static method receivers into lookup and late-static called-class names. +pub(in crate::interpreter) fn resolve_eval_static_method_receiver( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal)?, + "static" => dispatch_class.clone(), + _ => dispatch_class.clone(), + }; + Ok(EvalStaticMethodReceiver { + dispatch_class, + called_class, + }) +} + +/// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. +pub(in crate::interpreter) fn resolve_eval_static_member_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Returns true when an eval-declared class-like symbol should not fall through to AOT lookup. +pub(super) fn eval_static_member_context_owns_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.has_class(class_name) + || context.has_interface(class_name) + || context.has_trait(class_name) + || context.has_enum(class_name) +} + +/// Returns whether a static member receiver exists in eval metadata or generated metadata. +pub(super) fn eval_runtime_class_like_exists( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_static_member_context_owns_class(class_name, context) + || values.class_exists(class_name)? + || eval_runtime_interface_exists(class_name, values)? + || values.trait_exists(class_name)? + || values.enum_exists(class_name)?) +} + +/// Resolves class-name literal receivers without requiring named classes to exist. +pub(super) fn resolve_eval_class_name_literal( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Creates a backing object for an eval-declared class and runs its constructor. +pub(in crate::interpreter) fn eval_dynamic_class_new_object( + class: &EvalClass, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_class_new_object_with_ref_mode( + class, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + caller_scope, + values, + ) +} + +/// Creates an eval-declared object while using the selected constructor by-ref mode. +pub(super) fn eval_dynamic_class_new_object_with_ref_mode( + class: &EvalClass, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_dynamic_class_allocate_object(class, context, caller_scope, values)?; + if let Some((constructor_class, constructor)) = + context.class_method(class.name(), "__construct") + { + if validate_eval_member_access(&constructor_class, constructor.visibility(), context) + .is_err() + { + let _ = values.release(object); + return eval_throw_method_access_error( + &constructor_class, + constructor.name(), + constructor.visibility(), + context, + values, + ); + } + let result = eval_dynamic_method_with_values_and_ref_mode( + &constructor_class, + class.name(), + &constructor, + object, + constructor.parameter_is_by_ref(), + evaluated_args, + by_ref_mode, + context, + values, + )?; + eval_release_value(context, values, result)?; + } else if !evaluated_args.is_empty() { + if let Some(parent) = context.class_native_parent_name(class.name()) { + eval_native_constructor_with_evaluated_args_and_ref_mode( + &parent, + object, + evaluated_args, + by_ref_mode, + context, + values, + )?; + } else { + return Err(EvalStatus::RuntimeFatal); + } + } else if let Some(parent) = context.class_native_parent_name(class.name()) { + if eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + "__construct", + context, + values, + )? + .is_some() + { + eval_native_constructor_with_evaluated_args_and_ref_mode( + &parent, + object, + Vec::new(), + by_ref_mode, + context, + values, + )?; + } + } + Ok(object) +} + +/// Creates a PHP shallow clone and invokes an eval-declared `__clone()` hook when present. +pub(in crate::interpreter) fn eval_object_clone_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + let dynamic_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()); + let clone_method = dynamic_class_name + .as_deref() + .and_then(|class_name| context.class_method(class_name, "__clone")); + if let Some((declaring_class, method)) = &clone_method { + if validate_eval_member_access(declaring_class, method.visibility(), context).is_err() { + return eval_throw_clone_access_error( + declaring_class, + method.visibility(), + context, + values, + ); + } + } + let dynamic_native_clone_hook_scope = if clone_method.is_none() { + if let Some(class_name) = dynamic_class_name.as_deref() { + eval_dynamic_native_clone_hook_is_callable(class_name, context, values)? + } else { + None + } + } else { + None + }; + let should_call_aot_clone_hook = if dynamic_class_name.is_none() { + eval_aot_clone_hook_is_callable(object, context, values)? + } else { + false + }; + + let clone = values.object_clone_shallow(object)?; + if let Some(class_name) = dynamic_class_name { + let clone_identity = values.object_identity(clone)?; + context.register_dynamic_object(clone_identity, &class_name); + context.clone_dynamic_property_aliases(identity, clone_identity); + if let Some((declaring_class, method)) = clone_method { + let result = eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + clone, + Vec::new(), + context, + values, + )?; + eval_release_value(context, values, result)?; + } else if let Some(scope) = dynamic_native_clone_hook_scope { + let result = eval_native_method_call_with_scope( + &scope, + None, + clone, + "__clone", + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + } else if should_call_aot_clone_hook { + let result = values.method_call(clone, "__clone", Vec::new())?; + values.release(result)?; + } + Ok(clone) +} + +/// Returns the declaring scope for an inherited generated/AOT `__clone()` hook. +pub(super) fn eval_dynamic_native_clone_hook_is_callable( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(class_name, "__clone", context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } + Ok(Some(declaring_class)) +} + +/// Calls one generated/AOT method while presenting an explicit PHP class scope to the bridge. +pub(super) fn eval_native_method_call_with_scope( + scope: &str, + called_class_scope: Option<&str>, + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); + let result = values.method_call(object, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } + context.pop_class_scope(); + result +} + +/// Calls one generated/AOT static method while presenting an explicit PHP class scope. +pub(super) fn eval_native_static_method_call_with_scope( + scope: &str, + called_class_scope: Option<&str>, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); + let result = values.static_method_call(class_name, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } + context.pop_class_scope(); + result +} + +/// Runs one generated/AOT bridge operation while exposing an explicit PHP class scope. +pub(super) fn eval_with_native_bridge_scope( + scope: &str, + context: &mut ElephcEvalContext, + call: impl FnOnce() -> Result, +) -> Result { + context.push_class_scope(scope.to_string()); + let result = call(); + context.pop_class_scope(); + result +} + +/// Returns generated/AOT property metadata inherited by an eval-declared class. +pub(super) fn eval_dynamic_class_native_property_metadata( + called_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_reflection_aot_property_access_metadata(&parent, property_name, values) +} + +/// Returns generated/AOT class-constant metadata inherited by an eval-declared class. +pub(super) fn eval_dynamic_class_native_constant_metadata( + called_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + let Some(flags) = values.reflection_constant_flags(&parent, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(&parent, constant_name)? + .unwrap_or(parent); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some((declaring_class, visibility))) +} + +/// Returns whether an accessible instance AOT `__clone()` hook should run. +pub(super) fn eval_aot_clone_hook_is_callable( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_runtime_object_class_name(object, values)?; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, "__clone", values)? + else { + return Ok(false); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } + Ok(true) +} + +/// Reads the PHP-visible runtime class name for one AOT object handle. +pub(super) fn eval_runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Creates a backing object for an eval-declared class without running its constructor. +pub(super) fn eval_dynamic_class_allocate_object( + class: &EvalClass, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if class.is_abstract() || context.has_enum(class.name()) { + return Err(EvalStatus::RuntimeFatal); + } + let backing_class = context + .class_native_parent_name(class.name()) + .unwrap_or_else(|| String::from("stdClass")); + let object = values.new_object(&backing_class)?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, class.name()); + let mut class_chain = context.class_chain(class.name()); + if class_chain.is_empty() { + class_chain.push(class.clone()); + } + for class in &class_chain { + for property in class + .properties() + .iter() + .filter(|property| !property.is_static() && !property.is_abstract()) + { + let value = if let Some(default) = property.default() { + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + caller_scope, + values, + )?) + } else if property.property_type().is_none() { + Some(values.null()?) + } else { + None + }; + let storage_name = eval_instance_property_storage_name(class.name(), property); + if let Some(value) = value { + values.property_set(object, &storage_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_name); + } + } + } + Ok(object) +} diff --git a/crates/elephc-magician/src/interpreter/statements/closure_binding.rs b/crates/elephc-magician/src/interpreter/statements/closure_binding.rs new file mode 100644 index 0000000000..4d3df3b96d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/closure_binding.rs @@ -0,0 +1,451 @@ +//! Purpose: +//! Implements builtin Closure static methods and binding transformations. +//! +//! Called from: +//! - Static method dispatch for the eval Closure class surface. +//! +//! Key details: +//! - Receiver, scope, called class, and unbound targets are validated before cloning closures. + +use super::*; + +/// Dispatches static methods for eval's builtin `Closure` class slice. +pub(super) fn eval_closure_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return Ok(None); + } + if method_name.eq_ignore_ascii_case("fromCallable") { + return eval_closure_from_callable(evaluated_args, lexical_scope, context, values) + .map(Some); + } + if method_name.eq_ignore_ascii_case("bind") { + return eval_closure_bind_static(evaluated_args, context, values).map(Some); + } + Ok(None) +} + +/// Materializes `Closure::fromCallable()` from one normalized eval callback. +pub(super) fn eval_closure_from_callable( + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; + let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let callable = match lexical_scope { + Some(scope) => eval_callable_from_scope(callback, context, scope, values), + None => eval_callable(callback, context, values), + }; + let callable = match callable { + Ok(callable) => callable, + Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { + return eval_closure_from_callable_type_error( + "no array or string given", + context, + values, + ); + } + Err(status) => return Err(status), + }; + eval_validate_closure_from_callable_callback(&callable, context, values)?; + let target = eval_closure_object_target_from_callable(callable); + eval_closure_object_from_target(target, context, values) +} + +/// Converts a normalized callable target into the storage used by eval Closure objects. +pub(super) fn eval_closure_object_target_from_callable( + callable: EvaluatedCallable, +) -> EvalClosureObjectTarget { + match callable { + EvaluatedCallable::Named { name, .. } => EvalClosureObjectTarget::Named(name), + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + }, + EvaluatedCallable::InvokableObject { object } => { + EvalClosureObjectTarget::InvokableObject { object } + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + } +} + +/// Allocates a PHP-visible eval Closure object for one retained callable target. +pub(super) fn eval_closure_object_from_target( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object_target(identity, target); + Ok(object) +} + +/// Materializes `Closure::bind()` from a closure object and a persistent receiver. +pub(super) fn eval_closure_bind_static( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (target, bound_this, bound_scope, rebinds_function_scope) = + eval_closure_bind_static_args(evaluated_args, context, values)?; + eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) +} + +/// Binds static `Closure::bind()` arguments to their PHP parameter slots. +pub(super) fn eval_closure_bind_static_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result< + ( + EvalClosureObjectTarget, + Option, + Option, + bool, + ), + EvalStatus, +> { + let bound = eval_closure_bind_args( + &["closure", "newThis", "newScope"], + 2, + evaluated_args, + )?; + let closure = required_closure_bind_arg(&bound, 0)?; + let new_this = required_closure_bind_arg(&bound, 1)?; + let target = eval_closure_target_arg(closure.value, context, values)?; + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; + Ok((target, bound_this, bound_scope, rebinds_function_scope)) +} + +/// Binds `Closure::bindTo()` arguments to their PHP parameter slots. +pub(super) fn eval_closure_bind_to_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option, bool), EvalStatus> { + let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; + let new_this = required_closure_bind_arg(&bound, 0)?; + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; + Ok((bound_this, bound_scope, rebinds_function_scope)) +} + +/// Binds positional and named Closure binding arguments while accepting optional scope. +pub(super) fn eval_closure_bind_args( + params: &[&str], + required_count: usize, + evaluated_args: Vec, +) -> Result>, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + let mut saw_named = false; + + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + saw_named = true; + let Some(index) = params + .iter() + .position(|param| param.eq_ignore_ascii_case(name)) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[index] = Some(arg); + continue; + } + + if saw_named || next_positional >= params.len() || bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg); + next_positional += 1; + } + + if bound_args + .iter() + .take(required_count) + .any(Option::is_none) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(bound_args) +} + +/// Returns one required Closure binding argument. +pub(super) fn required_closure_bind_arg( + bound_args: &[Option], + index: usize, +) -> Result<&EvaluatedCallArg, EvalStatus> { + bound_args + .get(index) + .and_then(Option::as_ref) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Extracts a stored eval Closure object target from a runtime object. +pub(super) fn eval_closure_target_arg( + closure: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(closure)?; + context + .closure_object_target(identity) + .cloned() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts the `newThis` binding argument to an optional object receiver. +pub(super) fn eval_closure_bind_receiver_arg( + new_this: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(new_this)? { + return Ok(None); + } + if values.type_tag(new_this)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(new_this)) +} + +/// Converts `newScope` into class scope plus whether function scope was rebound. +pub(super) fn eval_closure_bind_scope_arg( + new_scope: Option<&Option>, + bound_this: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, bool), EvalStatus> { + let Some(new_scope) = new_scope.and_then(Option::as_ref) else { + return Ok((None, false)); + }; + if values.is_null(new_scope.value)? { + return Ok((None, false)); + } + if values.type_tag(new_scope.value)? == EVAL_TAG_OBJECT { + return eval_closure_bound_object_class_name(new_scope.value, context, values) + .map(|scope| (Some(scope), true)); + } + let bytes = values.string_bytes(new_scope.value)?; + let scope = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if scope.eq_ignore_ascii_case("static") { + let Some(bound_this) = bound_this else { + return Ok((None, false)); + }; + return eval_closure_bound_object_class_name(bound_this, context, values) + .map(|scope| (Some(scope), false)); + } + Ok(( + Some( + context + .resolve_class_name(&scope) + .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), + ), + true, + )) +} + +/// Creates a new Closure object with persistent binding metadata when supported. +pub(super) fn eval_closure_bind_target( + target: EvalClosureObjectTarget, + bound_this: Option, + bound_scope: Option, + rebinds_function_scope: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(bound_this) = bound_this else { + return eval_closure_unbind_target( + target, + bound_scope, + rebinds_function_scope, + context, + values, + ); + }; + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + let Some(closure) = context.closure(&name) else { + if eval_function_probe_exists(context, &name) { + if rebinds_function_scope { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } + return eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { + name, + bound_this: Some(bound_this), + bound_scope, + }, + context, + values, + ); + } + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + }; + if closure.is_static() { + return eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { + name, + bound_this: Some(bound_this), + bound_scope, + }, + context, + values, + ) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_bind_bound_class_matches_method( + object, "__invoke", bound_this, context, values, + )? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::InvokableObject { object: bound_this }, + context, + values, + ) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + native_class, + bridge_scope, + .. + } => { + if !eval_closure_bind_bound_class_matches_method( + object, &method, bound_this, context, values, + )? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + eval_closure_object_from_target( + EvalClosureObjectTarget::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }, + context, + values, + ) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ), + } +} + +/// Creates an unbound Closure object for targets that can drop `$this`. +pub(super) fn eval_closure_unbind_target( + target: EvalClosureObjectTarget, + bound_scope: Option, + rebinds_function_scope: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + if rebinds_function_scope + && context.closure(&name).is_none() + && eval_function_probe_exists(context, &name) + { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } + let target = match bound_scope { + Some(bound_scope) => EvalClosureObjectTarget::BoundNamed { + name, + bound_this: None, + bound_scope: Some(bound_scope), + }, + None => EvalClosureObjectTarget::Named(name), + }; + eval_closure_object_from_target(target, context, values) + } + EvalClosureObjectTarget::InvokableObject { .. } + | EvalClosureObjectTarget::ObjectMethod { .. } => { + eval_closure_call_warning_null("Cannot unbind $this of method", values) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot unbind $this of static method", + values, + ), + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/dispatch.rs b/crates/elephc-magician/src/interpreter/statements/dispatch.rs new file mode 100644 index 0000000000..572e7e6a3c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/dispatch.rs @@ -0,0 +1,617 @@ +//! Purpose: +//! Dispatches each EvalIR statement variant to its focused execution helper and +//! propagates structured loop, throw, and return control flow. +//! +//! Called from: +//! - `crate::interpreter::execute_program_outcome_with_context()`. +//! - Dynamic eval function and method execution. +//! +//! Key details: +//! - The exhaustive match remains centralized so every new `EvalStmt` variant +//! must explicitly define its runtime control-flow behavior. + +use super::*; + +/// Executes statements in source order and propagates the first eval `return`. +pub(in crate::interpreter) fn execute_statements( + statements: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + for stmt in statements { + match execute_stmt(stmt, context, scope, values)? { + EvalControl::None => {} + control => return Ok(control), + } + } + Ok(EvalControl::None) +} + +/// Executes one statement and returns `Some` only for eval `return`. +pub(in crate::interpreter) fn execute_stmt( + stmt: &EvalStmt, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match stmt { + EvalStmt::ArrayAppendVar { name, value } => { + eval_array_append_var_stmt(name, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::ArraySetVar { name, index, value } => { + eval_array_set_var_stmt(name, index, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::Break => Ok(EvalControl::Break), + EvalStmt::Continue => Ok(EvalControl::Continue), + EvalStmt::DoWhile { body, condition } => { + execute_do_while_stmt(body, condition, context, scope, values) + } + EvalStmt::Echo(expr) => { + let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + values.echo(value)?; + Ok(EvalControl::None) + } + EvalStmt::For { + init, + condition, + update, + body, + } => execute_for_stmt( + init, + condition.as_ref(), + update, + body, + context, + scope, + values, + ), + EvalStmt::ClassDecl(class) => { + execute_class_decl_stmt(class, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::EnumDecl(enum_decl) => { + execute_enum_decl_stmt(enum_decl, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::InterfaceDecl(interface) => { + execute_interface_decl_stmt(interface, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::TraitDecl(trait_decl) => { + execute_trait_decl_stmt(trait_decl, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::Foreach { + array, + key_name, + value_name, + body, + } => execute_foreach_stmt( + array, + key_name.as_deref(), + value_name, + body, + context, + scope, + values, + ), + EvalStmt::FunctionDecl { + name, + source_location, + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type, + body, + } => { + let key = name.to_ascii_lowercase(); + let mut function = EvalFunction::new(name.clone(), params.clone(), body.clone()) + .with_attributes(attributes.clone()) + .with_parameter_attributes(parameter_attributes.clone()) + .with_parameter_types(parameter_types.clone()) + .with_parameter_defaults(parameter_defaults.clone()) + .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) + .with_parameter_variadic_flags(parameter_is_variadic.clone()) + .with_return_type(return_type.clone()); + if let Some(source_location) = source_location { + function = function.with_source_location(*source_location); + } + context + .define_function(key, function) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvalControl::None) + } + EvalStmt::Global { vars } => { + execute_global_stmt(vars, context, scope)?; + Ok(EvalControl::None) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + execute_statements(then_branch, context, scope, values) + } else { + execute_statements(else_branch, context, scope, values) + } + } + EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( + expr, context, scope, values, + )?)), + EvalStmt::Return(None) => Ok(EvalControl::ReturnVoid), + EvalStmt::ReferenceAssign { target, source } => { + for replaced in set_reference_alias(context, scope, target, source, values)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::PropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_reference_bind_result(object, property, source, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_reference_bind_result( + object, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_append_result(object, &property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_set_result( + object, &property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let current = eval_property_get_result(object, &property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_inc_dec_result(object, &property, *increment, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticVar { name, init } => { + execute_static_var_stmt(name, init, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_property_set_result(object, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_append_result(object, property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_set_result( + object, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::PropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let current = eval_property_get_result(object, property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_inc_dec_result(object, property, *increment, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertySet { + class_name, + property, + value, + } => { + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(class_name, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + } => { + eval_static_property_reference_bind_result( + class_name, property, source, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + } => { + eval_static_property_array_append_result( + class_name, property, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + eval_static_property_array_set_result( + class_name, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyIncDec { + class_name, + property, + increment, + } => { + eval_static_property_inc_dec_result( + class_name, property, *increment, context, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_reference_bind_result( + &class_name, + property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_append_result( + &class_name, + property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_set_result( + &class_name, + property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_inc_dec_result( + &class_name, + property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_reference_bind_result( + &class_name, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_append_result( + &class_name, + &property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_set_result( + &class_name, + &property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_inc_dec_result( + &class_name, + &property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StoreVar { name, value } => { + let value = eval_expr(value, context, scope, values)?; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Owned, + )? { + eval_release_value(context, values, replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::Switch { expr, cases } => { + execute_switch_stmt(expr, cases, context, scope, values) + } + EvalStmt::Throw(expr) => { + let thrown = eval_expr(expr, context, scope, values)?; + if values.type_tag(thrown)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(EvalControl::Throw(thrown)) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => execute_try_stmt(body, catches, finally_body, context, scope, values), + EvalStmt::UnsetArrayElement { array, index } => { + eval_array_unset_element_stmt(array, index, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetStaticProperty { + class_name, + property, + } => { + eval_static_property_unset_result(class_name, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicStaticProperty { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_unset_result(&class_name, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_unset_result(&class_name, &property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetVar { name } => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + eval_release_value(context, values, replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::While { condition, body } => { + while { + let condition = eval_expr(condition, context, scope, values)?; + values.truthy(condition)? + } { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) + } + EvalStmt::Expr(expr) => { + let result = eval_expr(expr, context, scope, values)?; + eval_release_value(context, values, result)?; + Ok(EvalControl::None) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs b/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs new file mode 100644 index 0000000000..1d3ec07aef --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs @@ -0,0 +1,283 @@ +//! Purpose: +//! Executes eval-declared instance and static methods from prepared argument values. +//! +//! Called from: +//! - Method dispatch and callable invocation after target resolution. +//! +//! Key details: +//! - Called-class scope, reference modes, positional extraction, and writeback stay paired. + +use super::*; + +/// Executes one eval-declared class method with `$this` bound in method scope. +pub(in crate::interpreter) fn eval_dynamic_method_with_values( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + object, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + object, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + method.params(), + method.parameter_types(), + method.parameter_defaults(), + parameter_is_by_ref, + method.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; + let mut method_scope = ElephcEvalScope::new(); + method_scope.set("this", object, ScopeCellOwnership::Borrowed); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut method_scope, + method.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return_result +} + +/// Executes one eval-declared static class method without binding `$this`. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + method.params(), + method.parameter_types(), + method.parameter_defaults(), + parameter_is_by_ref, + method.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; + let mut method_scope = ElephcEvalScope::new(); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut method_scope, + method.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return_result +} + +/// Wraps positional method arguments into the shared dynamic-call binding shape. +pub(in crate::interpreter) fn positional_args( + args: Vec, +) -> Vec { + args.into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect() +} + +/// Extracts positional runtime values and rejects named args before runtime method dispatch. +pub(in crate::interpreter) fn positional_evaluated_arg_values( + args: Vec, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.into_iter().map(|arg| arg.value).collect()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs b/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs new file mode 100644 index 0000000000..ef167c5a73 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs @@ -0,0 +1,392 @@ +//! Purpose: +//! Validates, registers, and initializes eval-declared enums. +//! +//! Called from: +//! - Statement dispatch for enum declarations. +//! +//! Key details: +//! - Trait expansion, synthetic methods, backing values, cases, and static defaults stay ordered. + +use super::*; + +/// Registers an eval-declared enum and materializes its singleton cases. +pub(in crate::interpreter) fn execute_enum_decl_stmt( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = enum_decl.name().trim_start_matches('\\'); + if context.has_enum(name) + || context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || values.enum_exists(name)? + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_enum_direct_method_declarations(enum_decl)?; + let enum_decl = expand_eval_enum_traits(enum_decl, context)?; + let enum_decl = &enum_decl; + validate_eval_enum_decl(enum_decl, context, values)?; + if context.define_enum(enum_decl.clone()) { + initialize_eval_declared_constants( + enum_decl.name(), + enum_decl.constants(), + context, + scope, + values, + )?; + initialize_eval_enum_cases(enum_decl, context, scope, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Expands eval trait uses into enum metadata while rejecting imported properties. +pub(super) fn expand_eval_enum_traits( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, +) -> Result { + if enum_decl.traits().is_empty() { + return Ok(enum_decl.clone()); + } + let enum_class = enum_decl.as_class_metadata(); + validate_eval_trait_adaptations(&enum_class, context)?; + let mut enum_method_names = class_method_name_set(&enum_class); + insert_eval_enum_synthetic_method_names(enum_decl, &mut enum_method_names); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); + let mut trait_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in enum_decl.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + trait_decl, + enum_decl.constants(), + &mut trait_constants, + &mut constants, + )?; + append_eval_trait_properties( + trait_decl, + &[], + &mut trait_properties, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + enum_decl.trait_adaptations(), + &enum_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + if !properties.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + constants.extend(enum_decl.constants().iter().cloned()); + methods.extend(enum_decl.methods().iter().cloned()); + let mut expanded = EvalEnum::with_members_traits_adaptations( + enum_decl.name().to_string(), + enum_decl.backing_type(), + enum_decl.interfaces().to_vec(), + enum_decl.cases().to_vec(), + constants, + methods, + enum_decl.traits().to_vec(), + enum_decl.trait_adaptations().to_vec(), + ) + .with_attributes(enum_decl.attributes().to_vec()); + if let Some(source_location) = enum_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + +/// Adds PHP's enum-provided method names to the set that hides trait imports. +pub(super) fn insert_eval_enum_synthetic_method_names( + enum_decl: &EvalEnum, + method_names: &mut std::collections::HashSet, +) { + method_names.insert(String::from("cases")); + if enum_decl.backing_type().is_some() { + method_names.insert(String::from("from")); + method_names.insert(String::from("tryfrom")); + } +} + +/// Validates enum metadata before it is inserted into the dynamic context. +pub(super) fn validate_eval_enum_decl( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + validate_eval_enum_attribute_targets(enum_decl)?; + validate_eval_declared_constants(enum_decl.constants())?; + validate_eval_enum_case_declarations(enum_decl)?; + validate_eval_enum_forbidden_magic_methods(enum_decl)?; + let enum_class = enum_decl.as_class_metadata(); + validate_eval_class_modifiers(&enum_class, context, values)?; + validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; + validate_declared_class_builtin_interface_members(&enum_class, context)?; + validate_declared_class_aot_interface_members(&enum_class, context, values)?; + validate_concrete_class_builtin_interface_requirements(&enum_class, context)?; + validate_concrete_class_aot_interface_requirements(&enum_class, context, values)?; + validate_concrete_class_requirements(&enum_class, context) +} + +/// Validates PHP's special enum interface rules for one eval enum declaration. +pub(super) fn validate_eval_enum_interfaces( + enum_decl: &EvalEnum, + enum_class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for interface in enum_decl.interfaces() { + if eval_builtin_enum_interface_name(interface) { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_class_does_not_implement_throwable_interfaces(enum_class, context)?; + if enum_decl.backing_type().is_none() + && pending_class_interface_names(enum_class, context) + .iter() + .any(|interface| eval_builtin_backed_enum_interface_name(interface)) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates enum case names and pure/backed declaration shape. +pub(super) fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + let mut case_names = std::collections::HashSet::new(); + let constant_names = enum_decl + .constants() + .iter() + .map(|constant| constant.name().to_string()) + .collect::>(); + for case in enum_decl.cases() { + validate_eval_non_method_attribute_targets(case.attributes())?; + if !case_names.insert(case.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if constant_names.contains(case.name()) { + return Err(EvalStatus::RuntimeFatal); + } + match (enum_decl.backing_type(), case.value()) { + (None, None) | (Some(_), Some(_)) => {} + (None, Some(_)) | (Some(_), None) => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(()) +} + +/// Validates direct enum methods that PHP reserves on enum declarations. +pub(super) fn validate_eval_enum_direct_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if method.name().eq_ignore_ascii_case("cases") { + return Err(EvalStatus::RuntimeFatal); + } + if enum_decl.backing_type().is_some() + && (method.name().eq_ignore_ascii_case("from") + || method.name().eq_ignore_ascii_case("tryFrom")) + { + return Err(EvalStatus::RuntimeFatal); + } + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates enum methods, including trait imports, that PHP forbids by magic name. +pub(super) fn validate_eval_enum_forbidden_magic_methods(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns whether PHP forbids this magic method name inside enum declarations. +pub(super) fn is_forbidden_eval_enum_magic_method(name: &str) -> bool { + [ + "__construct", + "__destruct", + "__clone", + "__get", + "__set", + "__isset", + "__unset", + "__sleep", + "__wakeup", + "__serialize", + "__unserialize", + "__toString", + "__debugInfo", + "__set_state", + ] + .iter() + .any(|method| name.eq_ignore_ascii_case(method)) +} + +/// Initializes enum singleton case objects for a newly declared eval enum. +pub(super) fn initialize_eval_enum_cases( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut backing_values = Vec::new(); + for case in enum_decl.cases() { + let backing_value = if let Some(value_expr) = case.value() { + let value = eval_expr(value_expr, context, scope, values)?; + validate_eval_enum_backing_value(enum_decl.backing_type(), value, values)?; + for existing in &backing_values { + let equal = values.compare(EvalBinOp::StrictEq, value, *existing)?; + if values.truthy(equal)? { + return Err(EvalStatus::RuntimeFatal); + } + } + backing_values.push(value); + Some(value) + } else { + None + }; + initialize_eval_enum_case(enum_decl, case, backing_value, context, values)?; + } + Ok(()) +} + +/// Validates that one evaluated enum backing value matches the declared backing type. +pub(super) fn validate_eval_enum_backing_value( + backing_type: Option, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(backing_type) = backing_type else { + return Err(EvalStatus::RuntimeFatal); + }; + let tag = values.type_tag(value)?; + match backing_type { + EvalEnumBackingType::Int if tag == EVAL_TAG_INT => Ok(()), + EvalEnumBackingType::String if tag == EVAL_TAG_STRING => Ok(()), + EvalEnumBackingType::Int | EvalEnumBackingType::String => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates and stores one enum case singleton object. +pub(super) fn initialize_eval_enum_case( + enum_decl: &EvalEnum, + case: &EvalEnumCase, + backing_value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, enum_decl.name()); + let name = values.string(case.name())?; + values.property_set(object, "name", name)?; + if let Some(value) = backing_value { + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value(enum_decl.name(), case.name(), value) { + values.release(replaced)?; + } + } + if let Some(replaced) = context.set_enum_case(enum_decl.name(), case.name(), object) { + values.release(replaced)?; + } + Ok(()) +} + +/// Initializes class-like constant cells for a newly declared eval class-like. +pub(super) fn initialize_eval_declared_constants( + owner_name: &str, + constants: &[EvalClassConstant], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for constant in constants { + let value = eval_class_like_member_default( + owner_name, + constant.trait_origin(), + constant.value(), + context, + scope, + values, + )?; + if let Some(replaced) = context.set_class_constant_cell(owner_name, constant.name(), value) + { + values.release(replaced)?; + } + } + Ok(()) +} + +/// Evaluates a class-like constant or property initializer with PHP magic scope. +pub(super) fn eval_class_like_member_default( + owner_name: &str, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let trait_name = trait_origin.or_else(|| context.has_trait(owner_name).then_some(owner_name)); + context.push_class_like_member_magic_scope(owner_name, trait_name); + let result = eval_expr(default, context, scope, values); + context.pop_magic_scope(); + result +} + +/// Initializes static property cells for a newly declared eval class. +pub(super) fn initialize_eval_static_properties( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for property in class + .properties() + .iter() + .filter(|property| property.is_static()) + { + let value = if let Some(default) = property.default() { + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + scope, + values, + )?) + } else if property.property_type().is_none() { + Some(values.null()?) + } else { + None + }; + if let Some(value) = value { + if let Some(replaced) = + context.set_static_property(class.name(), property.name(), value) + { + values.release(replaced)?; + } + } + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/exceptions.rs b/crates/elephc-magician/src/interpreter/statements/exceptions.rs new file mode 100644 index 0000000000..0a225b5e82 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/exceptions.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Executes try, catch, and finally control-flow semantics. +//! +//! Called from: +//! - Statement dispatch for EvalIR exception handlers. +//! +//! Key details: +//! - Finally control overrides and throwable interface matching remain explicit. + +use super::*; + +/// Executes an eval `try` body and handles supported `catch` clauses. +pub(in crate::interpreter) fn execute_try_stmt( + body: &[EvalStmt], + catches: &[EvalCatch], + finally_body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let control = match execute_statements(body, context, scope, values) { + Ok(EvalControl::Throw(thrown)) => { + execute_matching_catch(thrown, catches, context, scope, values)? + } + Err(EvalStatus::UncaughtThrowable) => { + let Some(thrown) = context.take_pending_throw() else { + return Err(EvalStatus::UncaughtThrowable); + }; + execute_matching_catch(thrown, catches, context, scope, values)? + } + Ok(control) => control, + Err(status) => return Err(status), + }; + if finally_body.is_empty() { + return Ok(control); + } + match execute_statements(finally_body, context, scope, values) { + Ok(EvalControl::None) => Ok(control), + Ok(finally_control) => { + release_overridden_control(control, values)?; + Ok(finally_control) + } + Err(status) => { + release_overridden_control(control, values)?; + Err(status) + } + } +} + +/// Releases a pending control-flow value when `finally` replaces that action. +pub(in crate::interpreter) fn release_overridden_control( + control: EvalControl, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match control { + EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), + EvalControl::None + | EvalControl::ReturnVoid + | EvalControl::Break + | EvalControl::Continue => Ok(()), + } +} + +/// Executes the first supported catch clause for a thrown eval object. +pub(in crate::interpreter) fn execute_matching_catch( + thrown: RuntimeCellHandle, + catches: &[EvalCatch], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut matched = None; + for catch in catches { + if catch_types_match_thrown(thrown, &catch.class_names, context, values)? { + matched = Some(catch); + break; + } + } + let Some(catch) = matched else { + return Ok(EvalControl::Throw(thrown)); + }; + if let Some(var_name) = &catch.var_name { + for replaced in set_scope_cell( + context, + scope, + var_name.clone(), + thrown, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(thrown)?; + } + execute_statements(&catch.body, context, scope, values) +} + +/// Returns true when any type in one catch clause accepts the thrown object. +pub(in crate::interpreter) fn catch_types_match_thrown( + thrown: RuntimeCellHandle, + class_names: &[String], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for class_name in class_names { + let class_name = class_name.trim_start_matches('\\'); + if class_name.eq_ignore_ascii_case("Throwable") { + return Ok(true); + } + if let Some(matched) = dynamic_object_is_a(thrown, class_name, false, context, values)? { + if matched { + return Ok(true); + } + continue; + } + if values.object_is_a(thrown, class_name, false)? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether one name is a PHP native enum interface. +pub(in crate::interpreter) fn eval_builtin_enum_interface_name(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is PHP's native backed-enum interface. +pub(super) fn eval_builtin_backed_enum_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is PHP's native Throwable interface. +pub(super) fn eval_builtin_throwable_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("Throwable") +} + +/// Returns whether one name is visible as a native/runtime interface to eval. +pub(in crate::interpreter) fn eval_runtime_interface_exists( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_builtin_enum_interface_name(name) || values.interface_exists(name)?) +} diff --git a/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs b/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs new file mode 100644 index 0000000000..d8c375f045 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs @@ -0,0 +1,809 @@ +//! Purpose: +//! Reads, writes, binds, tests, and unsets eval instance properties. +//! +//! Called from: +//! - Expression and statement dispatch for object property operations. +//! +//! Key details: +//! - Declared, dynamic, magic, hooked, and reference-backed properties share visibility checks. + +use super::*; + +/// Reads one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_get_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return values.property_get(object, property_name); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + } + return values.property_get(object, property_name); + }; + let object_class_name = class.name().to_string(); + let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + if property.has_get_hook() + && !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return eval_throw_uninitialized_property_error( + &declaring_class, + property.name(), + context, + values, + ); + } + } + if !declared_property_found + && eval_object_public_property_exists(object, property_name, values)? + { + return values.property_get(object, property_name); + } + if !declared_property_found { + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if let Some(result) = eval_magic_property_get( + object, + &object_class_name, + property_name, + context, + values, + )? { + return Ok(result); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + }); + } + } + } + if !declared_property_found { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + values.property_get(object, &storage_property_name) +} + +/// Writes one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_set_result( + object: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return values.property_set(object, property_name, value); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + } + return values.property_set(object, property_name, value); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + let class_is_readonly = class.is_readonly_class(); + let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + if property.has_set_hook() { + if !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return eval_throw_property_hook_readonly_error( + &declaring_class, + property.name(), + context, + values, + ); + } + } + if !declared_property_found { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_set(object, property_name, value) + }); + } + } + } + if !declared_property_found + && eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? + { + return Ok(()); + } + if !declared_property_found && class_is_readonly { + return eval_throw_dynamic_property_creation_error( + &object_class_name, + property_name, + context, + values, + ); + } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + eval_reference_target_write( + identity, + &storage_property_name, + target, + value, + context, + values, + )?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + return values.property_set(object, &storage_property_name, value); + } + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Binds one eval object property to a by-reference source parameter. +pub(super) fn eval_property_reference_bind_result( + object: RuntimeCellHandle, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let class = context + .dynamic_object_class(identity) + .ok_or(EvalStatus::RuntimeFatal)?; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + let (declaring_class, property) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + validate_eval_property_write_access(&declaring_class, &property, context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + let target = eval_property_reference_target( + identity, + &storage_property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_dynamic_property_alias(identity, &storage_property_name, target); + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Resolves a local by-reference source into a persistent property alias target. +pub(super) fn eval_property_reference_target( + object_identity: u64, + storage_property_name: &str, + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_property_reference_alias_name(object_identity, storage_property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name that backs one property reference alias. +pub(super) fn eval_property_reference_alias_name(object_identity: u64, storage_property_name: &str) -> String { + format!("\0elephc_property_ref:{object_identity}:{storage_property_name}") +} + +/// Reads the current value from a persistent reference target. +pub(in crate::interpreter) fn eval_reference_target_value( + target: &EvalReferenceTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } + EvalReferenceTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = + visible_scope_cell(context, scope, array_name).map_or_else(|| values.null(), Ok)?; + values.array_get(array, *index) + } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => { + let array = eval_reference_target_value(array_target, context, values)?; + values.array_get(array, *index) + } + EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_property_get_result(*object, property, context, values); + context.replace_execution_scope(previous_scope); + result + } + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_static_property_get_result(class_name, property, context, values); + context.replace_execution_scope(previous_scope); + result + } + EvalReferenceTarget::Cell { cell } => Ok(*cell), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + eval_invoker_slot_ref_target_value(*slot, *source_tag, values) + } + } +} + +/// Writes a new value to a persistent reference target. +pub(super) fn eval_reference_target_write( + object_identity: u64, + storage_property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_dynamic_property_alias( + object_identity, + storage_property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + +/// Evaluates PHP `isset($object->property)` without forcing `__get()` first. +pub(in crate::interpreter) fn eval_property_isset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let object_class_name = class.name().to_string(); + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return Ok(false); + } + let value = eval_property_get_result(object, property_name, context, values)?; + return Ok(!values.is_null(value)?); + } + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if eval_object_public_property_exists(object, property_name, values)? { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_is_initialized(object, property_name) + })? { + return Ok(false); + } + let value = eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + })?; + return Ok(!values.is_null(value)?); + } + } + eval_magic_property_isset(object, &object_class_name, property_name, context, values) + .map(|result| result.unwrap_or(false)) +} + +/// Evaluates PHP `unset($object->property)` for eval-declared object receivers. +pub(in crate::interpreter) fn eval_property_unset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_unset_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_unset_error( + &declaring_class, + property.name(), + context, + values, + ); + } + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + context.remove_dynamic_property_alias(identity, &storage_property_name); + context.mark_dynamic_property_uninitialized(identity, &storage_property_name); + let null = values.null()?; + return values.property_set(object, &storage_property_name, null); + } + if eval_magic_property_unset(object, &object_class_name, property_name, context, values)? { + return Ok(()); + } + return Ok(()); + } + if eval_object_public_property_exists(object, property_name, values)? { + let null = values.null()?; + return values.property_set(object, property_name, null); + } + let _ = eval_magic_property_unset(object, &object_class_name, property_name, context, values)?; + Ok(()) +} + +/// Dispatches an undefined or inaccessible eval property read through `__get()`. +pub(super) fn eval_magic_property_get( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__get") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + ) + .map(Some) +} + +/// Dispatches an undefined or inaccessible eval property write through `__set()`. +pub(super) fn eval_magic_property_set( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__set") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property, value]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + +/// Dispatches an undefined or inaccessible eval property probe through `__isset()`. +pub(super) fn eval_magic_property_isset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__isset") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + let truthy = values.truthy(result)?; + values.release(result)?; + Ok(Some(truthy)) +} + +/// Dispatches an undefined or inaccessible eval property unset through `__unset()`. +pub(super) fn eval_magic_property_unset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__unset") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + +/// Returns whether the object already has a public dynamic property with this exact name. +pub(super) fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Validates that an object property may be used as a by-reference method argument. +pub(in crate::interpreter) fn validate_property_ref_target( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns true while executing the named hook accessor for one property. +pub(in crate::interpreter) fn current_eval_property_hook_is( + declaring_class: &str, + property_name: &str, + hook_method: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + let Some((_, method)) = context + .current_function() + .and_then(|function| function.rsplit_once("::")) + else { + return false; + }; + method.eq_ignore_ascii_case(hook_method) + || method.eq_ignore_ascii_case(&property_hook_get_method(property_name)) + || method.eq_ignore_ascii_case(&property_hook_set_method(property_name)) +} diff --git a/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs b/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs new file mode 100644 index 0000000000..3d75073d1e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs @@ -0,0 +1,736 @@ +//! Purpose: +//! Resolves interface hierarchies and checks method/property signature compatibility. +//! +//! Called from: +//! - Class declaration and abstract-requirement validation. +//! +//! Key details: +//! - Arity, by-reference flags, variadics, return types, and hook capabilities remain PHP-compatible. + +use super::*; + +/// Returns interface names inherited or directly declared by a pending eval class. +pub(super) fn pending_class_interface_names(class: &EvalClass, context: &ElephcEvalContext) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = class.parent() { + for interface in context.class_interface_names(parent) { + push_pending_class_interface_name(&interface, &mut interfaces, &mut seen); + } + } + for interface in class.interfaces() { + push_pending_class_interface_tree(interface, context, &mut interfaces, &mut seen); + } + interfaces +} + +/// Adds one interface and its eval-declared parent interfaces to a pending class list. +pub(super) fn push_pending_class_interface_tree( + interface: &str, + context: &ElephcEvalContext, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + push_pending_class_interface_name(interface, interfaces, seen); + for parent in context.interface_parent_names(interface) { + push_pending_class_interface_name(&parent, interfaces, seen); + } +} + +/// Adds one interface name once using PHP class-name case-insensitive matching. +pub(super) fn push_pending_class_interface_name( + interface: &str, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let interface = interface.trim_start_matches('\\'); + if seen.insert(interface.to_ascii_lowercase()) { + interfaces.push(interface.to_string()); + } +} + +/// Returns PHP builtin interface method requirements inherited by a pending class. +pub(super) fn pending_class_builtin_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + requirements.extend(builtin_interface_method_requirements(&interface)); + } + requirements +} + +/// Returns generated/AOT interface method requirements inherited by a pending class. +pub(super) fn pending_class_aot_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(eval_aot_interface_method_requirements( + &interface, context, values, + )?); + } + Ok(requirements) +} + +/// Returns generated/AOT interface property requirements inherited by a pending class. +pub(super) fn pending_class_aot_interface_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(context.native_interface_property_requirements(&interface)); + } + Ok(requirements) +} + +/// Returns generated/AOT method requirements for one runtime interface. +pub(super) fn eval_aot_interface_method_requirements( + interface: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let interface = interface.trim_start_matches('\\'); + let method_names = eval_aot_interface_method_names(interface, values)?; + let mut requirements = Vec::new(); + for method_name in method_names { + if let Some(requirement) = + eval_aot_interface_method_requirement(interface, &method_name, context, values)? + { + requirements.push(requirement); + } + } + Ok(requirements) +} + +/// Returns generated/AOT method names for one runtime interface. +pub(super) fn eval_aot_interface_method_names( + interface: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_aot_method_names(interface, values) +} + +/// Returns generated/AOT method names for one runtime class-like symbol. +pub(super) fn eval_aot_method_names( + class_like: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_names = values.reflection_method_names(class_like)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + Ok(names) +} + +/// Builds one generated/AOT abstract parent method requirement from metadata. +pub(super) fn eval_aot_abstract_method_requirement( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let owner = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = eval_aot_method_signature_requirement( + class_name, + method_name, + is_static, + context, + values, + )?; + Ok(EvalAotAbstractMethodRequirement { + owner, + is_static, + visibility: eval_aot_method_visibility(flags), + signature, + }) +} + +/// Builds one generated/AOT interface method requirement from reflection and signature metadata. +pub(super) fn eval_aot_interface_method_requirement( + interface: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(interface, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let owner = values + .reflection_method_declaring_class(interface, method_name)? + .unwrap_or_else(|| interface.to_string()); + let signature = if is_static { + context.native_static_method_signature(&owner, method_name) + } else { + context.native_method_signature(&owner, method_name) + }; + Ok(Some(EvalAotInterfaceMethodRequirement { + owner: owner.clone(), + name: method_name.to_string(), + is_static, + signature: signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + }), + })) +} + +/// Converts generated/AOT callable metadata into an eval interface method requirement. +pub(super) fn eval_native_signature_interface_method( + method_name: &str, + is_static: bool, + signature: &NativeCallableSignature, +) -> EvalInterfaceMethod { + let param_count = signature.param_count(); + EvalInterfaceMethod::new( + method_name, + (0..param_count) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{index}")) + }) + .collect(), + ) + .with_static(is_static) + .with_parameter_types( + (0..param_count) + .map(|index| signature.param_type(index).cloned()) + .collect(), + ) + .with_parameter_defaults( + (0..param_count) + .map(|index| { + signature + .param_default(index) + .map(|_| EvalExpr::Const(EvalConst::Null)) + }) + .collect(), + ) + .with_parameter_by_ref_flags( + (0..param_count) + .map(|index| signature.param_by_ref(index)) + .collect(), + ) + .with_parameter_variadic_flags( + (0..param_count) + .map(|index| signature.param_variadic(index)) + .collect(), + ) + .with_return_type(signature.return_type().cloned()) +} + +/// Copies a runtime string array into Rust-owned strings for declaration validation. +pub(super) fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_runtime_string_value(value, values)?); + } + Ok(result) +} + +/// Reads one runtime string cell as UTF-8 metadata. +pub(super) fn eval_runtime_string_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Validates that one eval class provides methods required by one eval interface. +pub(super) fn validate_class_implements_eval_interface( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + if !class_has_interface_method(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns whether a class or its eval parents satisfy one builtin interface method signature. +pub(super) fn class_has_builtin_interface_method( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + method, + class.name(), + requirement, + requirement_owner, + Some(class), + context, + ); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(declaring_class, method)| { + method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + &method, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }) +} + +/// Returns whether a class or its eval parents satisfy one generated/AOT interface method. +pub(super) fn class_has_aot_interface_method( + class: &EvalClass, + requirement: &EvalAotInterfaceMethodRequirement, + context: &ElephcEvalContext, +) -> bool { + if let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + { + return class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + requirement, + Some(class), + context, + true, + ); + } + false +} + +/// Returns whether a class or its eval parents satisfy one interface method signature. +pub(super) fn class_has_interface_method( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_interface_signature( + method, + class.name(), + requirement, + requirement_owner, + Some(class), + context, + ); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(declaring_class, method)| { + method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_interface_signature( + &method, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }) +} + +/// Returns whether one method satisfies a generated/AOT interface requirement. +pub(super) fn class_method_satisfies_aot_interface_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotInterfaceMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one method satisfies a generated/AOT abstract parent requirement. +pub(super) fn class_method_satisfies_aot_abstract_parent_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotAbstractMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.is_static() != requirement.is_static + || method_visibility_rank(method.visibility()) + < method_visibility_rank(requirement.visibility) + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one class method can accept every call required by an interface method. +pub(super) fn class_method_satisfies_interface_signature( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + false, + ) +} + +/// Returns whether one class method can satisfy a PHP builtin interface method contract. +pub(super) fn class_method_satisfies_builtin_interface_signature( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + true, + ) +} + +/// Returns whether one class method satisfies an interface signature with configurable return checks. +pub(super) fn class_method_satisfies_interface_signature_with_return_mode( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + allow_missing_return_type: bool, +) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + requirement.params().len(), + requirement.parameter_defaults(), + requirement.parameter_is_by_ref(), + requirement.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + requirement.parameter_types(), + requirement.parameter_is_variadic(), + requirement_owner, + requirement.params().len(), + pending_class, + context, + ) && ((allow_missing_return_type && method.return_type().is_none()) + || method_return_type_signature_accepts( + method.return_type(), + method_owner, + requirement.return_type(), + requirement_owner, + pending_class, + context, + )) +} + +/// Returns whether one class property is compatible with one interface property contract. +pub(super) fn class_property_can_cover_interface_contract( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property.visibility() != EvalVisibility::Public || property.is_static() { + return false; + } + if !class_property_type_satisfies_interface_contract( + property.property_type(), + property.settable_type(), + property_owner, + requirement, + requirement_owner, + pending_class, + context, + ) { + return false; + } + if requirement.requires_get() && !class_property_supports_interface_get(property) { + return false; + } + if requirement.requires_set() { + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_visibility_rank(property.write_visibility()) + >= property_visibility_rank(requirement.write_visibility()) + && class_property_supports_interface_set(property); + } + requirement.requires_get() +} + +/// Returns whether one property type is compatible with interface get/set hook signatures. +pub(super) fn class_property_type_satisfies_interface_contract( + property_type: Option<&EvalParameterType>, + settable_type: Option<&EvalParameterType>, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if requirement.requires_get() + && !method_return_type_signature_accepts( + property_type, + property_owner, + requirement.property_type(), + requirement_owner, + pending_class, + context, + ) + { + return false; + } + if requirement.requires_set() { + let property_types = vec![settable_type.cloned()]; + let requirement_types = vec![requirement.property_type().cloned()]; + return method_parameter_type_signature_accepts( + &property_types, + &[], + property_owner, + &requirement_types, + &[], + requirement_owner, + 1, + pending_class, + context, + ); + } + true +} + +/// Returns whether one property can satisfy an interface `get` hook. +pub(super) fn class_property_supports_interface_get(property: &EvalClassProperty) -> bool { + property.has_get_hook() || property.requires_get_hook() || !property.is_virtual() +} + +/// Returns whether one property can satisfy an interface `set` hook. +pub(super) fn class_property_supports_interface_set(property: &EvalClassProperty) -> bool { + property.has_set_hook() + || property.requires_set_hook() + || (!property.is_virtual() && !property.is_readonly()) +} + +/// Returns whether an implementing method accepts the full required arity range. +pub(super) fn method_signature_accepts( + implementation_param_count: usize, + implementation_defaults: &[Option], + implementation_by_refs: &[bool], + implementation_variadics: &[bool], + required_param_count: usize, + required_defaults: &[Option], + required_by_refs: &[bool], + required_variadics: &[bool], +) -> bool { + let implementation_min = method_signature_min_arity( + implementation_param_count, + implementation_defaults, + implementation_variadics, + ); + let required_min = + method_signature_min_arity(required_param_count, required_defaults, required_variadics); + if implementation_min > required_min { + return false; + } + + let implementation_max = + method_signature_max_arity(implementation_param_count, implementation_variadics); + let required_max = method_signature_max_arity(required_param_count, required_variadics); + let arity_accepted = match (implementation_max, required_max) { + (None, _) => true, + (Some(_), None) => false, + (Some(implementation_max), Some(required_max)) => implementation_max >= required_max, + }; + arity_accepted + && method_signature_by_refs_accept( + implementation_by_refs, + implementation_variadics, + required_param_count, + required_by_refs, + required_variadics, + ) +} + +/// Returns whether pass-by-reference requirements are compatible across accepted args. +pub(super) fn method_signature_by_refs_accept( + implementation_by_refs: &[bool], + implementation_variadics: &[bool], + required_param_count: usize, + required_by_refs: &[bool], + required_variadics: &[bool], +) -> bool { + (0..required_param_count).all(|position| { + method_signature_effective_by_ref( + implementation_by_refs, + implementation_variadics, + position, + ) == method_signature_effective_by_ref(required_by_refs, required_variadics, position) + }) +} + +/// Returns the by-reference mode that one signature applies at an argument position. +pub(super) fn method_signature_effective_by_ref( + by_refs: &[bool], + variadics: &[bool], + position: usize, +) -> bool { + if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { + if position >= variadic_index { + return by_refs.get(variadic_index).copied().unwrap_or(false); + } + } + by_refs.get(position).copied().unwrap_or(false) +} + +/// Returns the minimum argument count accepted by one eval method signature. +pub(super) fn method_signature_min_arity( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns the maximum argument count accepted by one eval method signature. +pub(super) fn method_signature_max_arity(param_count: usize, variadics: &[bool]) -> Option { + if variadics.iter().any(|is_variadic| *is_variadic) { + None + } else { + Some(param_count) + } +} + +/// Returns whether a class or its eval parents satisfy one interface property contract. +pub(super) fn class_has_interface_property( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceProperty, + context: &ElephcEvalContext, +) -> bool { + pending_class_property_with_owner(class, requirement.name(), context).is_some_and( + |(declaring_class, property)| { + !property.is_abstract() + && class_property_can_cover_interface_contract( + &property, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }, + ) +} + +/// Returns a property from a pending class or one of its already registered parents. +pub(super) fn pending_class_property_with_owner( + class: &EvalClass, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if let Some(property) = class + .properties() + .iter() + .find(|property| property.name() == property_name) + { + return Some((class.name().to_string(), property.clone())); + } + class + .parent() + .and_then(|parent| context.class_property(parent, property_name)) +} diff --git a/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs b/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs new file mode 100644 index 0000000000..4bc7f48c7f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs @@ -0,0 +1,360 @@ +//! Purpose: +//! Validates declared methods and properties against interface member contracts. +//! +//! Called from: +//! - Class declaration validation before abstract-requirement collection. +//! +//! Key details: +//! - Eval, builtin, and AOT interface signatures use the same compatibility checks. + +use super::*; + +/// Validates declared or inherited class members that already cover eval interface contracts. +pub(super) fn validate_declared_class_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for interface in pending_class_interface_names(class, context) { + if !context.has_interface(&interface) { + continue; + } + validate_declared_class_interface_methods(class, &interface, context)?; + validate_declared_class_interface_properties(class, &interface, context)?; + } + Ok(()) +} + +/// Validates declared class methods against PHP builtin runtime interface contracts. +pub(super) fn validate_declared_class_builtin_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_builtin_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates declared class methods against generated/AOT interface contracts. +pub(super) fn validate_declared_class_aot_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + else { + continue; + }; + if !class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + &requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates class methods present for an eval interface, even on abstract classes. +pub(super) fn validate_declared_class_interface_methods( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates class properties present for an eval interface, even on abstract classes. +pub(super) fn validate_declared_class_interface_properties( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns a method from a pending class or one of its already registered parents. +pub(super) fn pending_class_method( + class: &EvalClass, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(method) = class.method(method_name) { + return Some((class.name().to_string(), method.clone())); + } + class + .parent() + .and_then(|parent| context.class_method(parent, method_name)) +} + +/// Validates one method declaration against inherited eval method metadata. +pub(super) fn validate_method_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + let Some((parent_declaring_class, parent_method)) = context.class_method(parent, method.name()) + else { + return Ok(()); + }; + if parent_method.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_method.is_static() != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + if method_visibility_rank(method.visibility()) + < method_visibility_rank(parent_method.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + if parent_method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !parent_method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if !class_method_signature_accepts( + method, + class.name(), + &parent_method, + &parent_declaring_class, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates one method declaration against inherited generated/AOT method metadata. +pub(super) fn validate_method_aot_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = pending_class_native_parent_name(class, context) else { + return Ok(()); + }; + if !values.class_exists(&parent)? { + return Ok(()); + } + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { + return Ok(()); + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + return Ok(()); + } + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + if parent_is_static != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + let parent_visibility = eval_aot_method_visibility(flags); + if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_visibility) { + return Err(EvalStatus::RuntimeFatal); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let Some(required) = eval_aot_method_signature_requirement( + &parent, + method.name(), + parent_is_static, + context, + values, + )? else { + return Ok(()); + }; + if !class_method_satisfies_interface_signature( + method, + class.name(), + &required, + &eval_aot_method_declaring_class(&parent, method.name(), values)?, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT reflection flags. +pub(super) fn eval_aot_method_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 { + EvalVisibility::Public + } else { + EvalVisibility::Public + } +} + +/// Returns the generated/AOT declaring class for one reflected method. +pub(super) fn eval_aot_method_declaring_class( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_method_declaring_class(class_name, method_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + +/// Returns a generated/AOT parent method signature as an eval method requirement. +pub(super) fn eval_aot_method_signature_requirement( + class_name: &str, + method_name: &str, + is_static: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let declaring_class = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = if is_static { + context.native_static_method_signature(&declaring_class, method_name) + } else { + context.native_method_signature(&declaring_class, method_name) + }; + Ok(signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + })) +} + +/// Returns whether one eval class method can accept every call accepted by its parent method. +pub(super) fn class_method_signature_accepts( + method: &EvalClassMethod, + method_owner: &str, + required: &EvalClassMethod, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + required.params().len(), + required.parameter_defaults(), + required.parameter_is_by_ref(), + required.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + required.parameter_types(), + required.parameter_is_variadic(), + required_owner, + required.params().len(), + pending_class, + context, + ) && method_return_type_signature_accepts( + method.return_type(), + method_owner, + required.return_type(), + required_owner, + pending_class, + context, + ) +} + +/// Returns a comparable rank where larger means less restrictive visibility. +pub(super) fn method_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/loop_statements.rs b/crates/elephc-magician/src/interpreter/statements/loop_statements.rs new file mode 100644 index 0000000000..5b6030fd5d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/loop_statements.rs @@ -0,0 +1,353 @@ +//! Purpose: +//! Executes static-local declarations, switch, and loop statement families. +//! +//! Called from: +//! - `crate::interpreter::statements::execute_stmt()`. +//! +//! Key details: +//! - Break/continue control, foreach array/object/iterator traversal, and key materialization are preserved. + +use super::*; + +/// Executes a PHP `static $name = expr;` declaration in the current eval scope. +pub(in crate::interpreter) fn execute_static_var_stmt( + name: &str, + init: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(function_name) = context.current_function().map(str::to_string) else { + let value = eval_expr(init, context, scope, values)?; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { + values.release(replaced)?; + } + return Ok(()); + }; + if scope.contains_visible(name) { + return Ok(()); + } + let value = if let Some(value) = context.static_local(&function_name, name) { + value + } else { + let value = eval_expr(init, context, scope, values)?; + let _ = context.set_static_local(function_name.clone(), name.to_string(), value); + value + }; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. +pub(in crate::interpreter) fn execute_switch_stmt( + expr: &EvalExpr, + cases: &[EvalSwitchCase], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(expr, context, scope, values)?; + let mut default_index = None; + let mut matched_index = None; + for (index, case) in cases.iter().enumerate() { + let Some(condition) = &case.condition else { + if default_index.is_none() { + default_index = Some(index); + } + continue; + }; + let condition = eval_expr(condition, context, scope, values)?; + let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; + if values.truthy(matches)? { + matched_index = Some(index); + break; + } + } + let Some(start_index) = matched_index.or(default_index) else { + return Ok(EvalControl::None); + }; + for case in &cases[start_index..] { + match execute_statements(&case.body, context, scope, values)? { + EvalControl::None => {} + EvalControl::Break | EvalControl::Continue => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `do/while` loop, evaluating the condition after every body run. +pub(in crate::interpreter) fn execute_do_while_stmt( + body: &[EvalStmt], + condition: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + loop { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `for` loop while preserving update-on-continue semantics. +pub(in crate::interpreter) fn execute_for_stmt( + init: &[EvalStmt], + condition: Option<&EvalExpr>, + update: &[EvalStmt], + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(init, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + loop { + if let Some(condition) = condition { + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + match execute_statements(update, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `foreach` loop over eval array and Traversable object values. +pub(in crate::interpreter) fn execute_foreach_stmt( + array: &EvalExpr, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = eval_expr(array, context, scope, values)?; + match values.type_tag(array)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + execute_foreach_array_stmt(array, key_name, value_name, body, context, scope, values) + } + EVAL_TAG_OBJECT => { + execute_foreach_object_stmt(array, key_name, value_name, body, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Executes `foreach` over a PHP array value using insertion-order runtime hooks. +pub(super) fn execute_foreach_array_stmt( + array: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for index in 0..len { + let key = values.array_iter_key(array, index)?; + let value = values.array_get(array, key)?; + if let Some(key_name) = key_name { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(key)?; + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes `foreach` over an Iterator or IteratorAggregate object. +pub(super) fn execute_foreach_object_stmt( + object: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_foreach_object_is_a(object, "Iterator", context, values)? { + return execute_foreach_iterator_stmt( + object, key_name, value_name, body, context, scope, values, + ); + } + if eval_foreach_object_is_a(object, "IteratorAggregate", context, values)? { + let iterator = eval_method_call_result(object, "getIterator", Vec::new(), context, values)?; + return match values.type_tag(iterator)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => execute_foreach_array_stmt( + iterator, key_name, value_name, body, context, scope, values, + ), + EVAL_TAG_OBJECT if eval_foreach_object_is_a(iterator, "Iterator", context, values)? => { + execute_foreach_iterator_stmt( + iterator, key_name, value_name, body, context, scope, values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + }; + } + Err(EvalStatus::RuntimeFatal) +} + +/// Drives one Iterator object through PHP's `foreach` method-call sequence. +pub(super) fn execute_foreach_iterator_stmt( + iterator: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_method_call_result(iterator, "rewind", Vec::new(), context, values)?; + values.release(result)?; + loop { + let valid = eval_method_call_result(iterator, "valid", Vec::new(), context, values)?; + let is_valid = values.truthy(valid)?; + values.release(valid)?; + if !is_valid { + return Ok(EvalControl::None); + } + + let value = eval_method_call_result(iterator, "current", Vec::new(), context, values)?; + let key = if key_name.is_some() { + Some(eval_method_call_result( + iterator, + "key", + Vec::new(), + context, + values, + )?) + } else { + None + }; + if let Some((key_name, key)) = key_name.zip(key) { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => { + let result = + eval_method_call_result(iterator, "next", Vec::new(), context, values)?; + values.release(result)?; + } + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } +} + +/// Returns whether a foreach object satisfies one iterator interface. +pub(super) fn eval_foreach_object_is_a( + object: RuntimeCellHandle, + target: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(object, target, false, context, values)? + .map_or_else(|| values.object_is_a(object, target, false), Ok) +} + +/// Returns PHP's next automatic integer key for `$array[]` append writes. +pub(in crate::interpreter) fn eval_array_append_key( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut next_key = None; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + continue; + } + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current) = next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; + values.truthy(is_greater)? + } else { + true + }; + if replace { + next_key = Some(candidate); + } + } + next_key.map_or_else(|| values.int(0), Ok) +} diff --git a/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs new file mode 100644 index 0000000000..25421c06ce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs @@ -0,0 +1,863 @@ +//! Purpose: +//! Dispatches eval and native instance methods with dynamic visibility and scope. +//! +//! Called from: +//! - Method-call expression evaluation and reflected invocation. +//! +//! Key details: +//! - Private shadows, native bridges, closures, magic methods, and reference args remain ordered. + +use super::*; + +/// Dispatches a method call to an eval-declared class method or to the runtime hook. +pub(in crate::interpreter) fn eval_method_call_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_method_call_result_with_evaluated_args( + object, + method_name, + positional_args(evaluated_args), + context, + values, + ) +} + +/// Dispatches an object method call while preserving named-argument metadata for eval methods. +pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, method_name, evaluated_args); + }; + if let Some(target) = context.closure_object_target(identity).cloned() { + if let Some(result) = + eval_closure_object_method_result(target, method_name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } + } + if let Some(attribute_metadata) = context.eval_reflection_attribute(identity).cloned() { + if method_name.eq_ignore_ascii_case("newInstance") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return eval_reflection_attribute_new_instance_result( + attribute_metadata.attribute(), + context, + values, + ); + } + if method_name.eq_ignore_ascii_case("getArguments") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(args) = attribute_metadata.attribute().args() else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_class_attribute_args_result(args, values); + } + if method_name.eq_ignore_ascii_case("getTarget") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.int(attribute_metadata.target() as i64); + } + if method_name.eq_ignore_ascii_case("isRepeated") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.bool_value(attribute_metadata.is_repeated()); + } + } + if let Some(result) = eval_reflection_parameter_legacy_type_predicate_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_parameter_to_string_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } + if let Some(result) = + eval_reflection_type_to_string_result(object, method_name, evaluated_args.clone(), values)? + { + return Ok(result); + } + if let Some(result) = eval_reflection_class_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_implements_interface_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_is_subclass_of_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_is_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_source_location_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_basic_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_method_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_property_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_enum_methods_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_relation_objects_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_trait_aliases_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_default_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_static_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_set_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_method_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_method_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_prototype_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_set_accessible_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_hooks_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_is_initialized_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_lazy_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_constant_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_enum_case_get_enum_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_get_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_raw_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_set_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_reflection_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_reflection_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_members_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_member_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(instance) = eval_reflection_class_new_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } + if let Some(instance) = eval_reflection_class_new_instance_without_constructor_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + if method_name.eq_ignore_ascii_case("__clone") { + if let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, method_name, values)? + { + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } + } + return eval_native_method_with_evaluated_args( + object, + &class_name, + method_name, + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if eval_enum_static_builtin_applies(&called_class_name, method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } + let mut inaccessible_method = None; + if let Some((class_name, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&class_name, method.visibility(), context).is_ok() { + if method.is_static() { + return eval_dynamic_static_method_with_values( + &class_name, + &called_class_name, + &method, + evaluated_args, + context, + values, + ); + } + return eval_dynamic_method_with_values( + &class_name, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + inaccessible_method = Some((class_name, method)); + } + if inaccessible_method.is_none() { + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + if let Some((declaring_class, _, _, _)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + method_name, + context, + values, + )? + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + &parent, + method_name, + evaluated_args, + Some(&declaring_class), + Some(&called_class_name), + context, + values, + ); + } + if eval_native_instance_magic_method_available(&parent, context, values)? { + return eval_native_method_with_evaluated_args( + object, + &parent, + method_name, + evaluated_args, + context, + values, + ); + } + } + } + if let Some(result) = eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + if let Some((declaring_class, method)) = inaccessible_method { + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + eval_throw_undefined_method_call_error(&called_class_name, method_name, context, values) +} + +/// Dispatches PHP-visible methods on eval-backed `Closure` objects. +pub(super) fn eval_closure_object_method_result( + target: EvalClosureObjectTarget, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if method_name.eq_ignore_ascii_case("__invoke") { + return eval_closure_object_invoke_result(target, evaluated_args, context, values) + .map(Some); + } + if method_name.eq_ignore_ascii_case("bindTo") { + let (bound_this, bound_scope, rebinds_function_scope) = + eval_closure_bind_to_args(evaluated_args, context, values)?; + return eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) + .map(Some); + } + if !method_name.eq_ignore_ascii_case("call") { + return Ok(None); + } + let (bound_this, call_args) = eval_closure_call_split_args(evaluated_args)?; + match target { + EvalClosureObjectTarget::Named(name) => { + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), + bound_scope: None, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } + EvalClosureObjectTarget::BoundNamed { + name, bound_scope, .. + } => { + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), + bound_scope, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + let callable = EvaluatedCallable::InvokableObject { object: bound_this }; + eval_evaluated_callable_with_by_value_call_args(&callable, call_args, context, values) + .map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class: _, + native_class, + bridge_scope, + } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + let callable = EvaluatedCallable::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }; + eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ) + .map(Some), + } +} + +/// Invokes the callable target retained behind a PHP-visible eval `Closure` object. +pub(super) fn eval_closure_object_invoke_result( + target: EvalClosureObjectTarget, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable = match target { + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { + display_name: name.clone(), + name, + }, + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + }, + EvalClosureObjectTarget::InvokableObject { object } => { + EvaluatedCallable::InvokableObject { object } + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + }; + eval_evaluated_callable_with_call_array_args(&callable, evaluated_args, context, values) +} + +/// Splits `Closure::call()` arguments into the bound object and forwarded closure args. +pub(super) fn eval_closure_call_split_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut bound_this = None; + let mut consumed_positional_receiver = false; + let mut call_args = Vec::with_capacity(evaluated_args.len().saturating_sub(1)); + + for arg in evaluated_args { + if arg + .name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("newThis")) + { + if bound_this.replace(arg.value).is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + } + if arg.name.is_none() && !consumed_positional_receiver && bound_this.is_none() { + consumed_positional_receiver = true; + bound_this = Some(arg.value); + continue; + } + call_args.push(arg); + } + + bound_this + .map(|receiver| (receiver, call_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns whether `Closure::call()` may bind a method closure to the new object. +pub(super) fn eval_closure_call_bound_class_matches( + original_object: RuntimeCellHandle, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + let bound_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + Ok(original_class.eq_ignore_ascii_case(&bound_class)) +} + +/// Returns whether `Closure::bind()` may bind a method closure to the new object. +pub(super) fn eval_closure_bind_bound_class_matches_method( + original_object: RuntimeCellHandle, + method_name: &str, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = + eval_closure_bind_method_declaring_class(original_object, method_name, context, values)? + else { + return Ok(false); + }; + eval_closure_object_is_instance_of(bound_this, &declaring_class, context, values) +} + +/// Resolves the class that declares the method captured by a method Closure target. +pub(super) fn eval_closure_bind_method_declaring_class( + original_object: RuntimeCellHandle, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + if let Some((declaring_class, method)) = context.class_method(&original_class, method_name) { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + return Ok(Some(declaring_class)); + } + let native_class = context + .class_native_parent_name(&original_class) + .unwrap_or_else(|| original_class.clone()); + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + let declaring_class = eval_aot_method_declaring_class(&native_class, method_name, values)?; + Ok(Some(declaring_class)) +} + +/// Returns whether an object is an instance of the requested eval or generated class name. +pub(super) fn eval_closure_object_is_instance_of( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object_class = eval_closure_bound_object_class_name(object, context, values)?; + Ok(eval_static_syntax_object_matches_class( + &object_class, + class_name, + context, + )) +} + +/// Emits PHP's `Closure::call()` warning and returns `null`. +pub(super) fn eval_closure_call_warning_null( + message: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning(message)?; + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs b/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs new file mode 100644 index 0000000000..82d9eaecca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs @@ -0,0 +1,420 @@ +//! Purpose: +//! Binds native callable arguments and prepares by-reference writeback metadata. +//! +//! Called from: +//! - Native instance, static, constructor, and call_user_func dispatch. +//! +//! Key details: +//! - Named, variadic, typed, and degraded by-value reference modes share one binder. + +use super::*; + +/// Binds native AOT callable args using the selected by-reference degradation mode. +pub(super) fn bind_native_callable_bound_args_with_mode( + signature: Option, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(signature) = signature else { + return positional_evaluated_bound_args(None, args, by_ref_mode, context, values); + }; + if !signature.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } + if signature.param_names().len() == signature.param_count() { + bind_native_signature_args(&signature, args, by_ref_mode, context, values) + } else { + positional_evaluated_bound_args(Some(&signature), args, by_ref_mode, context, values) + } +} + +/// Binds positional-only native AOT args and validates registered by-reference slots. +pub(super) fn positional_evaluated_bound_args( + signature: Option<&NativeCallableSignature>, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut bound_args = args + .into_iter() + .enumerate() + .map(|(index, arg)| { + let ref_target = match signature { + Some(signature) => native_parameter_ref_target( + signature, + Some(index), + arg.ref_target, + by_ref_mode, + values, + )?, + None => None, + }; + Ok(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }) + }) + .collect::, _>>()?; + if let Some(signature) = signature { + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; + } + Ok(bound_args) +} + +/// Returns only runtime cell values from bound native AOT call arguments. +pub(in crate::interpreter) fn native_bound_arg_values( + args: &[BoundMethodArg], +) -> Vec { + args.iter().map(|arg| arg.value).collect() +} + +/// Writes native AOT by-reference argument cells back to their eval caller targets. +pub(in crate::interpreter) fn write_back_native_callable_ref_args( + bound_args: &[BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for bound_arg in bound_args { + if let Some(target) = bound_arg.ref_target.as_ref() { + write_back_method_ref_target(target, bound_arg.value, context, values)?; + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(bound_arg.value, *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + } + Ok(()) +} + +/// Binds native AOT callable args and fills omitted defaults from metadata. +pub(super) fn bind_native_signature_args( + signature: &NativeCallableSignature, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; signature.param_count()]; + let variadic_index = native_callable_variadic_index(signature); + let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + + if let Some(index) = variadic_index { + let array = values.array_new(args.len())?; + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + for arg in args { + if let Some(name) = arg.name { + bind_native_named_signature_arg( + signature, + variadic_index, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + bind_native_positional_signature_arg( + signature, + &mut bound_args, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } + if value.is_some() { + continue; + } + if position < signature.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = signature.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; + Ok(bound_args) +} + +/// Applies registered native AOT parameter types after argument binding and default filling. +pub(super) fn apply_native_callable_bound_arg_types( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let Some(param_type) = signature.param_type(position) else { + continue; + }; + if signature.param_variadic(position) { + apply_native_callable_variadic_arg_type(param_type, bound_arg, context, values)?; + } else { + bound_arg.value = + eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + } + Ok(()) +} + +/// Applies one registered native variadic parameter type to each collected argument. +pub(super) fn apply_native_callable_variadic_arg_type( + param_type: &EvalParameterType, + bound_arg: &mut BoundMethodArg, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(bound_arg.value)?; + for position in 0..len { + let key = values.array_iter_key(bound_arg.value, position)?; + let value = values.array_get(bound_arg.value, key)?; + let value = eval_method_parameter_value(param_type, value, context, values)?; + bound_arg.value = values.array_set(bound_arg.value, key, value)?; + } + Ok(()) +} + +/// Copies by-value degraded by-ref native method args before the generated bridge mutates them. +pub(super) fn copy_native_call_user_func_by_value_ref_args( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !matches!(by_ref_mode, EvalByRefBindingMode::WarnByValue { .. }) { + return Ok(()); + } + let variadic_index = native_callable_variadic_index(signature); + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !signature.param_by_ref(param_index) || bound_arg.ref_target.is_some() { + continue; + } + bound_arg.value = copy_native_call_user_func_by_value_ref_arg(bound_arg.value, values)?; + } + Ok(()) +} + +/// Allocates a temporary runtime cell for one by-value degraded by-ref native method arg. +pub(super) fn copy_native_call_user_func_by_value_ref_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + values.raw_word_value(tag, word) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + values.string_bytes_value(&bytes) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => values.array_clone_shallow(value), + EVAL_TAG_OBJECT => { + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + values.raw_heap_word_value(retained) + } + EVAL_TAG_NULL => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the native callable variadic slot, if metadata registered one. +pub(super) fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option { + (0..signature.param_count()).find(|index| signature.param_variadic(*index)) +} + +/// Binds one positional native AOT argument to a fixed slot or variadic array. +pub(super) fn bind_native_positional_signature_arg( + signature: &NativeCallableSignature, + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + let ref_target = + native_parameter_ref_target(signature, variadic_index, ref_target, by_ref_mode, values)?; + return bind_native_variadic_arg(bound_args, variadic_index, key, value, ref_target, values); + } + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_parameter_ref_target(signature, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Binds one named native AOT argument to a fixed non-variadic slot. +pub(super) fn bind_native_named_signature_arg( + signature: &NativeCallableSignature, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = native_parameter_ref_target( + signature, + Some(param_index), + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns the caller writeback target required by a native by-reference parameter. +pub(super) fn native_parameter_ref_target( + signature: &NativeCallableSignature, + param_index: Option, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !signature.param_by_ref(param_index) { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_callable_param_warning_name(signature, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } +} + +/// Returns the PHP parameter name used in native method by-reference warnings. +pub(super) fn native_callable_param_warning_name( + signature: &NativeCallableSignature, + param_index: usize, +) -> String { + signature + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) +} + +/// Returns the matching non-variadic native parameter index for one named arg. +pub(super) fn native_regular_param_index( + signature: &NativeCallableSignature, + variadic_index: Option, + name: &str, +) -> Option { + signature + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the native AOT variadic argument array. +pub(super) fn bind_native_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + let array = values.array_set(bound.value, key, value)?; + bound.value = array; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs b/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs new file mode 100644 index 0000000000..fb6a5153b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs @@ -0,0 +1,251 @@ +//! Purpose: +//! Resolves AOT method metadata, invokes native constructors, and materializes defaults. +//! +//! Called from: +//! - Native class construction and signature binding. +//! +//! Key details: +//! - Parent metadata lookup, constructor visibility, and compound default ownership stay centralized. + +use super::*; + +/// Finds generated/AOT method metadata on a class or its native parent chain. +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata_in_hierarchy( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = eval_aot_method_dispatch_metadata(¤t, method_name, values)? { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + +/// Runs one generated/AOT constructor after native signature binding. +pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name, + object, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Runs one generated/AOT constructor with caller-selected by-ref binding behavior. +pub(super) fn eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { + return eval_throw_error(&message, context, values); + } + let bridge_scope = + eval_native_constructor_bridge_scope(class_name, context, values)?; + let signature = context.native_constructor_signature(class_name); + let bound_args = bind_native_callable_bound_args_with_mode( + signature, + evaluated_args, + by_ref_mode, + context, + values, + )?; + let result = if let Some(scope) = bridge_scope.as_deref() { + eval_with_native_bridge_scope(scope, context, || { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }) + } else { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(()), Ok(())) => Ok(()), + } +} + +/// Returns the generated/AOT constructor scope that the runtime bridge can recognize. +pub(super) fn eval_native_constructor_bridge_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + +/// Returns PHP's constructor access error for generated/AOT constructors, if inaccessible. +pub(super) fn eval_native_constructor_access_error( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + return Ok(None); + } + Ok(Some(format!( + "Call to {} {}::__construct() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ))) +} + +/// Returns whether the current eval scope may call one generated/AOT constructor. +pub(super) fn eval_native_constructor_access_allowed( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> bool { + match visibility { + EvalVisibility::Public => true, + EvalVisibility::Private => context + .current_class_scope() + .is_some_and(|current| same_eval_class_name(current, declaring_class)), + EvalVisibility::Protected => context + .current_class_scope() + .is_some_and(|current| eval_classes_are_related(current, declaring_class, context)), + } +} + +/// Returns PHP's scope phrase for constructor access diagnostics. +pub(super) fn eval_native_constructor_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label. +pub(super) fn eval_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + +/// Allocates a fresh runtime cell for one invocation-safe native AOT default. +pub(in crate::interpreter) fn materialize_native_callable_default( + default: &NativeCallableDefault, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match default { + NativeCallableDefault::Null => values.null(), + NativeCallableDefault::Bool(value) => values.bool_value(*value), + NativeCallableDefault::Int(value) => values.int(*value), + NativeCallableDefault::Float(value) => values.float(*value), + NativeCallableDefault::String(value) => values.string(value), + NativeCallableDefault::EmptyArray => values.array_new(0), + NativeCallableDefault::Array(elements) => { + materialize_native_callable_array_default(elements, context, values) + } + NativeCallableDefault::Object { class_name, args } => { + materialize_native_callable_object_default(class_name, args, context, values) + } + } +} + +/// Allocates one array-valued native AOT parameter default with fresh element cells. +pub(super) fn materialize_native_callable_array_default( + elements: &[NativeCallableArrayDefaultElement], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let has_string_key = elements.iter().any(|element| { + matches!( + element.key, + Some(NativeCallableArrayDefaultKey::String(_)) + ) + }); + let mut array = if has_string_key { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + let mut next_auto_key = 0; + for element in elements { + let key = match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + if *value >= next_auto_key { + next_auto_key = value.saturating_add(1); + } + values.int(*value)? + } + Some(NativeCallableArrayDefaultKey::String(value)) => values.string(value)?, + None => { + let key = values.int(next_auto_key)?; + next_auto_key = next_auto_key.saturating_add(1); + key + } + }; + let value = materialize_native_callable_default(&element.value, context, values)?; + array = values.array_set(array, key, value)?; + } + Ok(array) +} + +/// Allocates and constructs one object-valued native AOT parameter default. +pub(super) fn materialize_native_callable_object_default( + class_name: &str, + args: &[NativeCallableObjectDefaultArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object(class_name)?; + let mut constructor_args = Vec::with_capacity(args.len()); + for arg in args { + constructor_args.push(EvaluatedCallArg { + name: arg.name.clone(), + value: materialize_native_callable_default(&arg.value, context, values)?, + ref_target: None, + }); + } + if let Err(err) = eval_native_constructor_with_evaluated_args( + class_name, + object, + constructor_args, + context, + values, + ) { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs b/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs new file mode 100644 index 0000000000..7cad8b8dfc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs @@ -0,0 +1,480 @@ +//! Purpose: +//! Executes generated/AOT instance and static methods through native bridge scopes. +//! +//! Called from: +//! - Method, static-method, Reflection, and call_user_func dispatch. +//! +//! Key details: +//! - Checked and unchecked entry points share binding, bridge scope, and reference-mode handling. + +use super::*; + +/// Calls one generated/AOT instance method after native signature binding. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method after validation with an optional bridge scope. +pub(super) fn eval_native_method_with_evaluated_args_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); + if resolved_bridge_scope.is_none() { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on + // the receiver's class; access is inherently allowed, so skip the + // hierarchy resolution (it would find the override instead). + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ); + } + } + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, _, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } + if !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + resolved_bridge_scope.as_deref(), + called_class_scope, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without enforcing member visibility. +pub(super) fn eval_native_method_with_evaluated_args_unchecked( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without visibility checks using an optional bridge scope. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT instance method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT instance method with a selected by-reference binding mode. +pub(super) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; + let result = if let Some(scope) = bridge_scope { + eval_native_method_call_with_scope( + scope, + called_class_scope, + object, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.method_call(object, method_name, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), + } +} + +/// Calls one generated/AOT static method after native signature binding. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method after validation with an optional bridge scope. +pub(super) fn eval_native_static_method_with_evaluated_args_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, is_static, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } + if is_static + && !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name, + method_name, + evaluated_args, + resolved_bridge_scope.as_deref(), + called_class_scope, + context, + values, + ) +} + +/// Calls one generated/AOT static method without enforcing member visibility. +pub(super) fn eval_native_static_method_with_evaluated_args_unchecked( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method without visibility checks using an optional bridge scope. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT static method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT static method with a selected by-reference binding mode. +pub(super) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_static_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; + let result = if let Some(scope) = bridge_scope { + eval_native_static_method_call_with_scope( + scope, + called_class_scope, + class_name, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), + } +} + +/// Returns whether a generated/AOT class has an instance `__call()` fallback. +pub(super) fn eval_native_instance_magic_method_available( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a static `__callStatic()` fallback. +pub(super) fn eval_native_static_magic_method_available( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Dispatches a missing or inaccessible generated/AOT instance method through `__call()`. +pub(in crate::interpreter) fn eval_native_magic_instance_method_call( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_method_with_evaluated_args_unchecked( + object, + class_name, + "__call", + magic_args, + context, + values, + ) +} + +/// Dispatches a missing or inaccessible generated/AOT static method through `__callStatic()`. +pub(in crate::interpreter) fn eval_native_magic_static_method_call( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_static_method_with_evaluated_args_unchecked( + class_name, + "__callStatic", + magic_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs new file mode 100644 index 0000000000..a0c8b2f455 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs @@ -0,0 +1,413 @@ +//! Purpose: +//! Dispatches AOT static syntax and builtin enum/property-hook static methods. +//! +//! Called from: +//! - Static method dispatch after eval-declared targets are exhausted. +//! +//! Key details: +//! - Native receiver metadata, enum backing values, and PHP-visible errors share this boundary. + +use super::*; + +/// Dispatches one generated/AOT method reached through PHP static-call syntax. +pub(super) fn eval_native_static_syntax_method_result( + class_name: &str, + called_class_scope: Option<&str>, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_static_method_with_evaluated_args( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return Ok(None); + }; + if is_abstract { + return eval_throw_abstract_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = + eval_static_syntax_instance_receiver(class_name, lexical_scope, context, values)? + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some) +} + +/// Returns `$this` when PHP permits static-call syntax to target an instance method. +pub(in crate::interpreter) fn eval_static_syntax_instance_receiver( + class_name: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope) = lexical_scope else { + return Ok(None); + }; + let Some(object) = visible_scope_cell(context, scope, "this") else { + return Ok(None); + }; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let object_class_name = eval_static_syntax_object_class_name(object, context, values)?; + if eval_static_syntax_object_matches_class(&object_class_name, class_name, context) { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Resolves the PHP-visible class name for the current static-syntax `$this` object. +pub(super) fn eval_static_syntax_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + +/// Returns whether `$this` is an instance of the class named by static-call syntax. +pub(super) fn eval_static_syntax_object_matches_class( + object_class_name: &str, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + same_eval_class_name(object_class_name, class_name) + || context.class_is_a(object_class_name, class_name, false) + || native_class_is_a(object_class_name, class_name, context) +} + +/// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. +pub(super) fn eval_builtin_property_hook_type_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + match eval_enum_static_builtin_name(method_name) { + Some("cases") => { + eval_builtin_property_hook_type_cases(evaluated_args, context, values).map(Some) + } + Some("from") => { + eval_builtin_property_hook_type_from(evaluated_args, false, context, values).map(Some) + } + Some("tryFrom") => { + eval_builtin_property_hook_type_from(evaluated_args, true, context, values).map(Some) + } + _ => Ok(None), + } +} + +/// Builds the indexed case array for eval's builtin `PropertyHookType` enum slice. +pub(super) fn eval_builtin_property_hook_type_cases( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let case_names = ["Get", "Set"]; + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = + eval_builtin_property_hook_type_case("PropertyHookType", case_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates builtin `PropertyHookType::from()` or `tryFrom()` inside eval. +pub(super) fn eval_builtin_property_hook_type_from( + evaluated_args: Vec, + nullable_miss: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let bytes = values.string_bytes(value)?; + let value_text = String::from_utf8_lossy(&bytes); + for constant_name in ["Get", "Set"] { + let Some((_, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + continue; + }; + if value_text == case_value { + return eval_builtin_property_hook_type_case( + "PropertyHookType", + constant_name, + context, + values, + )? + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + let message = eval_enum_invalid_backing_value_message( + "PropertyHookType", + EvalEnumBackingType::String, + value, + values, + )?; + eval_throw_value_error(&message, context, values) + } +} + +/// Returns a recognized enum-provided static method name. +pub(super) fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("cases") { + Some("cases") + } else if method_name.eq_ignore_ascii_case("from") { + Some("from") + } else if method_name.eq_ignore_ascii_case("tryFrom") { + Some("tryFrom") + } else { + None + } +} + +/// Returns a synthetic enum method only when that enum actually provides it. +pub(in crate::interpreter) fn eval_enum_static_builtin_applies( + enum_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<&'static str> { + let enum_decl = context.enum_decl(enum_name)?; + match eval_enum_static_builtin_name(method_name)? { + "cases" => Some("cases"), + "from" if enum_decl.backing_type().is_some() => Some("from"), + "tryFrom" if enum_decl.backing_type().is_some() => Some("tryFrom"), + _ => None, + } +} + +/// Dispatches enum-provided static methods for eval-declared enums. +pub(in crate::interpreter) fn eval_enum_builtin_static_method_result( + enum_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_enum_static_builtin_name(method_name).ok_or(EvalStatus::RuntimeFatal)? { + "cases" => eval_enum_cases_result(enum_name, evaluated_args, context, values), + "from" => eval_enum_from_result(enum_name, evaluated_args, false, context, values), + "tryFrom" => eval_enum_from_result(enum_name, evaluated_args, true, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the indexed array returned by `EnumName::cases()`. +pub(super) fn eval_enum_cases_result( + enum_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = context + .enum_case(enum_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates `EnumName::from()` or `EnumName::tryFrom()` for eval-backed enums. +pub(super) fn eval_enum_from_result( + enum_name: &str, + evaluated_args: Vec, + nullable_miss: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let backing_type = enum_decl.backing_type().ok_or(EvalStatus::RuntimeFatal)?; + let enum_display_name = enum_decl.name().trim_start_matches('\\').to_string(); + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + for case_name in case_names { + let case_value = context + .enum_case_value(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let equal = values.compare(EvalBinOp::StrictEq, value, case_value)?; + if values.truthy(equal)? { + return context + .enum_case(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + let message = eval_enum_invalid_backing_value_message( + &enum_display_name, + backing_type, + value, + values, + )?; + eval_throw_value_error(&message, context, values) + } +} + +/// Builds PHP's backed-enum `ValueError` message for an unmatched enum value. +pub(super) fn eval_enum_invalid_backing_value_message( + enum_name: &str, + backing_type: EvalEnumBackingType, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let value = String::from_utf8_lossy(&bytes); + let value = match backing_type { + EvalEnumBackingType::Int => value.into_owned(), + EvalEnumBackingType::String => format!("\"{}\"", value), + }; + Ok(format!( + "{} is not a valid backing value for enum {}", + value, enum_name + )) +} + +/// Creates and schedules a `ValueError` through eval's normal Throwable channel. +pub(super) fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates and schedules a `ReflectionException` through eval's normal Throwable channel. +pub(super) fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Schedules the Throwable category required by one ReflectionClass instantiation error. +pub(super) fn eval_throw_reflection_instantiation_error( + error: EvalReflectionInstantiationError, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match error { + EvalReflectionInstantiationError::ThrowableError(message) => { + eval_throw_error(&message, context, values) + } + EvalReflectionInstantiationError::ReflectionException(message) => { + eval_throw_reflection_exception(&message, context, values) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs b/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs new file mode 100644 index 0000000000..6fc296e6f4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs @@ -0,0 +1,371 @@ +//! Purpose: +//! Validates property and class-constant declarations against inherited contracts. +//! +//! Called from: +//! - Eval class and interface declaration validation. +//! +//! Key details: +//! - Readonly, asymmetric visibility, hook parameter types, and AOT redeclarations are checked here. + +use super::*; + +/// Validates property declarations that can be checked before class registration. +pub(super) fn validate_eval_declared_properties( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for property in class.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + if !names.insert(property.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_abstract() + && (!class.is_abstract() + || property.is_static() + || property.is_final() + || property.is_readonly() + || property.default().is_some() + || (!property.requires_get_hook() && !property.requires_set_hook())) + { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_static() && property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(set_visibility) = property.set_visibility() { + if property.is_static() || property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if property_visibility_rank(set_visibility) + > property_visibility_rank(property.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + } + if property.is_final() && property.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_readonly() && property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_readonly() && property.default().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + if (property.has_get_hook() || property.has_set_hook()) + && (property.is_static() || property.is_readonly() || property.default().is_some()) + { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_property_set_hook_parameter_type(class, property, context)?; + } + Ok(()) +} + +/// Validates that an explicit set-hook parameter type can accept every property value. +pub(super) fn validate_eval_property_set_hook_parameter_type( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(set_hook_type) = property.set_hook_type() else { + return Ok(()); + }; + let Some(property_type) = property.property_type() else { + return Err(EvalStatus::RuntimeFatal); + }; + let set_hook_types = vec![Some(set_hook_type.clone())]; + let property_types = vec![Some(property_type.clone())]; + method_parameter_type_signature_accepts( + &set_hook_types, + &[], + class.name(), + &property_types, + &[], + class.name(), + 1, + Some(class), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Validates one property declaration against inherited eval property metadata. +pub(super) fn validate_property_parent_redeclaration( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + if let Some((parent_declaring_class, parent_property)) = + context.class_property(parent, property.name()) + { + if parent_property.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_property.is_final() + || parent_property.set_visibility() == Some(EvalVisibility::Private) + { + return Err(EvalStatus::RuntimeFatal); + } + if parent_property.is_static() != property.is_static() + || (parent_property.is_readonly() && !property.is_readonly()) + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_property.visibility()) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_property.write_visibility()) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property.property_type(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(()); + } + validate_property_aot_parent_redeclaration(parent, class, property, context, values) +} + +/// Validates one property declaration against inherited generated/AOT property metadata. +pub(super) fn validate_property_aot_parent_redeclaration( + parent: &str, + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if context.has_class(parent) || !values.class_exists(parent)? { + return Ok(()); + } + let parent = parent.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(parent, property.name())? else { + return Ok(()); + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + return Ok(()); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 + { + return Err(EvalStatus::RuntimeFatal); + } + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_visibility = eval_aot_property_visibility(flags); + let parent_write_visibility = eval_aot_property_write_visibility(flags, parent_visibility); + let parent_is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let parent_declaring_class = + eval_aot_property_declaring_class(parent, property.name(), values)?; + let parent_property_type = context + .native_property_type(&parent_declaring_class, property.name()) + .or_else(|| context.native_property_type(parent, property.name())); + if parent_is_static != property.is_static() + || (parent_is_readonly && !property.is_readonly()) + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_visibility) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_write_visibility) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property_type.as_ref(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT property reflection flags. +pub(super) fn eval_aot_property_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Returns the eval write visibility represented by generated/AOT property flags. +pub(super) fn eval_aot_property_write_visibility( + flags: u64, + read_visibility: EvalVisibility, +) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + read_visibility + } +} + +/// Returns the generated/AOT declaring class for one reflected property. +pub(super) fn eval_aot_property_declaring_class( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_property_declaring_class(class_name, property_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + +/// Validates constant declarations that can be checked before registration. +pub(super) fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for constant in constants { + validate_eval_non_method_attribute_targets(constant.attributes())?; + if !names.insert(constant.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if constant.is_final() && constant.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates declarations that are specific to PHP interface constants. +pub(super) fn validate_eval_interface_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { + for constant in constants { + if constant.visibility() != EvalVisibility::Public { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates interface constants against inherited parent-interface constants. +pub(super) fn validate_interface_constant_parent_redeclarations( + interface: &EvalInterface, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for constant in interface.constants() { + for parent in interface.parents() { + if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) + { + if parent_constant.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_interface_constant_redeclaration(parent, constant, values)?; + } + } + Ok(()) +} + +/// Validates one constant declaration against inherited eval constant metadata. +pub(super) fn validate_constant_parent_redeclaration( + class: &EvalClass, + constant: &EvalClassConstant, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(parent) = class.parent() { + if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { + if parent_constant.visibility() != EvalVisibility::Private + && (parent_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(parent_constant.visibility())) + { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_class_constant_redeclaration(parent, constant, values)?; + } + for interface in pending_class_interface_names(class, context) { + if let Some((_, interface_constant)) = + context.interface_constant(&interface, constant.name()) + { + if interface_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(interface_constant.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_interface_constant_redeclaration(&interface, constant, values)?; + } + Ok(()) +} + +/// Validates a class constant redeclaration against a generated/AOT parent class constant. +pub(super) fn validate_aot_class_constant_redeclaration( + parent: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.class_exists(parent)? { + return Ok(()); + } + validate_aot_constant_redeclaration(parent, constant, false, values) +} + +/// Validates a class/interface constant redeclaration against a generated/AOT interface constant. +pub(super) fn validate_aot_interface_constant_redeclaration( + interface: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.interface_exists(interface)? { + return Ok(()); + } + validate_aot_constant_redeclaration(interface, constant, true, values) +} + +/// Applies PHP redeclaration rules to one generated/AOT constant metadata row. +pub(super) fn validate_aot_constant_redeclaration( + class_like: &str, + constant: &EvalClassConstant, + interface_context: bool, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_like = class_like.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(class_like, constant.name())? else { + return Ok(()); + }; + let inherited_visibility = eval_aot_constant_visibility(flags); + if !interface_context && inherited_visibility == EvalVisibility::Private { + return Ok(()); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(inherited_visibility) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT constant reflection flags. +pub(super) fn eval_aot_constant_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Returns a comparable rank where larger means less restrictive constant visibility. +pub(super) fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/property_validation.rs b/crates/elephc-magician/src/interpreter/statements/property_validation.rs new file mode 100644 index 0000000000..14cef80d84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/property_validation.rs @@ -0,0 +1,443 @@ +//! Purpose: +//! Validates property-hook context, readonly writes, visibility, and property failures. +//! +//! Called from: +//! - Instance and static property access paths. +//! +//! Key details: +//! - Synthetic hook names and PHP-visible access errors stay centralized. + +use super::*; + +/// Returns the synthetic get-hook method name for one property. +pub(in crate::interpreter) fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +pub(in crate::interpreter) fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} + +/// Rejects writes to readonly eval-declared properties outside their declaring constructor. +pub(super) fn validate_eval_readonly_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_method_is_declaring_constructor(declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns true while executing `__construct` for the property declaring class. +pub(super) fn current_eval_method_is_declaring_constructor( + declaring_class: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + context + .current_function() + .and_then(|function| function.rsplit_once("::")) + .is_some_and(|(_, method)| method.eq_ignore_ascii_case("__construct")) +} + +/// Resolves the property metadata visible from the current class scope, if any. +pub(super) fn eval_dynamic_property_for_access( + object_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, property)) = + context.class_own_property(current_class, property_name) + { + if property.visibility() == EvalVisibility::Private { + return Some((declaring_class, property)); + } + } + } + } + context.class_property(object_class_name, property_name) +} + +/// Returns the physical storage name for an eval object property slot. +pub(in crate::interpreter) fn eval_instance_property_storage_name( + declaring_class: &str, + property: &EvalClassProperty, +) -> String { + if property.visibility() == EvalVisibility::Private { + format!( + "\0{}\0{}", + declaring_class.trim_start_matches('\\'), + property.name() + ) + } else { + property.name().to_string() + } +} + +/// Validates the visibility that applies to property writes, including asymmetric `set` visibility. +pub(super) fn validate_eval_property_write_access( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + validate_eval_member_access(declaring_class, property.write_visibility(), context) +} + +/// Throws PHP's inaccessible property error for eval-declared properties. +pub(super) fn eval_throw_property_access_error( + declaring_class: &str, + property_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} property {}::${}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's write access error for eval-declared properties. +pub(super) fn eval_throw_property_write_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot modify {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's unset access error for asymmetric eval-declared properties. +pub(super) fn eval_throw_property_unset_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot unset {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's read-only property-hook write error. +pub(super) fn eval_throw_property_hook_readonly_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Property {}::${} is read-only", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's readonly property assignment error for eval-declared properties. +pub(super) fn eval_throw_readonly_property_modification_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot modify readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's readonly property unset error for eval-declared properties. +pub(super) fn eval_throw_readonly_property_unset_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot unset readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's dynamic property creation error for readonly eval-declared classes. +pub(super) fn eval_throw_dynamic_property_creation_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot create dynamic property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's undeclared static property error for static property access. +pub(super) fn eval_throw_undeclared_static_property_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Access to undeclared static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's uninitialized typed instance property error. +pub(super) fn eval_throw_uninitialized_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's uninitialized typed static property error. +pub(super) fn eval_throw_uninitialized_static_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed static property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's class-not-found error for unresolved static member receivers. +pub(in crate::interpreter) fn eval_throw_class_not_found_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!("Class \"{}\" not found", class_name.trim_start_matches('\\')), + context, + values, + ) +} + +/// Throws PHP's inaccessible constant error for eval-declared class constants. +pub(super) fn eval_throw_constant_access_error( + declaring_class: &str, + constant_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} constant {}::{}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) +} + +/// Throws PHP's inaccessible method error for eval-declared methods. +pub(super) fn eval_throw_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's inaccessible clone-expression error for `__clone()` hooks. +pub(super) fn eval_throw_clone_access_error( + declaring_class: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} {}::__clone() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's error for calling an instance method through static syntax. +pub(super) fn eval_throw_non_static_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's error for calling an abstract method directly. +pub(super) fn eval_throw_abstract_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's undefined method error after static magic fallback misses. +pub(super) fn eval_throw_undefined_method_call_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's error for invoking an object without `__invoke()`. +pub(in crate::interpreter) fn eval_throw_object_not_callable_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of type {} is not callable", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs b/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs new file mode 100644 index 0000000000..6f5a12cd42 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs @@ -0,0 +1,509 @@ +//! Purpose: +//! Binds and writes back method by-reference targets across runtime storage shapes. +//! +//! Called from: +//! - Eval and native method invocation after argument binding. +//! +//! Key details: +//! - Invoker slots, arrays, nested arrays, object properties, and static properties preserve aliases. + +use super::*; + +/// Returns the calling-scope class when PHP's private-method shadowing rule +/// selects it as the dispatch scope: `$obj->m()` from class scope S calls S's +/// own private instance method `m` — never the receiver's override — when S +/// declares one and the receiver is an instance of S. The returned scope +/// string drives the native bridge's hidden private shadow slot directly. +pub(in crate::interpreter) fn eval_private_scope_shadow_bridge_scope( + receiver_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope_class) = context.current_class_scope() else { + return Ok(None); + }; + let scope_class = scope_class.to_string(); + if !same_eval_class_name(receiver_class, &scope_class) + && !native_class_is_a(receiver_class, &scope_class, context) + { + return Ok(None); + } + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&scope_class, method_name, values)? + else { + return Ok(None); + }; + if visibility == EvalVisibility::Private + && !is_static + && !is_abstract + && same_eval_class_name(&declaring_class, &scope_class) + { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + +/// Binds method parameters into a fresh method scope and marks by-reference params as aliases. +pub(in crate::interpreter) fn bind_method_scope_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], +) { + for (position, (name, bound_arg)) in params.iter().zip(bound_args.iter()).enumerate() { + if parameter_is_by_ref.get(position).copied().unwrap_or(false) { + method_scope.set_reference( + name.clone(), + name.clone(), + bound_arg.value, + ScopeCellOwnership::Borrowed, + ); + if let Some(target) = bound_arg.ref_target.clone() { + method_scope.set_reference_target(name.clone(), target); + } + } else { + method_scope.set(name.clone(), bound_arg.value, ScopeCellOwnership::Borrowed); + } + } + alias_duplicate_method_ref_args(method_scope, params, bound_args); +} + +/// Creates local aliases when two by-reference method parameters point at the same caller variable. +pub(super) fn alias_duplicate_method_ref_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + bound_args: &[BoundMethodArg], +) { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(target) = bound_arg.ref_target.as_ref() else { + continue; + }; + let Some(param) = params.get(position) else { + continue; + }; + for previous_position in 0..position { + let Some(previous_target) = bound_args[previous_position].ref_target.as_ref() else { + continue; + }; + if !same_method_ref_target(target, previous_target) { + continue; + } + if let Some(previous_param) = params.get(previous_position) { + method_scope.set_reference( + param.clone(), + previous_param.clone(), + bound_args[previous_position].value, + ScopeCellOwnership::Borrowed, + ); + } + break; + } + } +} + +/// Returns true when two evaluated arguments target the same caller-side variable. +pub(super) fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarget) -> bool { + match (left, right) { + ( + EvalReferenceTarget::Variable { + scope: left_scope, + name: left_name, + }, + EvalReferenceTarget::Variable { + scope: right_scope, + name: right_name, + }, + ) => left_scope == right_scope && left_name == right_name, + ( + EvalReferenceTarget::ArrayElement { + scope: left_scope, + array_name: left_name, + index: left_index, + }, + EvalReferenceTarget::ArrayElement { + scope: right_scope, + array_name: right_name, + index: right_index, + }, + ) => left_scope == right_scope && left_name == right_name && left_index == right_index, + ( + EvalReferenceTarget::NestedArrayElement { + array_target: left_target, + index: left_index, + }, + EvalReferenceTarget::NestedArrayElement { + array_target: right_target, + index: right_index, + }, + ) => left_index == right_index && same_method_ref_target(left_target, right_target), + ( + EvalReferenceTarget::ObjectProperty { + object: left_object, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::ObjectProperty { + object: right_object, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_object == right_object + && left_property == right_property + && left_access_scope == right_access_scope + } + ( + EvalReferenceTarget::Cell { cell: left_cell }, + EvalReferenceTarget::Cell { cell: right_cell }, + ) => left_cell == right_cell, + ( + EvalReferenceTarget::InvokerSlot { + slot: left_slot, + source_tag: left_source_tag, + }, + EvalReferenceTarget::InvokerSlot { + slot: right_slot, + source_tag: right_source_tag, + }, + ) => left_slot == right_slot && left_source_tag == right_source_tag, + ( + EvalReferenceTarget::StaticProperty { + class_name: left_class_name, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::StaticProperty { + class_name: right_class_name, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_class_name == right_class_name + && left_property == right_property + && left_access_scope == right_access_scope + } + _ => false, + } +} + +/// Writes completed by-reference method parameter values back to their caller-side variables. +pub(in crate::interpreter) fn write_back_method_ref_args( + params: &[String], + bound_args: &[BoundMethodArg], + method_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(param) = params.get(position) else { + continue; + }; + if let Some(target) = bound_arg.ref_target.as_ref() { + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + write_back_method_variadic_ref_args(param, bound_arg, method_scope, context, values)?; + } + Ok(()) +} + +/// Writes element-level changes from a by-reference variadic method parameter back to callers. +pub(super) fn write_back_method_variadic_ref_args( + param: &str, + bound_arg: &BoundMethodArg, + method_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bound_arg.variadic_ref_targets.is_empty() { + return Ok(()); + } + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + return Ok(()); + }; + if entry.cell() != bound_arg.value { + return Ok(()); + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(entry.cell(), *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + Ok(()) +} + +/// Stores one by-reference result in the original caller-side target. +pub(in crate::interpreter) fn write_back_method_ref_target( + target: &EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Borrowed, + )? { + values.release(replaced)?; + } + Ok(()) + } + EvalReferenceTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + write_back_method_array_element_ref_target( + scope, array_name, *index, value, context, values, + ) + } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => write_back_method_nested_array_element_ref_target( + array_target, + *index, + value, + context, + values, + ), + EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + } => write_back_method_object_property_ref_target( + *object, + property, + access_scope.clone(), + value, + context, + values, + ), + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => write_back_method_static_property_ref_target( + class_name, + property, + access_scope.clone(), + value, + context, + values, + ), + EvalReferenceTarget::Cell { .. } => Ok(()), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + write_back_invoker_slot_ref_target(*slot, *source_tag, value, values) + } + } +} + +/// Reads a value from a native descriptor-invoker by-reference slot. +pub(super) fn eval_invoker_slot_ref_target_value( + slot: usize, + source_tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_STRING => { + let words = unsafe { *(slot as *const [u64; 2]) }; + values.raw_string_value(words[0], words[1]) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_MIXED => { + let value = unsafe { *(slot as *const RuntimeCellHandle) }; + values.retain(value) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Writes a value back into a native descriptor-invoker by-reference slot. +pub(super) fn write_back_invoker_slot_ref_target( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + unsafe { + *(slot as *mut u64) = word; + } + Ok(()) + } + EVAL_TAG_STRING => write_back_invoker_string_slot(slot, value, values), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + write_back_invoker_heap_slot(slot, source_tag, value, values) + } + EVAL_TAG_MIXED => { + let retained = values.retain(value)?; + let replaced = unsafe { + let slot = slot as *mut RuntimeCellHandle; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release(replaced) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Writes a boxed string value back into a native descriptor-invoker string slot. +pub(super) fn write_back_invoker_string_slot( + slot: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_STRING { + return Err(EvalStatus::RuntimeFatal); + } + let ptr = values.raw_value_word(value)?; + let len = values.raw_value_high_word(value)?; + let retained = values.retain_raw_string_words(ptr, len)?; + let replaced = unsafe { + let slot = slot as *mut [u64; 2]; + let replaced = *slot; + *slot = [retained.0, retained.1]; + replaced + }; + values.release_raw_string_words(replaced[0], replaced[1]) +} + +/// Writes a boxed heap value back into a native descriptor-invoker raw heap slot. +pub(super) fn write_back_invoker_heap_slot( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != source_tag { + return Err(EvalStatus::RuntimeFatal); + } + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + let replaced = unsafe { + let slot = slot as *mut u64; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release_raw_heap_word(replaced) +} + +/// Stores one by-reference method result in a caller-side array element. +pub(super) fn write_back_method_array_element_ref_target( + scope: &mut ElephcEvalScope, + array_name: &str, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope_entry(context, scope, array_name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + ownership = existing.flags().ownership; + values.array_clone_shallow(existing.cell())? + } else { + eval_new_array_for_index(index, values)? + } + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, array_name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Stores one by-reference method result in an element of a nested caller-side array target. +pub(super) fn write_back_method_nested_array_element_ref_target( + array_target: &EvalReferenceTarget, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_reference_target_value(array_target, context, values)?; + let array = if values.is_array_like(current)? { + values.array_clone_shallow(current)? + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + write_back_method_ref_target(array_target, array, context, values) +} + +/// Stores one by-reference method result in a caller-side object property. +pub(super) fn write_back_method_object_property_ref_target( + object: RuntimeCellHandle, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_property_set_result(object, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + +/// Stores one by-reference method result in a caller-side static property. +pub(super) fn write_back_method_static_property_ref_target( + class_name: &str, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_static_property_set_result(class_name, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + +/// Creates an indexed or associative array according to the first write key. +pub(super) fn eval_new_array_for_index( + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(index)? == EVAL_TAG_STRING { + values.assoc_new(1) + } else { + values.array_new(1) + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs b/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs new file mode 100644 index 0000000000..ec277218e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs @@ -0,0 +1,384 @@ +//! Purpose: +//! Implements Reflection-driven class and attribute instantiation plus access checks. +//! +//! Called from: +//! - Method dispatch for ReflectionClass and ReflectionAttribute owners. +//! +//! Key details: +//! - Constructor visibility, without-constructor allocation, and attribute argument materialization align with PHP. + +use super::*; + +/// Returns the runtime-visible class name for a non-eval object receiver. +pub(in crate::interpreter) fn runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Instantiates the class named by a materialized eval `ReflectionClass` object. +pub(super) fn eval_reflection_class_new_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let direct_new_instance = method_name.eq_ignore_ascii_case("newInstance"); + let constructor_args = if direct_new_instance { + eval_reflection_constructor_by_value_args(evaluated_args) + } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { + eval_reflection_class_new_instance_args(evaluated_args, context, values)? + } else { + return Ok(None); + }; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } + if let Some(class) = context.class(&reflected_name).cloned() { + if let Some((_, constructor)) = context.class_method(class.name(), "__construct") { + if constructor.visibility() != EvalVisibility::Public { + return eval_throw_reflection_exception( + &format!( + "Access to non-public constructor of class {}", + class.name() + ), + context, + values, + ); + } + } + return eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = + format!("{}::__construct", class.name().trim_start_matches('\\')); + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + }; + let mut scope = ElephcEvalScope::new(); + eval_dynamic_class_new_object_with_ref_mode( + &class, + constructor_args, + by_ref_mode, + context, + &mut scope, + values, + ) + .map(Some) + }); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + if let Some(error) = eval_reflection_aot_class_public_instantiation_error(&class_name, values)? + { + return eval_throw_reflection_instantiation_error(error, context, values); + } + eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = format!("{}::__construct", class_name.trim_start_matches('\\')); + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + }; + let instance = values.new_object(&class_name)?; + eval_native_constructor_with_evaluated_args_and_ref_mode( + &class_name, + instance, + constructor_args, + by_ref_mode, + context, + values, + )?; + Ok(Some(instance)) + }) +} + +/// Removes caller writeback targets for ReflectionClass::newInstance() by-value forwarding. +pub(super) fn eval_reflection_constructor_by_value_args( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} + +/// Expands the single `ReflectionClass::newInstanceArgs()` array argument. +pub(super) fn eval_reflection_class_new_instance_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], context, values) +} + +/// Runs ReflectionClass construction with only public constructor visibility. +pub(super) fn eval_reflection_public_constructor_scope( + context: &mut ElephcEvalContext, + values: &mut V, + action: impl FnOnce(&mut ElephcEvalContext, &mut V) -> Result, +) -> Result { + context.push_class_scope(String::new()); + let result = action(context, values); + context.pop_class_scope(); + result +} + +/// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. +pub(super) fn eval_reflection_class_new_instance_without_constructor_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("newInstanceWithoutConstructor") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } + if let Some(class) = context.class(&reflected_name).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_allocate_object(&class, context, &mut scope, values).map(Some); + } + if context.has_interface(&reflected_name) + || context.has_trait(&reflected_name) + || context.has_enum(&reflected_name) + { + return Err(EvalStatus::RuntimeFatal); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + if let Some(message) = + eval_reflection_aot_class_without_constructor_error(&class_name, values)? + { + return eval_throw_error(&message, context, values); + } + values.new_object(&class_name).map(Some) +} + +/// Builds PHP's reflection instantiation error for eval non-instantiable class-likes. +pub(super) fn eval_reflection_eval_instantiation_error_message( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(class) = context.class(reflected_name) { + if class.is_abstract() { + return Some(format!("Cannot instantiate abstract class {}", class.name())); + } + if let Some(enum_decl) = context.enum_decl(class.name()) { + return Some(format!("Cannot instantiate enum {}", enum_decl.name())); + } + return None; + } + if let Some(interface) = context.interface(reflected_name) { + return Some(format!("Cannot instantiate interface {}", interface.name())); + } + if let Some(trait_decl) = context.trait_decl(reflected_name) { + return Some(format!("Cannot instantiate trait {}", trait_decl.name())); + } + context + .enum_decl(reflected_name) + .map(|enum_decl| format!("Cannot instantiate enum {}", enum_decl.name())) +} + +/// Instantiates an attribute class for `ReflectionAttribute::newInstance()`. +pub(super) fn eval_reflection_attribute_new_instance_result( + attribute: &EvalAttribute, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = eval_reflection_attribute_evaluated_args(attribute, values)?; + if let Some(class) = context.class(attribute.name()).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_new_object(&class, args, context, &mut scope, values); + } + let class_name = context + .resolve_class_name(attribute.name()) + .unwrap_or_else(|| attribute.name().trim_start_matches('\\').to_string()); + if !values.class_exists(&class_name)? { + return values.null(); + } + let object = values.new_object(&class_name)?; + if let Err(err) = eval_native_constructor_with_evaluated_args( + &class_name, + object, + args, + context, + values, + ) { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} + +/// Materializes eval attribute literal arguments as evaluated constructor args. +pub(super) fn eval_reflection_attribute_evaluated_args( + attribute: &EvalAttribute, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + args.iter() + .map(|arg| { + Ok(EvaluatedCallArg { + name: arg.name().map(str::to_string), + value: eval_reflection_attribute_arg_value(arg.value(), values)?, + ref_target: None, + }) + }) + .collect() +} + +/// Materializes one eval attribute literal as a constructor argument cell. +pub(super) fn eval_reflection_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => { + eval_reflection_attribute_array_arg_value(elements, values) + } + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_reflection_attribute_arg_value(value, values) + } + } +} + +/// Materializes one retained attribute array literal for constructor calls. +pub(super) fn eval_reflection_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + for (index, element) in elements.iter().enumerate() { + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, + }; + let value = eval_reflection_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Resolves the method metadata visible from the current class scope. +pub(in crate::interpreter) fn eval_dynamic_method_for_call( + object_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(object_class_name, method_name) +} + +/// Returns whether the current eval class scope can access one declared member. +pub(in crate::interpreter) fn validate_eval_member_access( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if visibility == EvalVisibility::Public { + return Ok(()); + } + let Some(current_class) = context.current_class_scope() else { + return Err(EvalStatus::RuntimeFatal); + }; + match visibility { + EvalVisibility::Public => Ok(()), + EvalVisibility::Private => same_eval_class_name(current_class, declaring_class) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal), + EvalVisibility::Protected => { + eval_classes_are_related(current_class, declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) + } + } +} + +/// Returns true when two PHP class names refer to the same eval class. +pub(super) fn same_eval_class_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns true when two eval or generated classes are in the same inheritance family. +pub(super) fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext) -> bool { + same_eval_class_name(left, right) + || context.class_is_a(left, right, false) + || context.class_is_a(right, left, false) + || native_class_is_a(left, right, context) + || native_class_is_a(right, left, context) +} + +/// Returns true when generated AOT parent metadata proves one class extends another. +pub(super) fn native_class_is_a(class_name: &str, target: &str, context: &ElephcEvalContext) -> bool { + let mut current = class_name.trim_start_matches('\\').to_string(); + let target = target.trim_start_matches('\\'); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if same_eval_class_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs new file mode 100644 index 0000000000..31cf91fa48 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs @@ -0,0 +1,240 @@ +//! Purpose: +//! Resolves and dispatches eval-declared static method calls. +//! +//! Called from: +//! - Static-call expression evaluation. +//! +//! Key details: +//! - Called-class scope and private-method resolution are preserved across entry points. + +use super::*; + +/// Dispatches a static method call to an eval-declared static method. +pub(in crate::interpreter) fn eval_static_method_call_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Dispatches a static-syntax method call from an expression scope that may hold `$this`. +pub(in crate::interpreter) fn eval_static_method_call_result_from_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + Some(scope), + context, + values, + ) +} + +/// Dispatches a static method call using a first-class callable's captured called class. +pub(in crate::interpreter) fn eval_static_method_call_result_with_called_class( + class_name: &str, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + let called_class_name = context + .resolve_class_name(called_class_name) + .unwrap_or_else(|| called_class_name.trim_start_matches('\\').to_string()); + eval_static_method_call_result_resolved( + class_name, + called_class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Dispatches a static method call after lookup and late-static names have been resolved. +pub(super) fn eval_static_method_call_result_resolved( + class_name: String, + called_class_name: String, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_closure_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_builtin_property_hook_type_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_create_from_method_name_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + &class_name, + method_name, + evaluated_args, + context, + values, + ); + } + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&class_name, method_name, context) + { + if method.is_abstract() { + return eval_throw_abstract_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + if let Some(result) = eval_magic_static_method_call( + &class_name, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if !method.is_static() { + if let Some(object) = + eval_static_syntax_instance_receiver(&class_name, lexical_scope, context, values)? + { + return eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } + return eval_dynamic_static_method_with_values( + &declaring_class, + &called_class_name, + &method, + evaluated_args, + context, + values, + ); + } + if context.has_class(&class_name) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some(result) = eval_native_static_syntax_method_result( + &parent, + Some(&called_class_name), + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? + { + return Ok(result); + } + } + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if let Some(result) = eval_magic_static_method_call( + &class_name, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + return eval_throw_undefined_method_call_error( + &class_name, + method_name, + context, + values, + ); + } + if let Some(result) = eval_native_static_syntax_method_result( + &class_name, + None, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { + return Ok(result); + } + eval_native_static_method_with_evaluated_args( + &class_name, + method_name, + evaluated_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/static_property_access.rs b/crates/elephc-magician/src/interpreter/statements/static_property_access.rs new file mode 100644 index 0000000000..6c90827259 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/static_property_access.rs @@ -0,0 +1,827 @@ +//! Purpose: +//! Executes static-property, class-constant, and static reference operations. +//! +//! Called from: +//! - Expression and statement dispatch for class-member access. +//! +//! Key details: +//! - Receiver resolution, hooks, enum/reflection constants, and reference aliases share one path. + +use super::*; + +/// Reads one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_get_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if let Some(value) = context.static_property(&declaring_class, property.name()) { + return Ok(value); + } + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property.name(), + context, + values, + ); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(value); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } + } + } + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(value); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} + +/// Returns whether a static property is PHP-`isset()` without throwing for missing properties. +pub(in crate::interpreter) fn eval_static_property_isset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return Ok(false); + } + let value = if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_reference_target_value(&target, context, values)? + } else { + let Some(value) = context.static_property(&declaring_class, property.name()) else { + return Ok(false); + }; + value + }; + return Ok(!values.is_null(value)?); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return Ok(false); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(!values.is_null(value)?); + } + } + } + return Ok(false); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return Ok(false); + } + } else if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(!values.is_null(value)?); + } + Ok(false) +} + +/// Throws PHP's catchable error for attempts to unset an existing static property target. +pub(in crate::interpreter) fn eval_static_property_unset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + eval_throw_error( + &format!( + "Attempt to unset static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Reads one eval-declared class constant after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_class_constant_fetch_result( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_builtin_reflection_class_constant(class_name, constant_name, values)? + { + return Ok(value); + } + if let Some(value) = + eval_builtin_property_hook_type_case(class_name, constant_name, context, values)? + { + return Ok(value); + } + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some(case) = context.enum_case(&class_name, constant_name) { + return Ok(case); + } + if let Some((declaring_class, constant)) = context.class_constant(&class_name, constant_name) { + if validate_eval_member_access(&declaring_class, constant.visibility(), context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant.name(), + constant.visibility(), + context, + values, + ); + } + return context + .class_constant_cell(&declaring_class, constant.name()) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some((declaring_class, visibility)) = + eval_dynamic_class_native_constant_metadata( + &class_name, + constant_name, + context, + values, + )? + { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant_name, + visibility, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.class_constant_get(&declaring_class, constant_name), + )? { + return Ok(value); + } + } + return Err(EvalStatus::RuntimeFatal); + } + if let Some(value) = values.class_constant_get(&class_name, constant_name)? { + return Ok(value); + } + eval_throw_error( + &format!( + "Undefined constant {}::{}", + class_name.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) +} + +/// Resolves eval-visible built-in Reflection class constants. +pub(super) fn eval_builtin_reflection_class_constant( + class_name: &str, + constant_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + let value = if class_name.eq_ignore_ascii_case("ReflectionClass") { + match constant_name { + "IS_IMPLICIT_ABSTRACT" => Some(16), + "IS_FINAL" => Some(32), + "IS_EXPLICIT_ABSTRACT" => Some(64), + "IS_READONLY" => Some(65_536), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionMethod") { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_STATIC" => Some(16), + "IS_FINAL" => Some(32), + "IS_ABSTRACT" => Some(64), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionProperty") { + match constant_name { + "IS_STATIC" => Some(16), + "IS_READONLY" => Some(128), + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_ABSTRACT" => Some(64), + "IS_PROTECTED_SET" => Some(2048), + "IS_PRIVATE_SET" => Some(4096), + "IS_VIRTUAL" => Some(512), + "IS_FINAL" => Some(32), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") + || class_name.eq_ignore_ascii_case("ReflectionEnumUnitCase") + || class_name.eq_ignore_ascii_case("ReflectionEnumBackedCase") + { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_FINAL" => Some(32), + _ => None, + } + } else { + None + }; + value.map(|value| values.int(value)).transpose() +} + +/// Resolves eval-visible `PropertyHookType` builtin enum cases. +pub(super) fn eval_builtin_property_hook_type_case( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + let Some((case_name, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + return Ok(None); + }; + if let Some(case) = context.enum_case("PropertyHookType", case_name) { + return Ok(Some(case)); + } + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, "PropertyHookType"); + let name = values.string(case_name)?; + values.property_set(object, "name", name)?; + let value = values.string(case_value)?; + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value("PropertyHookType", case_name, value) { + values.release(replaced)?; + } + if let Some(replaced) = context.set_enum_case("PropertyHookType", case_name, object) { + values.release(replaced)?; + } + Ok(Some(object)) +} + +/// Returns the PHP case name and backed value for a builtin property-hook case. +pub(super) fn eval_property_hook_type_case_parts(constant_name: &str) -> Option<(&'static str, &'static str)> { + match constant_name { + "Get" => Some(("Get", "get")), + "Set" => Some(("Set", "set")), + _ => None, + } +} + +/// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. +pub(in crate::interpreter) fn eval_class_name_fetch_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_class_name_literal(class_name, context)?; + values.string(&class_name) +} + +/// Binds one eval-declared static property to a by-reference source variable. +pub(super) fn eval_static_property_reference_bind_result( + class_name: &str, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property.name(), + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property.name(), target); + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if values.static_property_set(&class_name, property_name, value)? { + return Ok(()); + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} + +/// Resolves a local by-reference source into a persistent static-property alias target. +pub(super) fn eval_static_property_reference_target( + class_name: &str, + property_name: &str, + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_static_property_reference_alias_name(class_name, property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name backing one static-property reference alias. +pub(super) fn eval_static_property_reference_alias_name(class_name: &str, property_name: &str) -> String { + format!("\0elephc_static_property_ref:{class_name}:{property_name}") +} + +/// Writes one eval static-property assignment through its persistent reference target. +pub(super) fn eval_static_reference_target_write( + class_name: &str, + property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_static_property_alias( + class_name, + property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + +/// Writes one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_set_result( + class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property.name(), + target, + value, + context, + values, + )?; + } + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + if is_static { + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + } + } + if values.static_property_set(&class_name, property_name, value)? { + return Ok(()); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs b/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs new file mode 100644 index 0000000000..473fd78305 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs @@ -0,0 +1,791 @@ +//! Purpose: +//! Registers interfaces and traits and composes trait members into classes. +//! +//! Called from: +//! - Class-like declaration execution before validation and registration. +//! +//! Key details: +//! - Trait adaptations, conflicts, aliases, visibility, constants, and properties are resolved here. + +use super::*; + +/// Registers an eval-declared interface in the dynamic interface table. +pub(in crate::interpreter) fn execute_interface_decl_stmt( + interface: &EvalInterface, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = interface.name().trim_start_matches('\\'); + if context.has_interface(name) + || context.has_class(name) + || context.has_enum(name) + || eval_runtime_interface_exists(name, values)? + || values.class_exists(name)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + for parent in interface.parents() { + if context + .interface_parent_names(parent) + .iter() + .any(|ancestor| ancestor.eq_ignore_ascii_case(name)) + { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(parent) && !eval_runtime_interface_exists(parent, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_interface_attribute_targets(interface)?; + validate_eval_interface_override_attributes(interface, context, values)?; + validate_eval_declared_constants(interface.constants())?; + validate_eval_interface_constants(interface.constants())?; + validate_interface_constant_parent_redeclarations(interface, context, values)?; + if context.define_interface(interface.clone()) { + initialize_eval_declared_constants( + interface.name(), + interface.constants(), + context, + scope, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Registers an eval-declared trait in the dynamic trait table. +pub(in crate::interpreter) fn execute_trait_decl_stmt( + trait_decl: &EvalTrait, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = trait_decl.name().trim_start_matches('\\'); + if context.has_trait(name) + || context.has_class(name) + || context.has_interface(name) + || context.has_enum(name) + || values.trait_exists(name)? + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + let trait_decl = expand_eval_trait_traits(trait_decl, context)?; + validate_eval_trait_attribute_targets(&trait_decl)?; + validate_eval_declared_constants(trait_decl.constants())?; + validate_eval_magic_methods(trait_decl.methods())?; + if context.define_trait(trait_decl.clone()) { + initialize_eval_declared_constants( + trait_decl.name(), + trait_decl.constants(), + context, + scope, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Expands nested eval trait uses into the trait metadata registered by eval. +pub(super) fn expand_eval_trait_traits( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result { + if trait_decl.traits().is_empty() { + return Ok(trait_decl.clone()); + } + validate_eval_trait_decl_adaptations(trait_decl, context)?; + let trait_method_names = trait_method_name_set(trait_decl); + let mut imported_method_names = std::collections::HashSet::new(); + let mut imported_properties = std::collections::HashMap::new(); + let mut imported_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for used_trait_name in trait_decl.traits() { + let Some(used_trait_decl) = context.trait_decl(used_trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + used_trait_decl, + trait_decl.constants(), + &mut imported_constants, + &mut constants, + )?; + append_eval_trait_properties( + used_trait_decl, + trait_decl.properties(), + &mut imported_properties, + &mut properties, + )?; + append_eval_trait_methods( + used_trait_decl, + trait_decl.trait_adaptations(), + &trait_method_names, + &mut imported_method_names, + &mut methods, + )?; + } + constants.extend(trait_decl.constants().iter().cloned()); + properties.extend(trait_decl.properties().iter().cloned()); + methods.extend(trait_decl.methods().iter().cloned()); + let mut expanded = EvalTrait::with_constants_traits_adaptations( + trait_decl.name().to_string(), + constants, + properties, + methods, + trait_decl.traits().to_vec(), + trait_decl.trait_adaptations().to_vec(), + ) + .with_attributes(trait_decl.attributes().to_vec()); + if let Some(source_location) = trait_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + +/// Validates that trait-level adaptations reference directly used traits and methods. +pub(super) fn validate_eval_trait_decl_adaptations( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in trait_decl.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + trait_name.as_deref(), + method, + )?, + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + Some(trait_name), + method, + )?; + for suppressed in instead_of { + if eval_trait_used_trait_decl(trait_decl, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one trait-level adaptation method target. +pub(super) fn validate_eval_trait_decl_adaptation_method( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(used_trait_decl) = eval_trait_used_trait_decl(trait_decl, context, trait_name) + else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(used_trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + trait_decl + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|used_trait_decl| trait_has_method(used_trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending trait directly uses that trait. +pub(super) fn eval_trait_used_trait_decl<'a>( + trait_decl: &EvalTrait, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + trait_decl + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + +/// Expands eval trait uses into the class metadata used by dynamic dispatch. +pub(super) fn expand_eval_class_traits( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result { + if class.traits().is_empty() { + return Ok(class.clone()); + } + validate_eval_trait_adaptations(class, context)?; + let class_method_names = class_method_name_set(class); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); + let mut trait_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in class.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + trait_decl, + class.constants(), + &mut trait_constants, + &mut constants, + )?; + append_eval_trait_properties( + trait_decl, + class.properties(), + &mut trait_properties, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + class.trait_adaptations(), + &class_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + constants.extend(class.constants().iter().cloned()); + properties.extend(class.properties().iter().cloned()); + methods.extend(class.methods().iter().cloned()); + let mut expanded = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + class.name().to_string(), + class.is_abstract(), + class.is_final(), + class.is_readonly_class(), + class.parent().map(str::to_string), + class.interfaces().to_vec(), + class.traits().to_vec(), + class.trait_adaptations().to_vec(), + constants, + properties, + methods, + ) + .with_attributes(class.attributes().to_vec()); + if class.is_anonymous() { + expanded = expanded.with_anonymous(); + } + Ok(expanded) +} + +/// Validates that trait adaptations reference used traits and existing methods. +pub(super) fn validate_eval_trait_adaptations( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in class.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => { + validate_eval_trait_adaptation_method(class, context, trait_name.as_deref(), method)? + } + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_adaptation_method(class, context, Some(trait_name), method)?; + for suppressed in instead_of { + if eval_used_trait_decl(class, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one adaptation method target, allowing unqualified alias targets. +pub(super) fn validate_eval_trait_adaptation_method( + class: &EvalClass, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(trait_decl) = eval_used_trait_decl(class, context, trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + class + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|trait_decl| trait_has_method(trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending class directly uses that trait. +pub(super) fn eval_used_trait_decl<'a>( + class: &EvalClass, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + class + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + +/// Returns whether a trait declares a method by PHP case-insensitive method name. +pub(super) fn trait_has_method(trait_decl: &EvalTrait, method: &str) -> bool { + trait_decl + .methods() + .iter() + .any(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) +} + +/// Returns case-insensitive method names declared directly by a pending trait. +pub(super) fn trait_method_name_set(trait_decl: &EvalTrait) -> std::collections::HashSet { + trait_decl + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + +/// Returns case-insensitive method names declared directly by a pending class. +pub(super) fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { + class + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + +/// Appends trait constants while enforcing PHP-compatible same-name conflicts. +pub(super) fn append_eval_trait_constants( + trait_decl: &EvalTrait, + class_constants: &[EvalClassConstant], + trait_constants: &mut std::collections::HashMap, + constants: &mut Vec, +) -> Result<(), EvalStatus> { + for constant in trait_decl.constants() { + if let Some(class_constant) = class_constants + .iter() + .find(|class_constant| class_constant.name() == constant.name()) + { + validate_eval_trait_constant_compatibility(class_constant, constant)?; + continue; + } + if let Some(existing) = trait_constants.get(constant.name()) { + validate_eval_trait_constant_compatibility(existing, constant)?; + continue; + } + let constant = constant + .clone() + .with_trait_origin(trait_decl.name().to_string()); + trait_constants.insert(constant.name().to_string(), constant.clone()); + constants.push(constant); + } + Ok(()) +} + +/// Validates that a same-name trait constant definition is compatible with PHP composition. +pub(super) fn validate_eval_trait_constant_compatibility( + existing: &EvalClassConstant, + incoming: &EvalClassConstant, +) -> Result<(), EvalStatus> { + if existing.visibility() == incoming.visibility() + && existing.is_final() == incoming.is_final() + && existing.value() == incoming.value() + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Appends trait properties while enforcing PHP-compatible same-name conflicts. +pub(super) fn append_eval_trait_properties( + trait_decl: &EvalTrait, + class_properties: &[EvalClassProperty], + trait_properties: &mut std::collections::HashMap, + properties: &mut Vec, +) -> Result<(), EvalStatus> { + for property in trait_decl.properties() { + if let Some(class_property) = class_properties + .iter() + .find(|class_property| class_property.name() == property.name()) + { + validate_eval_trait_property_compatibility(class_property, property)?; + continue; + } + if let Some(existing) = trait_properties.get(property.name()) { + let resolved = resolve_eval_trait_property_conflict(existing, property)?; + if &resolved != existing { + trait_properties.insert(property.name().to_string(), resolved.clone()); + if let Some(slot) = properties + .iter_mut() + .find(|candidate| candidate.name() == property.name()) + { + *slot = resolved; + } + } + continue; + } + let property = property + .clone() + .with_trait_origin(trait_decl.name().to_string()); + trait_properties.insert(property.name().to_string(), property.clone()); + properties.push(property); + } + Ok(()) +} + +/// Validates that a same-name trait property definition is compatible with PHP composition. +pub(super) fn validate_eval_trait_property_compatibility( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result<(), EvalStatus> { + resolve_eval_trait_property_conflict(existing, incoming).map(|_| ()) +} + +/// Resolves compatible same-name properties imported from classes and traits. +pub(super) fn resolve_eval_trait_property_conflict( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result { + if existing.is_abstract() && !incoming.is_abstract() { + return class_property_satisfies_abstract_contract(incoming, existing) + .then(|| incoming.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if incoming.is_abstract() && !existing.is_abstract() { + return class_property_satisfies_abstract_contract(existing, incoming) + .then(|| existing.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if existing.is_abstract() && incoming.is_abstract() { + return eval_trait_abstract_properties_are_compatible(existing, incoming) + .then(|| merge_abstract_property_contracts(existing, incoming)) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_trait_concrete_properties_are_compatible(existing, incoming) { + Ok(existing.clone()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether two concrete same-name trait properties are identical enough to deduplicate. +pub(super) fn eval_trait_concrete_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.is_abstract() == incoming.is_abstract() + && existing.has_get_hook() == incoming.has_get_hook() + && existing.has_set_hook() == incoming.has_set_hook() + && existing.requires_get_hook() == incoming.requires_get_hook() + && existing.requires_set_hook() == incoming.requires_set_hook() + && existing.is_virtual() == incoming.is_virtual() + && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() + && existing.default() == incoming.default() +} + +/// Returns whether two abstract trait property contracts can be merged. +pub(super) fn eval_trait_abstract_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() + && existing.default() == incoming.default() +} + +/// Appends trait methods unless the class provides a same-name method. +pub(super) fn append_eval_trait_methods( + trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for method in trait_decl.methods() { + if trait_method_suppressed_by_insteadof(trait_decl.name(), method.name(), trait_adaptations) + { + continue; + } + let key = method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) { + continue; + } + let method = method + .clone() + .with_trait_origin(trait_decl.name().to_string()); + let method = apply_trait_visibility_adaptations( + trait_decl.name(), + &method, + trait_adaptations, + ); + if !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(method); + } + append_eval_trait_method_aliases( + trait_decl, + trait_adaptations, + class_method_names, + trait_method_names, + methods, + ) +} + +/// Appends trait method aliases declared with `as`. +pub(super) fn append_eval_trait_method_aliases( + trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + visibility, + } = adaptation + else { + continue; + }; + if !trait_adaptation_target_matches( + trait_name.as_deref(), + method, + trait_decl.name(), + method, + ) { + continue; + } + let Some(source_method) = trait_decl + .methods() + .iter() + .find(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) + else { + if trait_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + }; + let mut alias_method = source_method + .clone() + .with_trait_origin(trait_decl.name().to_string()) + .renamed(alias.clone()); + if let Some(visibility) = visibility { + alias_method = alias_method.with_visibility_override(*visibility); + } + let key = alias_method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) { + continue; + } + if trait_method_names.contains(&key) + && source_method.name().eq_ignore_ascii_case(alias) + && alias_method.visibility() == source_method.visibility() + { + continue; + } + if !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(alias_method); + } + Ok(()) +} + +/// Returns whether an `insteadof` adaptation suppresses this trait method import. +pub(super) fn trait_method_suppressed_by_insteadof( + trait_name: &str, + method_name: &str, + trait_adaptations: &[EvalTraitAdaptation], +) -> bool { + trait_adaptations.iter().any(|adaptation| { + let EvalTraitAdaptation::InsteadOf { + trait_name: selected_trait, + method, + instead_of, + } = adaptation + else { + return false; + }; + method.eq_ignore_ascii_case(method_name) + && instead_of + .iter() + .any(|suppressed| same_eval_class_name(suppressed, trait_name)) + && !selected_trait + .as_deref() + .is_some_and(|selected| same_eval_class_name(selected, trait_name)) + }) +} + +/// Applies visibility-only `as` adaptations to an imported trait method. +pub(super) fn apply_trait_visibility_adaptations( + trait_name: &str, + method: &EvalClassMethod, + trait_adaptations: &[EvalTraitAdaptation], +) -> EvalClassMethod { + let mut method = method.clone(); + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name: target_trait, + method: target_method, + alias: None, + visibility: Some(visibility), + } = adaptation + else { + continue; + }; + if trait_adaptation_target_matches( + target_trait.as_deref(), + target_method, + trait_name, + method.name(), + ) { + method = method.with_visibility_override(*visibility); + } + } + method +} + +/// Returns whether an adaptation target selects one trait method. +pub(super) fn trait_adaptation_target_matches( + target_trait: Option<&str>, + target_method: &str, + trait_name: &str, + method_name: &str, +) -> bool { + target_method.eq_ignore_ascii_case(method_name) + && target_trait.map_or(true, |target_trait| { + same_eval_class_name(target_trait, trait_name) + }) +} + +/// Rejects non-enum classes that implement PHP's native enum interfaces. +pub(super) fn validate_eval_class_does_not_implement_enum_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_enum_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects eval classes and enums that directly implement PHP's Throwable contract. +pub(super) fn validate_eval_class_does_not_implement_throwable_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_throwable_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Validates abstract/final modifiers on an eval-declared class and its methods. +pub(super) fn validate_eval_class_modifiers( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if class.is_abstract() && class.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_class_attribute_targets(class.attributes())?; + if class.is_readonly_class() && eval_class_has_allow_dynamic_properties_attribute(class) { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_declared_constants(class.constants())?; + for constant in class.constants() { + validate_constant_parent_redeclaration(class, constant, context, values)?; + } + validate_eval_declared_properties(class, context)?; + for property in class.properties() { + validate_property_parent_redeclaration(class, property, context, values)?; + } + for method in class.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + validate_eval_magic_method(method)?; + if method.is_abstract() && method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && method.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_static() && method.name().eq_ignore_ascii_case("__construct") { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !class.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_method_parent_override(class, method, context)?; + validate_method_aot_parent_override(class, method, context, values)?; + validate_eval_override_attribute(class, method, context, values)?; + } + Ok(()) +} + +/// Returns whether a class carries PHP's global `#[AllowDynamicProperties]` attribute. +pub(super) fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool { + eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") +} diff --git a/crates/elephc-magician/src/interpreter/tests/array_literals.rs b/crates/elephc-magician/src/interpreter/tests/array_literals.rs new file mode 100644 index 0000000000..af619cb715 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/array_literals.rs @@ -0,0 +1,166 @@ +//! Purpose: +//! Interpreter tests for EvalIR array literal key allocation and reads. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases focus on PHP array-key normalization during literal evaluation. + +use super::super::*; +use super::support::*; + +/// Verifies indexed array literals and reads execute through runtime hooks. +#[test] +fn execute_program_reads_indexed_array_literal() { + let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. +#[test] +fn execute_program_reads_legacy_array_literal() { + let program = + parse_fragment(br#"return array("a", "b" => "bee",)[0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("a".to_string())); +} +/// Verifies associative array literals and string-key reads execute through runtime hooks. +#[test] +fn execute_program_reads_assoc_array_literal() { + let program = + parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} +/// Verifies unkeyed assoc literal entries start at zero after string keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { + let program = + parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} +/// Verifies unkeyed assoc literal entries use one plus the largest integer key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { + let program = + parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { + let program = + parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies numeric string literal keys update the next automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { + let program = + parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies leading-zero string literal keys do not update the automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { + let program = + parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies null literal keys normalize to empty strings without advancing automatic keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_null_key() { + let program = + parse_fragment(br#"return [null => "empty", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies null literal keys are readable through the empty-string key. +#[test] +fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { + let program = parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("empty".to_string())); +} +/// Verifies boolean literal keys update the next automatic key after integer normalization. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { + let program = + parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies false literal keys update the next automatic key from zero. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_false_key() { + let program = + parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies float literal keys update the next automatic key after truncation. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_float_key() { + let program = + parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs new file mode 100644 index 0000000000..a4a7f6b772 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -0,0 +1,611 @@ +//! Purpose: +//! Interpreter tests for array aggregation, mapping, mutation, and sorting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover array builtins that transform or reorder array values. + +use super::super::*; +use super::support::*; + +/// Verifies eval `ord()` returns the first byte and supports callable dispatch. +#[test] +fn execute_program_dispatches_ord_builtin() { + let program = parse_fragment( + br#"echo ord("A"); +echo ":" . ord(""); +echo ":" . call_user_func("ord", "B"); +echo ":" . call_user_func_array("ord", ["C"]); +echo ":"; echo function_exists("ord"); +return ord("Z");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "65:0:66:67:1"); + assert_eq!(values.get(result), FakeValue::Int(90)); +} +/// Verifies eval array aggregate builtins iterate array values and support callable dispatch. +#[test] +fn execute_program_dispatches_array_aggregate_builtins() { + let program = parse_fragment( + br#"echo array_sum([1, 2, 3]); +echo ":" . array_product([2, 3, 4]); +echo ":" . array_sum([]); +echo ":" . array_product([]); +echo ":" . array_sum(["a" => 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); +return function_exists("array_product");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:24:0:1:7:7:10:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_map()` applies callbacks and preserves source keys. +#[test] +fn execute_program_dispatches_array_map_builtin() { + let program = parse_fragment( + br#"function eval_map_double($value) { return $value * 2; } +$mapped = array_map("eval_map_double", [1, 2, 3]); +echo $mapped[0] . ":" . $mapped[2] . ":"; +$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +return function_exists("array_map");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_reduce()` folds values through a string callback. +#[test] +fn execute_program_dispatches_array_reduce_builtin() { + let program = parse_fragment( + br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } +echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; +function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce([4, 5], "eval_reduce_sum") . ":"; +echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; +$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); +echo $named . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); +echo $call . ":"; +$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +return function_exists("array_reduce");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "16:9:ab:13:9:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. +#[test] +fn execute_program_dispatches_array_walk_builtin() { + let program = parse_fragment( + br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } +$show = ["a" => 2, "b" => 3]; +echo array_walk($show, "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +function eval_walk_add_key(&$value, $key) { $value = $value + $key + 1; } +$items = [10, 20]; +array_walk($items, "eval_walk_add_key"); +echo $items[0] . ":" . $items[1] . ":"; +$aliased = [3, 4]; +$alias =& $aliased; +array_walk($alias, "eval_walk_add_key"); +echo $aliased[0] . ":" . $alias[1] . ":"; +$original = [5, 6]; +$copy = $original; +array_walk($copy, "eval_walk_add_key"); +echo $original[0] . ":" . $original[1] . ":" . $copy[0] . ":" . $copy[1] . ":"; +class EvalArrayWalkByRefCallbackBox { + public function tag(&$value, $key) { $value = $value . $key; } +} +$letters = ["a" => "X", "b" => "Y"]; +array_walk($letters, [new EvalArrayWalkByRefCallbackBox(), "tag"]); +echo $letters["a"] . ":" . $letters["b"] . ":"; +$named = [1]; +array_walk(callback: "eval_walk_add_key", array: $named); +echo $named[0] . ":"; +return function_exists("array_walk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a=2;b=3;T:0=4;1=5;z=6;11:22:4:6:5:6:6:8:Xa:Yb:2:" + ); + assert_eq!( + values.warnings, + vec![ + "array_walk(): Argument #1 ($array) must be passed by reference, value given", + "array_walk(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_pop()` and `array_shift()` write back writable lvalue calls. +#[test] +fn execute_program_dispatches_array_pop_shift_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; +echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +class EvalArrayPopShiftPropertyBox { + public array $items = ["p", "q"]; + public static array $staticItems = ["s", "t"]; +} +$box = new EvalArrayPopShiftPropertyBox(); +echo array_pop($box->items) . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$name = "items"; +echo array_push($box->{$name}, "r") . ":" . $box->items[1] . ":"; +echo array_shift(EvalArrayPopShiftPropertyBox::$staticItems) . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":"; +$class = "EvalArrayPopShiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "u") . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":" . EvalArrayPopShiftPropertyBox::$staticItems[1] . ":"; +return function_exists("array_pop") && function_exists("array_shift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:" + ); + assert_eq!( + values.warnings, + vec![ + "array_pop(): Argument #1 ($array) must be passed by reference, value given", + "array_shift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_push()` and `array_unshift()` write back writable lvalue calls. +#[test] +fn execute_program_dispatches_array_push_unshift_builtins() { + let program = parse_fragment( + br#"$a = [1]; +echo array_push($a, 2, 3) . ":" . $a[2] . ":"; +$b = ["x" => 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +class EvalArrayPushUnshiftPropertyBox { + public array $items = ["p"]; + public static array $staticItems = ["s"]; +} +$box = new EvalArrayPushUnshiftPropertyBox(); +echo array_push($box->items, "q", "r") . ":" . $box->items[2] . ":"; +$name = "items"; +echo array_unshift($box->{$name}, "o") . ":" . $box->items[0] . ":" . $box->items[3] . ":"; +echo array_push(EvalArrayPushUnshiftPropertyBox::$staticItems, "t") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[1] . ":"; +$class = "EvalArrayPushUnshiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "r") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[0] . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[2] . ":"; +return function_exists("array_push") && function_exists("array_unshift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:3:r:4:o:r:2:t:3:r:t:" + ); + assert_eq!( + values.warnings, + vec![ + "array_push(): Argument #1 ($array) must be passed by reference, value given", + "array_unshift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval array mutators write aliases and preserve copied array sources. +#[test] +fn execute_program_array_mutators_preserve_aliases_and_cow() { + let program = parse_fragment( + br#"$original = [1, 2, 3]; +$copy = $original; +array_pop($copy); +echo count($original) . ":" . $original[2] . ":" . count($copy) . ":"; +$aliased = [4, 5]; +$alias =& $aliased; +array_push($alias, 6); +echo count($aliased) . ":" . $aliased[2] . ":"; +$sortOriginal = [3, 1]; +$sortCopy = $sortOriginal; +sort($sortCopy); +echo $sortOriginal[0] . ":" . $sortCopy[0] . ":"; +$popFn = array_pop(...); +$callableCopy = [7, 8]; +echo $popFn($callableCopy) . ":" . count($callableCopy) . ":"; +$sortFn = sort(...); +$callableSort = [9, 7]; +$sortFn($callableSort); +echo $callableSort[0] . ":"; +function eval_mutator_walk(&$value, $key) { $value = $value + $key + 1; } +$walkFn = array_walk(...); +$walked = [2]; +$walkFn($walked, "eval_mutator_walk"); +echo $walked[0] . ":"; +$setFn = settype(...); +$typed = 10; +echo $setFn($typed, "string") ? gettype($typed) . ":" . $typed . ":" : "bad:"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:2:3:6:3:1:8:1:7:3:string:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_splice()` returns removed values and writes back writable lvalue calls. +#[test] +fn execute_program_dispatches_array_splice_builtin() { + let program = parse_fragment( + br#"$a = [10, 20, 30, 40]; +$removed = array_splice($a, 1, 2); +echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +class EvalArraySplicePropertyBox { + public array $items = ["a", "b", "c"]; + public static array $staticItems = ["x", "y", "z"]; +} +$box = new EvalArraySplicePropertyBox(); +$propRemoved = array_splice($box->items, 1, 1, ["B"]); +echo count($propRemoved) . ":" . $propRemoved[0] . ":" . $box->items[1] . ":" . $box->items[2] . ":"; +$name = "items"; +$dynRemoved = array_splice($box->{$name}, 0, 1); +echo $dynRemoved[0] . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$staticRemoved = array_splice(EvalArraySplicePropertyBox::$staticItems, 1, 1); +echo $staticRemoved[0] . ":" . count(EvalArraySplicePropertyBox::$staticItems) . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +$class = "EvalArraySplicePropertyBox"; +$staticName = "staticItems"; +$dynStaticRemoved = array_splice($class::${$staticName}, 0, 1, ["w"]); +echo $dynStaticRemoved[0] . ":" . EvalArraySplicePropertyBox::$staticItems[0] . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +return function_exists("array_splice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1:b:B:c:a:2:B:y:2:z:x:w:z:" + ); + assert_eq!( + values.warnings, + vec![ + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sort()` and `rsort()` reindex writable array lvalues. +#[test] +fn execute_program_dispatches_sort_builtins() { + let program = parse_fragment( + br#"$a = [3, 1, 2]; +echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; +$b = ["banana", "apple", "cherry"]; +echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; +$c = ["x" => 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +class EvalSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b", "a"]; +} +$box = new EvalSortPropertyBox(); +echo sort($box->items) . ":" . $box->items[0] . $box->items[1] . $box->items[2] . ":"; +$name = "items"; +echo rsort($box->{$name}) . ":" . $box->items[0] . ":" . $box->items[2] . ":"; +echo sort(EvalSortPropertyBox::$staticItems) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +$class = "EvalSortPropertyBox"; +$staticName = "staticItems"; +echo rsort($class::${$staticName}) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +return function_exists("sort") && function_exists("rsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:123:1:cherry:apple:123:1:312:1:1:3:1:123:1:3:1:1:ab:1:ba:" + ); + assert_eq!( + values.warnings, + vec![ + "sort(): Argument #1 ($array) must be passed by reference, value given", + "rsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval key-preserving array ordering builtins write back writable lvalue calls. +#[test] +fn execute_program_dispatches_key_preserving_sort_builtins() { + let program = parse_fragment( + br#"$a = ["x" => 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +class EvalKeySortPropertyBox { + public array $items = ["x" => 2, "y" => 1]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalKeySortPropertyBox(); +echo asort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +$name = "items"; +echo arsort($box->{$name}) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +echo ksort(EvalKeySortPropertyBox::$staticItems) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +$class = "EvalKeySortPropertyBox"; +$staticName = "staticItems"; +echo krsort($class::${$staticName}) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1:y1x2:1:x2y1:1:a2b1:1:b1a2:" + ); + assert_eq!( + values.warnings, + vec![ + "asort(): Argument #1 ($array) must be passed by reference, value given", + "krsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval natural sort builtins preserve keys and use natural string order. +#[test] +fn execute_program_dispatches_natural_sort_builtins() { + let program = parse_fragment( + br#"$a = ["img10", "img2", "img1"]; +echo natsort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +class EvalNaturalSortPropertyBox { + public array $items = ["img10", "img2", "img1"]; + public static array $staticItems = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +} +$box = new EvalNaturalSortPropertyBox(); +echo natsort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$class = "EvalNaturalSortPropertyBox"; +$staticName = "staticItems"; +echo natcasesort($class::${$staticName}) . ":"; +foreach (EvalNaturalSortPropertyBox::$staticItems as $key => $value) { echo $key . $value . ";"; } +echo ":"; +return function_exists("natsort") && function_exists("natcasesort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:" + ); + assert_eq!( + values.warnings, + vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `shuffle()` reindexes writable array lvalues. +#[test] +fn execute_program_dispatches_shuffle_builtin() { + let program = parse_fragment( + br#"$a = ["x" => 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +class EvalShufflePropertyBox { + public array $items = ["x" => 1, "y" => 2]; + public static array $staticItems = ["a" => 3, "b" => 4]; +} +$box = new EvalShufflePropertyBox(); +echo shuffle($box->items) . ":" . (isset($box->items["x"]) ? "bad" : "prop") . ":" . count($box->items) . ":" . array_sum($box->items) . ":"; +$class = "EvalShufflePropertyBox"; +$staticName = "staticItems"; +echo shuffle($class::${$staticName}) . ":" . (isset(EvalShufflePropertyBox::$staticItems["a"]) ? "bad" : "static") . ":" . count(EvalShufflePropertyBox::$staticItems) . ":" . array_sum(EvalShufflePropertyBox::$staticItems) . ":"; +return function_exists("shuffle");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:reindexed:2:3:1:12:1:prop:2:3:1:static:2:7:"); + assert_eq!( + values.warnings, + vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval user-comparator sort builtins call callbacks and write back writable lvalues. +#[test] +fn execute_program_dispatches_user_sort_builtins() { + let program = parse_fragment( + br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +function eval_sort_quiet_cmp($left, $right) { return $left <=> $right; } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +class EvalUserSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalUserSortPropertyBox(); +echo usort($box->items, "eval_sort_quiet_cmp") . ":"; +foreach ($box->items as $value) { echo $value; } +echo ":"; +$name = "items"; +echo usort($box->{$name}, "eval_sort_quiet_cmp") . ":" . $box->items[0] . $box->items[2] . ":"; +$class = "EvalUserSortPropertyBox"; +$staticName = "staticItems"; +echo uksort($class::${$staticName}, "eval_key_cmp") . ":"; +foreach (EvalUserSortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:" + ); + assert_eq!( + values.warnings, + vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs new file mode 100644 index 0000000000..f5ea27e774 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs @@ -0,0 +1,140 @@ +//! Purpose: +//! Interpreter tests for iterator-style array builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise callback iteration and object iterator dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies eval iterator array helpers support direct and dynamic builtin calls. +#[test] +fn execute_program_dispatches_iterator_array_builtins() { + let program = parse_fragment( + br#"$items = ["x" => 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +return function_exists("iterator_count") && function_exists("iterator_to_array");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:12:reindexed:12:2:12:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `iterator_apply()` drives Iterator objects and callback args. +#[test] +fn execute_program_dispatches_iterator_apply_object_builtin() { + let program = parse_fragment( + br#"function eval_apply($prefix) { echo $prefix; return true; } +echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; +echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; +return function_exists("iterator_apply");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xxx3:yyy3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `iterator_apply()` accepts object-method callable arrays. +#[test] +fn execute_program_iterator_apply_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(5); +echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; +return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} +/// Verifies eval `iterator_apply()` counts the position where the callback stops. +#[test] +fn execute_program_iterator_apply_stops_on_falsey_callback() { + let program = parse_fragment( + br#"function eval_stop() { echo "s"; return false; } +return iterator_apply($it, "eval_stop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "s"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval `array_filter()` removes falsey values while preserving original keys. +#[test] +fn execute_program_dispatches_array_filter_builtin() { + let program = parse_fragment( + br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); +echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; +$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; +return function_exists("array_filter");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs new file mode 100644 index 0000000000..0686df941f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs @@ -0,0 +1,469 @@ +//! Purpose: +//! Interpreter tests for array set, projection, shape, range, and lookup builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover array builtins that preserve or query collection structure. + +use super::super::*; +use super::support::*; + +/// Verifies eval `array_combine()` converts key values through PHP string-key rules. +#[test] +fn execute_program_dispatches_array_combine_builtin() { + let program = parse_fragment( + br#"$pairs = array_combine(["a", "b"], [10, 20]); +echo $pairs["a"] . ":" . $pairs["b"]; +$numeric = array_combine(["1", "01"], ["n", "z"]); +echo ":" . $numeric[1] . $numeric["01"]; +$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); +echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; +$named = array_combine(keys: ["k"], values: ["v"]); +echo ":" . $named["k"]; +$call = call_user_func("array_combine", ["x"], [7]); +echo ":" . $call["x"]; +$spread = call_user_func_array("array_combine", [["y"], [8]]); +echo ":" . $spread["y"] . ":"; +return function_exists("array_combine");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_column()` extracts present row columns and reindexes them. +#[test] +fn execute_program_dispatches_array_column_builtin() { + let program = parse_fragment( + br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +return function_exists("array_column");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. +#[test] +fn execute_program_dispatches_array_shape_builtins() { + let program = parse_fragment( + br#"$right = array_pad([1, 2], 5, 0); +echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; +$left = array_pad([1, 2], -4, 9); +echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; +$copy = array_pad([7, 8], 1, 0); +echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; +$chunks = array_chunk([1, 2, 3, 4, 5], 2); +echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; +$named = array_pad(array: ["a"], length: 2, value: "b"); +echo ":" . $named[1]; +$call = call_user_func("array_chunk", [6, 7, 8], 2); +echo ":" . $call[1][0]; +$spread = call_user_func_array("array_pad", [[1], 3, 2]); +echo ":" . $spread[2] . ":"; +return function_exists("array_pad") && function_exists("array_chunk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_slice()` observes PHP offset and length bounds. +#[test] +fn execute_program_dispatches_array_slice_builtin() { + let program = parse_fragment( + br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); +echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; +$tail = array_slice([10, 20, 30, 40], -2, 1); +echo ":" . $tail[0]; +$open = array_slice([10, 20, 30, 40, 50], 2); +echo ":" . count($open) . ":" . $open[0] . $open[2]; +$null_len = array_slice([5, 6, 7], 1, null); +echo ":" . $null_len[0] . $null_len[1]; +$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); +echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; +$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); +echo ":" . $named[0]; +$call = call_user_func("array_slice", [6, 7, 8], 1, 2); +echo ":" . $call[1]; +$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); +echo ":" . $spread[0] . ":"; +return function_exists("array_slice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. +#[test] +fn execute_program_dispatches_array_merge_builtin() { + let program = parse_fragment( + br#"$merged = array_merge([1, 2], [3, 4]); +echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; +$left = [1, 2]; +$right = [3]; +$copy = array_merge($left, $right); +echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; +$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +return function_exists("array_merge");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. +#[test] +fn execute_program_dispatches_array_value_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +return function_exists("array_diff") && function_exists("array_intersect");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. +#[test] +fn execute_program_dispatches_array_key_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. +#[test] +fn execute_program_dispatches_range_builtin() { + let program = parse_fragment( + br#"$up = range(1, 4); +echo count($up) . ":" . $up[0] . $up[3]; +$down = range(4, 1); +echo ":" . count($down) . ":" . $down[0] . $down[3]; +$single = range(3, 3); +echo ":" . count($single) . ":" . $single[0]; +$named = range(start: 2, end: 4); +echo ":" . $named[0] . $named[2]; +$call = call_user_func("range", 5, 7); +echo ":" . $call[2]; +$spread = call_user_func_array("range", [8, 6]); +echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; +return function_exists("range");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_rand()` returns a key that exists in the source array. +#[test] +fn execute_program_dispatches_array_rand_builtin() { + let program = parse_fragment( + br#"$nums = [10, 20, 30]; +$idx = array_rand($nums); +echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +return function_exists("array_rand");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "idx:assoc:named:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval random builtins return values inside PHP inclusive ranges. +#[test] +fn execute_program_dispatches_rand_builtins() { + let program = parse_fragment( + br#"$plain = rand(); +echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +echo function_exists("rand"); +echo function_exists("mt_rand"); +return function_exists("random_int");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. +#[test] +fn execute_program_dispatches_array_fill_builtins() { + let program = parse_fragment( + br#"$filled = array_fill(2, 3, "x"); +echo count($filled) . ":" . $filled[2] . $filled[4]; +$negative = array_fill(-2, 3, 7); +echo ":" . $negative[-2] . $negative[-1] . $negative[0]; +$empty = array_fill(5, 0, "x"); +echo ":" . count($empty); +$map = array_fill_keys(["a", "1", "01"], 8); +echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; +$named = array_fill(start_index: 1, count: 2, value: "n"); +echo ":" . $named[1] . $named[2]; +$call = call_user_func("array_fill", 0, 2, "c"); +echo ":" . $call[0] . $call[1]; +$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); +echo ":" . $spread["x"] . $spread["y"] . ":"; +return function_exists("array_fill") && function_exists("array_fill_keys");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. +#[test] +fn execute_program_dispatches_array_flip_builtin() { + let program = parse_fragment( + br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +return function_exists("array_flip");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_unique()` preserves first keys using default string comparison. +#[test] +fn execute_program_dispatches_array_unique_builtin() { + let program = parse_fragment( + br#"$unique = array_unique(["a", "b", "a", "2", 2]); +echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; +$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +return function_exists("array_unique");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval array projection builtins produce indexed key/value arrays. +#[test] +fn execute_program_dispatches_array_projection_builtins() { + let program = parse_fragment( + br#"$values = array_values(["a" => 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); +return function_exists("array_values");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:a:b:0:z:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_reverse()` handles PHP key preservation rules. +#[test] +fn execute_program_dispatches_array_reverse_builtin() { + let program = parse_fragment( + br#"$indexed = array_reverse([1, 2, 3]); +echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; +$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +return function_exists("array_reverse");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "321:cba:cba:yx:54:76:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. +#[test] +fn execute_program_dispatches_array_key_exists_builtin() { + let program = parse_fragment( + br#"$map = ["name" => null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; +return function_exists("array_key_exists");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval array search builtins use loose comparison and return keys or booleans. +#[test] +fn execute_program_dispatches_array_search_builtins() { + let program = parse_fragment( + br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; +echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; +echo ":" . array_search(20, [10, 20, 30]); +echo ":" . array_search("Grace", ["name" => "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); +return function_exists("array_search");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs new file mode 100644 index 0000000000..732a93706d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs @@ -0,0 +1,463 @@ +//! Purpose: +//! Interpreter tests for Reflection attributes, origins, sources, and prototypes. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Coverage includes eval-declared attributes and callable metadata provenance. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies class attribute helpers expose eval class-level metadata. +#[test] +fn execute_program_dispatches_class_attribute_metadata_builtins() { + let program = parse_fragment( + br#"class EvalAttrDep {} +#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class, ["nested", 2])] +#[Tag("first"), Tag("second")] +class EvalAttrMeta {} +$names = class_attribute_names("EvalAttrMeta"); +echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":"; echo $names[2]; echo ":"; +$args = class_attribute_args("EvalAttrMeta", "route"); +echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; +echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; +echo $args[5]; echo ":"; +echo count($args[6]); echo ":"; echo $args[6][0]; echo ":"; echo $args[6][1]; echo ":"; +$tag = class_attribute_args("evalattrmeta", "Tag"); +echo $tag[0]; echo ":"; +$missing = class_attribute_args("EvalAttrMeta", "Missing"); +echo count($missing); echo ":"; +$attrs = class_get_attributes("EvalAttrMeta"); +echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; +$attr_args = $attrs[0]->getArguments(); +echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; +echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; +echo $attr_args[5]; echo ":"; +echo count($attr_args[6]); echo ":"; echo $attr_args[6][0]; echo ":"; echo $attr_args[6][1]; echo ":"; +$tag_attr_args = $attrs[1]->getArguments(); +echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; +echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; +$call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); +echo $call_names[0]; echo ":"; +$call_args = call_user_func_array( + "class_attribute_args", + ["class_name" => "EvalAttrMeta", "attribute_name" => "Route"] +); +echo $call_args[0]; echo ":"; +echo function_exists("class_attribute_names"); echo function_exists("class_get_attributes"); +echo function_exists("class_attribute_args"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:0:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N:Route:/home:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval class attribute metadata preserves named literal arguments. +#[test] +fn execute_program_dispatches_named_class_attribute_args() { + let program = parse_fragment( + br#"#[Route(path: "/eval", secure: true, code: 9)] +class EvalNamedAttrMeta {} +$args = class_attribute_args("EvalNamedAttrMeta", "Route"); +echo count($args); echo ":"; +echo $args["path"]; echo ":"; +echo $args["secure"] ? "T" : "F"; echo ":"; +echo $args["code"]; echo ":"; +$attrs = class_get_attributes("EvalNamedAttrMeta"); +$attr_args = $attrs[0]->getArguments(); +echo $attr_args["path"]; echo ":"; +echo $attr_args["secure"] ? "T" : "F"; echo ":"; +echo $attr_args["code"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:/eval:T:9:/eval:T:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionAttribute::newInstance instantiates eval-declared attribute classes. +#[test] +fn execute_program_instantiates_eval_declared_reflection_attribute() { + let program = parse_fragment( + br#"class EvalRoute { + public $path; + public $code; + public $enabled; + public function __construct($path, $code, $enabled) { + $this->path = $path; + $this->code = $code; + $this->enabled = $enabled; + } + public function summary() { + return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); + } +} +#[EvalRoute(enabled: true, code: -7, path: "/home")] +class EvalRouteTarget {} +$attrs = class_get_attributes("EvalRouteTarget"); +$instance = $attrs[0]->newInstance(); +echo get_class($instance); echo ":"; echo $instance->summary(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalRoute:/home:-7:T"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass/Method/Property expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_member_attributes() { + let program = parse_fragment( + br#"class EvalMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +#[EvalMarker("class")] +class EvalReflectTarget { + #[EvalMarker("method")] + public function handle() {} + #[EvalMarker("property")] + public $id; +} +$class_attrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); +echo count($class_attrs); echo ":"; echo (new ReflectionClass("EvalReflectTarget"))->getName(); echo ":"; +echo $class_attrs[0]->getName(); echo ":"; echo $class_attrs[0]->newInstance()->label(); echo ":"; +$method_attrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); +echo count($method_attrs); echo ":"; echo (new ReflectionMethod("EvalReflectTarget", "handle"))->getName(); echo ":"; +echo $method_attrs[0]->getName(); echo ":"; +echo $method_attrs[0]->getArguments()[0]; echo ":"; echo $method_attrs[0]->newInstance()->label(); echo ":"; +$property_attrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); +echo count($property_attrs); echo ":"; echo (new ReflectionProperty("EvalReflectTarget", "id"))->getName(); echo ":"; +echo $property_attrs[0]->getName(); echo ":"; +echo $property_attrs[0]->getArguments()[0]; echo ":"; echo $property_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionAttribute reports target bitmasks and repeated-owner metadata. +#[test] +fn execute_program_reflection_attribute_reports_target_and_repetition() { + let program = parse_fragment( + br#"class EvalTargetMarker { + public function __construct($name = null) {} +} +#[EvalTargetMarker("class-a"), EvalTargetMarker("class-b")] +class EvalReflectAttributeTarget { + #[EvalTargetMarker("method")] + public function run(#[EvalTargetMarker("param")] $id) {} + #[EvalTargetMarker("property")] + public $id; + #[EvalTargetMarker("const")] + public const ANSWER = 42; +} +enum EvalReflectAttributeEnum { + #[EvalTargetMarker("case")] + case Ready; +} +$class_attrs = (new ReflectionClass("EvalReflectAttributeTarget"))->getAttributes(); +echo $class_attrs[0]->getTarget(); echo "/"; +echo $class_attrs[0]->isRepeated() ? "R" : "r"; echo ":"; +echo $class_attrs[1]->getTarget(); echo "/"; +echo $class_attrs[1]->isRepeated() ? "R" : "r"; echo ":"; +$method_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; +echo $method_attr->getTarget(); echo "/"; +echo $method_attr->isRepeated() ? "R" : "r"; echo ":"; +$property_attr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; +echo $property_attr->getTarget(); echo "/"; +echo $property_attr->isRepeated() ? "R" : "r"; echo ":"; +$param_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; +echo $param_attr->getTarget(); echo "/"; +echo $param_attr->isRepeated() ? "R" : "r"; echo ":"; +$const_attr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; +echo $const_attr->getTarget(); echo "/"; +echo $const_attr->isRepeated() ? "R" : "r"; echo ":"; +$case_attr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; +echo $case_attr->getTarget(); echo "/"; +echo $case_attr->isRepeated() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies reflection owner origin metadata APIs report eval user-defined defaults. +#[test] +fn execute_program_reflection_owners_report_origin_metadata_defaults() { + let program = parse_fragment( + br#"class EvalReflectOriginTarget { + public $id; + public const ANSWER = 42; + public function run() {} +} +enum EvalReflectOriginCase: string { + case Ready = "ready"; +} +$class = new ReflectionClass("EvalReflectOriginTarget"); +$method = new ReflectionMethod("EvalReflectOriginTarget", "run"); +$property = new ReflectionProperty("EvalReflectOriginTarget", "id"); +$constant = new ReflectionClassConstant("EvalReflectOriginTarget", "ANSWER"); +$unit = new ReflectionEnumUnitCase("EvalReflectOriginCase", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalReflectOriginCase", "Ready"); +echo ($class->getDocComment() === false) ? "C" : "c"; echo ":"; +echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; +echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; +echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; +echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; +echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; +echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($method->getExtensionName() === false) ? "N" : "n"; echo ":"; +echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; +echo ($method->getExtension() === null) ? "Y" : "y"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "C:M:P:K:U:B:E:N:X:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass and ReflectionMethod report eval source-location metadata. +#[test] +fn execute_program_reflection_class_and_method_report_source_location() { + let program = parse_fragment( + br#"class EvalReflectSource { + public function run() { + return 1; + } +} +interface EvalReflectSourceIface { + public function iface(); +} +$class = new ReflectionClass("EvalReflectSource"); +$method = new ReflectionMethod("EvalReflectSource", "run"); +$iface = new ReflectionClass("EvalReflectSourceIface"); +$ifaceMethod = new ReflectionMethod("EvalReflectSourceIface", "iface"); +echo $class->getFileName(); echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; +echo $iface->getStartLine(); echo ":"; echo $iface->getEndLine(); echo ":"; +echo $ifaceMethod->getStartLine(); echo ":"; echo $ifaceMethod->getEndLine(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/eval-class-source.php", "/tmp", 23); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "/tmp/eval-class-source.php(23) : eval()'d code:1:5:2:4:6:8:7:7" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes PHP-compatible name and origin predicate metadata. +#[test] +fn execute_program_reflection_method_reports_name_and_origin_predicates() { + let program = parse_fragment( + br#"namespace EvalReflectMethodNs; +class Target { + public function run(...$items) {} + public static function stat() {} +} +$ref = new \ReflectionMethod(Target::class, "run"); +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; echo ":"; +echo $ref->isInternal() ? "I" : "i"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $ref->returnsReference() ? "R" : "r"; echo ":"; +echo $ref->hasReturnType() ? "T" : "t"; echo ":"; +echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; +echo $ref->isGenerator() ? "G" : "g"; echo ":"; +echo $ref->isVariadic() ? "V" : "v"; echo ":"; +echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo count($ref->getClosureUsedVariables()); echo ":"; +echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; +echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; +echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; +$static = new \ReflectionMethod(Target::class, "stat"); +echo $static->isStatic() ? "S" : "s"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod derives `isDeprecated()` from eval-retained attributes. +#[test] +fn execute_program_reflection_method_reports_deprecated_attribute() { + let program = parse_fragment( + br#"class EvalReflectDeprecatedMethodTarget { + #[\Deprecated] + public function old() {} + public function fresh() {} +} +$deprecated = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "old"); +$plain = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "fresh"); +echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; +echo $plain->isDeprecated() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:d"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval static locals using the declaring class key. +#[test] +fn execute_program_reflection_method_reports_static_variables() { + let program = parse_fragment( + br#"class EvalReflectMethodStaticBase { + public function tick() { + static $count = 3; + static $label = "method"; + $count = $count + 1; + return $count; + } +} +class EvalReflectMethodStaticChild extends EvalReflectMethodStaticBase {} +$object = new EvalReflectMethodStaticChild(); +$ref = new ReflectionMethod("EvalReflectMethodStaticChild", "tick"); +$before = $ref->getStaticVariables(); +echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; +echo $ref->invoke($object); echo ":"; +$after = $ref->getStaticVariables(); +echo $after["count"]; echo ":"; echo $after["label"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:method:4:4:method"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval parent and interface prototypes. +#[test] +fn execute_program_reflection_method_reports_eval_prototypes() { + let program = parse_fragment( + br#"interface EvalProtoParentIface { + public function parented(); +} +interface EvalProtoChildIface extends EvalProtoParentIface {} +interface EvalProtoIface { + public function iface(); +} +class EvalProtoBase { + public function run() {} + public function inherited() {} +} +class EvalProtoChild extends EvalProtoBase implements EvalProtoIface, EvalProtoChildIface { + public function run() {} + public function iface() {} + public function parented() {} + public function own() {} +} +$override = new ReflectionMethod("EvalProtoChild", "run"); +$overrideProto = $override->getPrototype(); +echo $override->hasPrototype() ? "Y" : "N"; echo ":"; +echo $overrideProto->getDeclaringClass()->getName(); echo "::"; +echo $overrideProto->getName(); echo ":"; +$iface = new ReflectionMethod("EvalProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo $iface->hasPrototype() ? "Y" : "N"; echo ":"; +echo $ifaceProto->getDeclaringClass()->getName(); echo "::"; +echo $ifaceProto->getName(); echo ":"; +$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName(); echo "::"; +echo $parentIfaceProto->getName(); echo ":"; +$own = new ReflectionMethod("EvalProtoChild", "own"); +echo $own->hasPrototype() ? "Y" : "N"; echo ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs new file mode 100644 index 0000000000..b8d05ec0f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs @@ -0,0 +1,403 @@ +//! Purpose: +//! Interpreter tests for reflected callable parameters, types, and string formatting. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Method, parameter, property, and composite type metadata stay PHP-compatible. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. +#[test] +fn execute_program_reflects_eval_method_parameters() { + let program = parse_fragment( +br##"interface EvalReflectLeft {} +interface EvalReflectRight {} +class EvalReflectParamTarget { + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, ?array $items = null, ?callable $callback = null, \App\Name|null $second = null, &...$rest) {} +} +$method = new ReflectionMethod("EvalReflectParamTarget", "run"); +echo $method->getNumberOfParameters(); echo "/"; +echo $method->getNumberOfRequiredParameters(); echo ":"; +$params = $method->getParameters(); +foreach ($params as $param) { + echo $param->getName(); echo "#"; echo $param->getPosition(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->canBePassedByValue() ? "Y" : "N"; + echo $param->hasType() ? "T" : "t"; + echo $param->allowsNull() ? "N" : "n"; + echo $param->isArray() ? "A" : "a"; + echo $param->isCallable() ? "C" : "c"; + $type = $param->getType(); + if ($param->getName() == "union") { + echo ":union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($param->getName() == "both") { + echo ":intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo ":"; echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo ":null"; + } + $attrs = $param->getAttributes(); + echo ":A"; echo count($attrs); + if (count($attrs) > 0) { + echo ":"; echo $attrs[0]->getName(); + echo ":"; echo $attrs[0]->getArguments()[0]; + } + echo $param->isDefaultValueAvailable() ? ":D" : ":d"; + if ($param->isDefaultValueAvailable()) { + echo "="; + echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); + } + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7/3:first#0rvRNTnac:int!B:A1:EvalParamTag:first:d|union#1rvbYTnac:union!:intB:stringB:A0:d|both#2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items#3OvbYTNAc:array?B:A0:D=null|callback#4OvbYTNaC:callable?B:A0:D=null|second#5OvbYTNac:App\\Name?C:A0:D=null|rest#6OVRNtNac:null:A0:d|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionType objects stringify retained eval parameter metadata. +#[test] +fn execute_program_reflection_type_to_string() { + let program = parse_fragment( +br##"class EvalReflectTypeStringDep {} +interface EvalReflectTypeStringLeft {} +interface EvalReflectTypeStringRight {} +class EvalReflectTypeStringTarget { + public function run(?EvalReflectTypeStringDep $dep, int|string|null $union, EvalReflectTypeStringLeft&EvalReflectTypeStringRight $both, mixed $mixed, ?array $items) {} +} +$params = (new ReflectionMethod("EvalReflectTypeStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName(); echo ":"; + echo $type->__toString(); echo "|"; +} +$unionType = $params[1]->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter formats retained eval parameter metadata through `__toString()`. +#[test] +fn execute_program_reflection_parameter_to_string() { + let program = parse_fragment( + br#"class EvalReflectParameterStringTarget { + const LABEL = "L"; + public function run(string $name, int $count = 3, $label = self::LABEL, &...$items) {} +} +$params = (new ReflectionMethod("EvalReflectParameterStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval-declared return type metadata. +#[test] +fn execute_program_reflection_method_reports_return_type_metadata() { + let program = parse_fragment( + br#"interface EvalReflectReturnIface { + public function read(): string; +} +class EvalReflectReturnTarget implements EvalReflectReturnIface { + public function read(): string { return "ok"; } + public function selfReturn(): static { return $this; } + public function done(): void {} +} +$iface = new ReflectionMethod("EvalReflectReturnIface", "read"); +$ifaceType = $iface->getReturnType(); +echo $iface->hasReturnType() ? "I" : "i"; echo ":"; +echo $ifaceType->getName(); echo ":"; +echo $ifaceType->isBuiltin() ? "B" : "b"; echo ":"; +$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); +echo $self->getName(); echo ":"; +echo $self->isBuiltin() ? "B" : "b"; echo ":"; +$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); +echo $void->getName(); echo ":"; +echo $void->allowsNull() ? "N" : "n"; echo ":"; +echo $void->isBuiltin() ? "B" : "b"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "I:string:B:static:b:void:n:B"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod formats retained eval method metadata through `__toString()`. +#[test] +fn execute_program_reflection_method_to_string() { + let program = parse_fragment( + br#"class EvalReflectMethodStringTarget { + final public static function run(?int $id, string $label = "ok"): ?string { + return $label; + } +} +$ref = new ReflectionMethod("EvalReflectMethodStringTarget", "run"); +echo str_replace("\n", "|", $ref->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedParamTarget { + public function __construct(public int $id, string $name = "Ada") {} + public function run(int $id) {} +} +$ctorParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "__construct"))->getParameters(); +$runParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "run"))->getParameters(); +echo $ctorParams[0]->isPromoted() ? "I" : "i"; +echo $ctorParams[1]->isPromoted() ? "N" : "n"; +echo $runParams[0]->isPromoted() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Inr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property get/set type metadata. +#[test] +fn execute_program_reflection_property_get_type_metadata() { + let program = parse_fragment( + br##"class EvalReflectPropertyTypeDep {} +class EvalReflectPropertyTypeTarget { + public int $id; + public ?string $name; + public EvalReflectPropertyTypeDep $dep; + public $plain; + public int|string $union; +} +$properties = (new ReflectionClass("EvalReflectPropertyTypeTarget"))->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($property->getName() == "union") { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); +$directType = $direct->getType(); +echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; +echo $directType->getName(); +$directSettableType = $direct->getSettableType(); +echo ":set:"; echo $directSettableType->getName(); +$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); +echo ":plainSet:"; echo $plain->getSettableType() === null ? "N" : "n"; +$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); +echo ":unionSet:"; echo count($directUnion->getSettableType()->getTypes()); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty retains explicit set-hook parameter metadata as the settable type. +#[test] +fn execute_program_reflection_property_get_settable_type_uses_set_hook_parameter() { + let program = parse_fragment( + br##"class EvalReflectSettableTypeTarget { + public string $value { + get => $this->value; + set(int|string $raw) => (string) $raw; + } +} +$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); +$type = $property->getType(); +$settable = $property->getSettableType(); +echo $type->getName(); echo ":"; +echo count($settable->getTypes()); +foreach ($settable->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; +} +$setHook = $property->getHook(PropertyHookType::Set); +$paramType = $setHook->getParameters()[0]->getType(); +echo ":"; echo count($paramType->getTypes()); +$box = new EvalReflectSettableTypeTarget(); +$box->value = 7; +echo ":"; echo $box->value; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "string:2:intB:stringB:2:7"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property default metadata. +#[test] +fn execute_program_reflection_property_get_default_value_metadata() { + let program = parse_fragment( + br#"class EvalReflectPropertyDefaultTarget { + public $implicit; + public int $typed; + public ?string $nullableTyped; + public $explicitNull = null; + public int $count = 7; + public static string $label = "ok"; +} + +foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { + $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); + echo $property->getName(); echo ":"; + echo $property->isDefault() ? "Y:" : "N:"; + echo $property->hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); +echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty formats retained property metadata through `__toString()`. +#[test] +fn execute_program_reflection_property_to_string() { + let program = parse_fragment( + br#"class EvalReflectPropertyStringTarget { + public int $id = 7; + protected static string $label = "ok"; + private $implicit; + public $virtual { + get => 1; + } +} +foreach (["id", "label", "implicit", "virtual"] as $name) { + echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs new file mode 100644 index 0000000000..319d923930 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs @@ -0,0 +1,247 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass capabilities and construction metadata. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Readonly, instantiable, cloneable, iterable, and origin predicates are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass exposes eval readonly class metadata. +#[test] +fn execute_program_reflects_eval_class_readonly_predicate() { + let program = parse_fragment( + br#"class EvalReadonlyPlain {} +readonly class EvalReadonlyReflect {} +final readonly class EvalReadonlyFinalReflect {} +enum EvalReadonlyEnumReflect { case Ready; } +interface EvalReadonlyIface {} +trait EvalReadonlyTrait {} +echo (new ReflectionClass("EvalReadonlyPlain"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "rRRrrr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval class instantiability metadata. +#[test] +fn execute_program_reflects_eval_class_instantiable_predicate() { + let program = parse_fragment( + br#"abstract class EvalInstAbstract {} +class EvalInstPublic {} +final class EvalInstFinal {} +class EvalInstPrivate { private function __construct() {} } +class EvalInstProtected { protected function __construct() {} } +interface EvalInstIface {} +trait EvalInstTrait {} +enum EvalInstEnum { case Ready; } +echo (new ReflectionClass("EvalInstAbstract"))->isInstantiable() ? "A" : "a"; +echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aBCprite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isAnonymous reports false for eval-declared named class-like symbols. +#[test] +fn execute_program_reflection_class_reports_named_classes_not_anonymous() { + let program = parse_fragment( + br#"class EvalNamedAnonymousReflect {} +interface EvalNamedAnonymousIface {} +trait EvalNamedAnonymousTrait {} +enum EvalNamedAnonymousEnum { case Ready; } +echo (new ReflectionClass("EvalNamedAnonymousReflect"))->isAnonymous() ? "C" : "c"; +echo (new ReflectionClass("EvalNamedAnonymousIface"))->isAnonymous() ? "I" : "i"; +echo (new ReflectionClass("EvalNamedAnonymousTrait"))->isAnonymous() ? "T" : "t"; +echo (new ReflectionClass("EvalNamedAnonymousEnum"))->isAnonymous() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isCloneable reports eval class clone metadata. +#[test] +fn execute_program_reflects_eval_class_cloneable_predicate() { + let program = parse_fragment( + br#"abstract class EvalCloneAbstract {} +class EvalClonePlain {} +final class EvalCloneFinal {} +class EvalClonePrivate { private function __clone() {} } +class EvalCloneProtected { protected function __clone() {} } +class EvalClonePublic { public function __clone() {} } +interface EvalCloneIface {} +trait EvalCloneTrait {} +enum EvalCloneEnum { case Ready; } +echo (new ReflectionClass("EvalCloneAbstract"))->isCloneable() ? "A" : "a"; +echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aPFvrUite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isIterable reports eval Traversable-compatible class metadata. +#[test] +fn execute_program_reflects_eval_class_iterable_predicate() { + let program = parse_fragment( + br#"class EvalIterablePlain {} +abstract class EvalIterableAbstract implements Iterator {} +interface EvalIterableIface extends Iterator {} +trait EvalIterableTrait {} +enum EvalIterableEnum { case Ready; } +class EvalIterableIterator implements Iterator { + public function current() { return null; } + public function key() { return null; } + public function next() {} + public function valid() { return false; } + public function rewind() {} +} +class EvalIterableAggregate implements IteratorAggregate { + public function getIterator() { return $this; } +} +echo (new ReflectionClass("EvalIterablePlain"))->isIterable() ? "P" : "p"; +$iter = new ReflectionClass("EvalIterableIterator"); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; +echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; +echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; +echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; +echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "pIAGbfeh"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass origin predicates report eval class-like symbols as user-defined. +#[test] +fn execute_program_reflects_eval_class_origin_predicates() { + let program = parse_fragment( + br#"class EvalOriginClass {} +interface EvalOriginIface {} +trait EvalOriginTrait {} +enum EvalOriginEnum { case Ready; } +function eval_reflect_origin($name) { + $r = new ReflectionClass($name); + echo $r->isInternal() ? "I" : "i"; + echo $r->isUserDefined() ? "U" : "u"; + echo ":"; +} +eval_reflect_origin("EvalOriginClass"); +eval_reflect_origin("EvalOriginIface"); +eval_reflect_origin("EvalOriginTrait"); +eval_reflect_origin("EvalOriginEnum"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "iU:iU:iU:iU:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. +#[test] +fn execute_program_reflection_class_get_constructor() { + let program = parse_fragment( + br#"class EvalCtorBase { + public function __construct($required, $optional = 2) {} +} +class EvalCtorChild extends EvalCtorBase {} +class EvalCtorPlain {} +interface EvalCtorInterface { + public function __construct($required); +} +trait EvalCtorTrait { + public function __construct($required, $optional = null, ...$rest) {} +} +$base = (new ReflectionClass("EvalCtorBase"))->getConstructor(); +echo $base->getName(); echo "/"; +echo $base->getNumberOfParameters(); echo "/"; +echo $base->getNumberOfRequiredParameters(); echo ":"; +$child = (new ReflectionClass("EvalCtorChild"))->getConstructor(); +echo $child->getName(); echo "/"; +echo $child->getNumberOfParameters(); echo "/"; +echo $child->getNumberOfRequiredParameters(); echo ":"; +$plain = (new ReflectionClass("EvalCtorPlain"))->getConstructor(); +echo $plain === null ? "null" : "bad"; echo ":"; +$interface = (new ReflectionClass("EvalCtorInterface"))->getConstructor(); +echo $interface->getName(); echo "/"; +echo $interface->getNumberOfParameters(); echo "/"; +echo $interface->getNumberOfRequiredParameters(); echo ":"; +$trait = (new ReflectionClass("EvalCtorTrait"))->getConstructor(); +echo $trait->getName(); echo "/"; +echo $trait->getNumberOfParameters(); echo "/"; +echo $trait->getNumberOfRequiredParameters(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs new file mode 100644 index 0000000000..9906789553 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs @@ -0,0 +1,418 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass identity, relations, and modifier flags. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval and AOT class-like targets are checked through the same APIs. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass exposes eval class namespace-derived name parts. +#[test] +fn execute_program_reflects_eval_class_name_parts() { + let program = parse_fragment( + br#"namespace Eval\Ns; +class Thing {} +$ref = new \ReflectionClass("Eval\\Ns\\Thing"); +echo $ref->getName(); echo ":"; +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval interface and trait relation names. +#[test] +fn execute_program_reflects_eval_class_relation_names() { + let program = parse_fragment( + br#"interface EvalRelationIface {} +trait EvalRelationTrait { + public function primary() {} +} +trait EvalRelationOtherTrait { + public function other() {} +} +class EvalRelationTarget implements EvalRelationIface { + use EvalRelationTrait, EvalRelationOtherTrait { + EvalRelationTrait::primary as relationAlias; + EvalRelationOtherTrait::other as private hiddenOther; + EvalRelationOtherTrait::other as protected; + } +} +class EvalRelationInherited extends EvalRelationTarget {} +interface EvalRelationParent {} +interface EvalRelationChild extends EvalRelationParent {} +$ref = new ReflectionClass("EvalRelationTarget"); +$interfaces = $ref->getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; +echo count($traits); echo ":"; echo $traits[0]; echo ":"; echo $traits[1]; echo ":"; +$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); +echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; +$interfaceObjects = $ref->getInterfaces(); +echo ":"; echo count($interfaceObjects); echo ":"; echo $interfaceObjects["EvalRelationIface"]->getName(); +$traitObjects = $ref->getTraits(); +echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); echo ":"; echo $traitObjects["EvalRelationOtherTrait"]->getName(); +$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); +echo ":"; echo count($parentInterfaceObjects); echo ":"; echo $parentInterfaceObjects["EvalRelationParent"]->getName(); +$aliases = $ref->getTraitAliases(); +echo ":"; echo count($aliases); echo ":"; echo $aliases["relationAlias"]; echo ":"; echo $aliases["hiddenOther"]; +$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); +echo ":"; echo count($inheritedAliases); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass relation-name helpers read fake generated/AOT metadata. +#[test] +fn execute_program_reflects_aot_class_relation_names() { + let program = parse_fragment( + br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); +echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; +$interface_names = (new ReflectionClass("KnownInterface"))->getInterfaceNames(); +echo count($interface_names); echo ":"; echo $interface_names[0]; echo ":"; +$class_objects = (new ReflectionClass("KnownClass"))->getInterfaces(); +echo count($class_objects); echo ":"; echo $class_objects["KnownInterface"]->getName(); echo ":"; +$interface_objects = (new ReflectionClass("KnownInterface"))->getInterfaces(); +echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); echo ":"; +$trait_names = (new ReflectionClass("KnownClass"))->getTraitNames(); +echo count($trait_names); echo ":"; echo $trait_names[0]; echo ":"; +$trait_objects = (new ReflectionClass("KnownClass"))->getTraits(); +echo count($trait_objects); echo ":"; echo $trait_objects["KnownTrait"]->getName(); echo ":"; +$nested_trait_names = (new ReflectionClass("KnownTrait"))->getTraitNames(); +echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; echo ":"; +$aliases = (new ReflectionClass("KnownClass"))->getTraitAliases(); +echo count($aliases); echo ":"; echo $aliases["knownAlias"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait:1:KnownTrait::source" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface reports eval class, enum, and +/// interface metadata using case-insensitive interface names. +#[test] +fn execute_program_reflects_eval_class_implements_interface_predicate() { + let program = parse_fragment( + br#"interface EvalImplBase {} +interface EvalImplChild extends EvalImplBase {} +class EvalImplTarget implements EvalImplChild {} +enum EvalImplEnum implements EvalImplBase { case Ready; } +trait EvalImplTrait {} +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("EvalImplChild") ? "C" : "c"; +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; +echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; +echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBEIPt"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface checks fake generated/AOT relations. +#[test] +fn execute_program_reflects_aot_class_implements_interface_predicate() { + let program = parse_fragment( + br#"$ref = new ReflectionClass("KnownClass"); +echo $ref->implementsInterface("KnownInterface") ? "Y" : "N"; echo ":"; +echo $ref->implementsInterface("Iterator") ? "bad" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface rejects non-interface names with catchable errors. +#[test] +fn execute_program_reflection_class_implements_interface_rejects_non_interfaces() { + let program = parse_fragment( + br#"interface EvalImplRejectIface {} +class EvalImplRejectTarget {} +class EvalImplRejectClass {} +trait EvalImplRejectTrait {} +enum EvalImplRejectEnum { case Ready; } +$ref = new ReflectionClass("EvalImplRejectTarget"); +echo $ref->implementsInterface("EvalImplRejectIface") ? "T" : "F"; +try { + $ref->implementsInterface("EvalImplRejectClass"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectTrait"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectEnum"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isSubclassOf reports eval parent/interface metadata. +#[test] +fn execute_program_reflection_class_is_subclass_of_predicate() { + let program = parse_fragment( + br#"interface EvalSubclassIface {} +interface EvalSubclassChildIface extends EvalSubclassIface {} +class EvalSubclassBase {} +class EvalSubclassParent extends EvalSubclassBase {} +class EvalSubclassChild extends EvalSubclassParent implements EvalSubclassChildIface {} +trait EvalSubclassTrait {} +enum EvalSubclassEnum implements EvalSubclassIface { case Ready; } +$ref = new ReflectionClass("EvalSubclassChild"); +echo $ref->isSubclassOf("EvalSubclassParent") ? "P" : "p"; +echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; +echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; +echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; +echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; +echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; +echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; +echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; +try { + $ref->isSubclassOf("EvalSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "PBIsJxtqE:missing"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isInstance reports eval object class/interface metadata. +#[test] +fn execute_program_reflection_class_is_instance_predicate() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +trait EvalInstanceTrait {} +enum EvalInstanceEnum implements EvalInstanceIface { case Ready; } +$base = new ReflectionClass("EvalInstanceBase"); +$child = new ReflectionClass("EvalInstanceChild"); +$iface = new ReflectionClass("EvalInstanceIface"); +$trait = new ReflectionClass("EvalInstanceTrait"); +$enum = new ReflectionClass("EvalInstanceEnum"); +$childObj = new EvalInstanceChild(); +$objectRef = new ReflectionClass($childObj); +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName(); echo ":"; +echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; +echo $base->isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval class-like final and abstract flags. +#[test] +fn execute_program_reflects_eval_class_modifier_flags() { + let program = parse_fragment( + br#"abstract class EvalAbstractReflect {} +final class EvalFinalReflect {} +interface EvalIfaceReflect {} +trait EvalTraitReflect {} +enum EvalEnumReflect { case Ready; } +echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Afite:aFite:aFitE:afIte:afiTe"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes PHP modifier bitmasks for eval class-like metadata. +#[test] +fn execute_program_reflects_eval_class_modifier_bitmask() { + let program = parse_fragment( + br#"abstract class EvalModifierAbstract {} +final class EvalModifierFinal {} +readonly class EvalModifierReadonly {} +final readonly class EvalModifierFinalReadonly {} +enum EvalModifierEnum { case Ready; } +interface EvalModifierIface {} +trait EvalModifierTrait {} +echo (new ReflectionClass("EvalModifierAbstract"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinal"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierEnum"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierIface"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierTrait"))->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "64:32:65536:65568:32:0:0"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval can read built-in Reflection `IS_*` class constants. +#[test] +fn execute_program_reads_builtin_reflection_modifier_constants() { + let program = parse_fragment( + br#"echo ReflectionClass::IS_FINAL; echo ":"; +echo ReflectionClass::IS_EXPLICIT_ABSTRACT; echo ":"; +echo ReflectionClass::IS_READONLY; echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; +echo ReflectionMethod::IS_PRIVATE; echo ":"; +echo ReflectionMethod::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_STATIC; echo ":"; +echo ReflectionProperty::IS_READONLY; echo ":"; +echo ReflectionProperty::IS_PUBLIC; echo ":"; +echo ReflectionProperty::IS_PROTECTED; echo ":"; +echo ReflectionProperty::IS_PRIVATE; echo ":"; +echo ReflectionProperty::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_PROTECTED_SET; echo ":"; +echo ReflectionProperty::IS_PRIVATE_SET; echo ":"; +echo ReflectionProperty::IS_VIRTUAL; echo ":"; +echo ReflectionProperty::IS_FINAL; echo ":"; +echo ReflectionClassConstant::IS_PUBLIC; echo ":"; +echo ReflectionClassConstant::IS_PROTECTED; echo ":"; +echo ReflectionClassConstant::IS_PRIVATE; echo ":"; +echo ReflectionClassConstant::IS_FINAL; echo ":"; +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32:1:2:4:32:1:2:4:32" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs new file mode 100644 index 0000000000..a8999e6295 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass member lists, instantiation, and invocation. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Object allocation and reflected method calls retain PHP visibility checks. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. +#[test] +fn execute_program_reflection_class_lists_eval_method_parameters() { + let program = parse_fragment( + br#"class EvalReflectListedParamTarget { + public function first($left) {} + public function second($right, $tail) {} +} +$methods = (new ReflectionClass("EvalReflectListedParamTarget"))->getMethods(); +foreach ($methods as $method) { + $params = $method->getParameters(); + echo $method->getName(); echo ":"; + echo $method->getNumberOfParameters(); echo "/"; + echo $method->getNumberOfRequiredParameters(); + if (count($params) > 0) { + echo ":"; echo $params[0]->getName(); echo ":"; echo $params[0]->getPosition(); + } + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:1/1:left:0|second:2/2:right:0|"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass getMethods/getProperties return eval member objects. +#[test] +fn execute_program_reflection_class_lists_eval_member_objects() { + let program = parse_fragment( + br#"#[Attribute] +class EvalListMarker {} +class EvalReflectListTarget { + #[EvalListMarker] + public function first() {} + private static function helper() {} + #[EvalListMarker] + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectListTarget"); +$methods = $ref->getMethods(); +$properties = $ref->getProperties(); +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +$nullMethods = $ref->getMethods(null); +$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); +$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); +$noProperties = $ref->getProperties(0); +echo count($methods); echo ":"; echo count($properties); echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; +$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); +echo "D"; echo $direct->getModifiers(); echo ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F"; echo count($method->getAttributes()); + echo "M"; echo $method->getModifiers(); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + echo "M"; echo $method->getModifiers(); + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V"; echo count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + echo "M"; echo $property->getModifiers(); + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + echo "M"; echo $property->getModifiers(); + } +} +echo ":"; +echo count($staticMethods); echo $staticMethods[0]->getName(); echo ":"; +echo count($privateMethods); echo $privateMethods[0]->getName(); echo ":"; +echo count($noMethods); echo ":"; echo count($nullMethods); echo ":"; +echo count($staticProperties); echo $staticProperties[0]->getName(); echo ":"; +echo count($protectedProperties); echo $protectedProperties[0]->getName(); echo ":"; +echo count($noProperties); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass getMethod/getProperty return eval member objects. +#[test] +fn execute_program_reflection_class_gets_eval_member_objects() { + let program = parse_fragment( + br#"class EvalReflectLookupTarget { + public function first() {} + private static function helper() {} + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectLookupTarget"); +$method = $ref->getMethod("FIRST"); +echo $method->getName(); echo ":"; +echo $method->isPublic() ? "U" : "u"; echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName(); echo ":"; +echo $property->isProtected() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:U:PS:visible:R"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::getParentClass returns eval parent metadata or false. +#[test] +fn execute_program_reflection_class_get_parent_class() { + let program = parse_fragment( + br#"class EvalReflectParentBase {} +class EvalReflectParentChild extends EvalReflectParentBase {} +$parent = (new ReflectionClass("EvalReflectParentChild"))->getParentClass(); +echo $parent->getName(); +echo ":"; +$root = (new ReflectionClass("EvalReflectParentBase"))->getParentClass(); +if ($root === false) { + echo "false"; +} else { + echo "bad"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectParentBase:false"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstance constructs eval-declared classes. +#[test] +fn execute_program_reflection_class_new_instance_constructs_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNewTarget { + public $label; + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function label() { + return $this->label; + } +} +$ref = new ReflectionClass("EvalReflectNewTarget"); +$first = $ref->newInstance("I", "J"); +echo $first->label(); echo ":"; +$second = $ref->newInstance(...["K", "L"]); +echo $second->label(); echo ":"; +$third = $ref->newInstanceArgs(["right" => "N", "left" => "M"]); +echo $third->label(); echo ":"; +$fourth = $ref->newInstanceArgs(["O", "P"]); +echo $fourth->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "IJ:KL:MN:OP"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstance throws for non-public eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_rejects_non_public_eval_constructors() { + let program = parse_fragment( + br#"class EvalReflectNewPrivateCtor { + private function __construct() {} +} +class EvalReflectNewProtectedCtor { + protected function __construct() {} +} +try { + (new ReflectionClass("EvalReflectNewPrivateCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass instantiation throws Error for eval non-instantiable class-likes. +#[test] +fn execute_program_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { + let program = parse_fragment( + br#"abstract class EvalReflectNewAbstract {} +interface EvalReflectNewIface {} +trait EvalReflectNewTrait {} +enum EvalReflectNewEnum { case Ready; } +function eval_reflect_new_error($class, $without) { + try { + $ref = new ReflectionClass($class); + if ($without) { + $ref->newInstanceWithoutConstructor(); + } else { + $ref->newInstance(); + } + echo "bad"; + } catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); + } +} +eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; +eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", true); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::invoke dispatches eval-declared methods. +#[test] +fn execute_program_reflection_method_invoke_calls_eval_method() { + let program = parse_fragment( + br#"class EvalReflectInvokeBase { + private function hidden($label = "H") { + return "hidden:" . $label; + } + public function who() { + return static::class; + } + public static function make($left, $right = "S") { + return static::class . ":" . $left . $right; + } +} +class EvalReflectInvokeChild extends EvalReflectInvokeBase { + public function join($a, $b = "B") { + return $a . $b; + } + public function mutate(&$value) { + $value = $value . "!"; + return $value; + } +} +$object = new EvalReflectInvokeChild(); +$hidden = new ReflectionMethod("EvalReflectInvokeBase", "hidden"); +echo $hidden->invoke($object, "X"); echo ":"; +$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); +echo $who->invoke($object); echo ":"; +$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X"); echo ":"; +echo $static->invoke($object, "A"); echo ":"; +$join = null; +foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { + if ($method->getName() === "join") { + $join = $method; + } +} +$value = "Q"; +$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); +echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]); echo ":"; +echo $mutate->invoke($object, $value); echo ":"; echo $value; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::invoke throws for incompatible eval receivers. +#[test] +fn execute_program_reflection_method_invoke_rejects_wrong_object() { + let program = parse_fragment( + br#"class EvalReflectInvokeOwner { + public function run() { + return "owner"; + } +} +class EvalReflectInvokeOther {} +try { + (new ReflectionMethod("EvalReflectInvokeOwner", "run"))->invoke(new EvalReflectInvokeOther()); + echo "bad"; +} catch (ReflectionException $e) { + echo "caught"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "caught"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. +#[test] +fn execute_program_reflection_set_accessible_is_noop() { + let program = parse_fragment( + br#"class EvalReflectAccessTarget { + private $secret = "s"; + private function hidden() { + return $this->secret; + } +} +$object = new EvalReflectAccessTarget(); +$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); +echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; +echo $method->invoke($object); echo ":"; +$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); +echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; +echo $property->getValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "M:s:P:s"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNoCtorTarget { + public $label = "default"; + private $secret = "hidden"; + public function __construct() { + $this->label = "ctor"; + } + public function label() { + return $this->label; + } + public function secret() { + return $this->secret; + } +} +$ref = new ReflectionClass("EvalReflectNoCtorTarget"); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label(); echo ":"; +echo $without->secret(); echo ":"; +$with = $ref->newInstance(); +echo $with->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "default:hidden:ctor"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs new file mode 100644 index 0000000000..7628955d0f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs @@ -0,0 +1,432 @@ +//! Purpose: +//! Interpreter tests for class constants, enum cases, and ReflectionObject. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Enum ownership, constant types, dynamic properties, and constructor errors are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_constant_and_enum_case_attributes() { + let program = parse_fragment( + br#"class EvalConstMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +class EvalConstReflectTarget { + #[EvalConstMarker("const")] + final public const ANSWER = 42; +} +enum EvalCaseReflectTarget: string { + #[EvalConstMarker("case")] + case Ready = "ready"; +} +$const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); +echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getValue(); echo ":"; +echo ((new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "E" : "e"; echo ":"; +echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; +echo $const_attrs[0]->newInstance()->label(); echo ":"; +$case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo count($case_attrs); echo ":"; echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo $case_attrs[0]->getName(); echo ":"; echo $case_attrs[0]->getArguments()[0]; echo ":"; +$unit_attrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; +echo $unit_attrs[0]->newInstance()->label(); echo ":"; +$backed_attrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue(); echo ":"; +echo $backed_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:ANSWER:F:plain:42:E:enum:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant and enum case metadata expose PHP's untyped defaults. +#[test] +fn execute_program_reflects_eval_constant_type_metadata_defaults() { + let program = parse_fragment( + br#"class EvalConstTypeTarget { + public const ANSWER = 42; +} +enum EvalConstTypeEnum: string { + case Ready = "ready"; +} +$constant = new ReflectionClassConstant("EvalConstTypeTarget", "ANSWER"); +echo $constant->isDeprecated() ? "D" : "d"; echo ":"; +echo $constant->hasType() ? "T" : "t"; echo ":"; +echo $constant->getType() === null ? "N" : "n"; echo ":"; +$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); +echo $case->isDeprecated() ? "D" : "d"; echo ":"; +echo $case->hasType() ? "T" : "t"; echo ":"; +echo $case->getType() === null ? "N" : "n"; echo ":"; +$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); +echo $unit->isDeprecated() ? "D" : "d"; echo ":"; +echo $unit->hasType() ? "T" : "t"; echo ":"; +echo $unit->getType() === null ? "N" : "n"; echo ":"; +$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); +echo $backed->isDeprecated() ? "D" : "d"; echo ":"; +echo $backed->hasType() ? "T" : "t"; echo ":"; +echo $backed->getType() === null ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "d:t:N:d:t:N:d:t:N:d:t:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant and enum case objects stringify retained metadata. +#[test] +fn execute_program_reflects_eval_constant_to_string() { + let program = parse_fragment( + br#"class EvalConstStringTarget { + public const ANSWER = 42; + final protected const LIMIT = 7; + private const FLAG = true; + public const LABEL = "ok"; + public const NOTHING = null; +} +enum EvalConstStringEnum: string { + case Ready = "ready"; +} +foreach (["ANSWER", "LIMIT", "FLAG", "LABEL", "NOTHING"] as $name) { + echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringTarget", $name))->__toString()); + echo "|"; +} +echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Constant [ public int ANSWER ] { 42 }\\n|Constant [ final protected int LIMIT ] { 7 }\\n|Constant [ private bool FLAG ] { 1 }\\n|Constant [ public string LABEL ] { ok }\\n|Constant [ public null NOTHING ] { }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies enum-case reflection owners expose inherited constant metadata predicates. +#[test] +fn execute_program_reflects_enum_case_visibility_and_modifiers() { + let program = parse_fragment( + br#"enum EvalEnumCaseVisibility: string { + case Ready = "ready"; +} +$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); +foreach ([$unit, $backed] as $case) { + echo $case->isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers(); echo ":"; +} +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ErpUf1:ErpUf1:1:2:4:32:1:2:4:32"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnum exposes eval-declared enum cases and backing metadata. +#[test] +fn execute_program_reflects_eval_enum_owner_metadata() { + let program = parse_fragment( + br#"enum EvalReflectPure { + case Ready; + case Done; +} +enum EvalReflectBacked: string { + case Ready = "ready"; + case Done = "done"; +} +$pure = new ReflectionEnum("EvalReflectPure"); +echo $pure->getName(); echo ":"; +echo $pure->isEnum() ? "E" : "e"; echo ":"; +echo $pure->isBacked() ? "B" : "b"; echo ":"; +echo $pure->getBackingType() === null ? "N" : "n"; echo ":"; +echo $pure->hasCase("Ready") ? "R" : "r"; +echo $pure->hasCase("Missing") ? "M" : "m"; echo ":"; +$case = $pure->getCase("Done"); +echo $case->getName(); echo ":"; +echo $case->getEnum()->getName(); echo ":"; +$cases = $pure->getCases(); +echo count($cases); echo ":"; +echo $cases[0]->getName(); echo ":"; +echo $cases[1]->getEnum()->getName(); echo ":"; +$backed = new ReflectionEnum("EvalReflectBacked"); +$type = $backed->getBackingType(); +echo $backed->isBacked() ? "B" : "b"; echo ":"; +echo $type->getName(); echo ":"; +echo $type->isBuiltin() ? "I" : "i"; echo ":"; +$backed_case = $backed->getCase("Ready"); +echo $backed_case->getName(); echo ":"; +echo $backed_case->getBackingValue(); echo ":"; +echo $backed_case->getEnum()->isBacked() ? "E" : "e"; echo ":"; +$backed_cases = $backed->getCases(); +echo count($backed_cases); echo ":"; +echo $backed_cases[1]->getBackingValue(); echo ":"; +echo $backed_cases[0]->getEnum()->getBackingType()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalReflectPure:E:b:N:Rm:Done:EvalReflectPure:2:Ready:EvalReflectPure:B:string:I:Ready:ready:E:2:done:string" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnum construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_enum_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectNotEnumClass {} +interface EvalReflectNotEnumIface {} +trait EvalReflectNotEnumTrait {} +enum EvalReflectActualEnum { + case Ready; +} +try { + new ReflectionEnum("EvalReflectNotEnumClass"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumIface"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumTrait"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectMissingEnum"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnum("EvalReflectActualEnum"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Class \"EvalReflectNotEnumClass\" is not an enum|Class \"EvalReflectNotEnumIface\" is not an enum|Class \"EvalReflectNotEnumTrait\" is not an enum|Class \"EvalReflectMissingEnum\" does not exist|EvalReflectActualEnum" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject reflects the runtime class of an eval object instance. +#[test] +fn execute_program_reflection_object_reflects_eval_instances() { + let program = parse_fragment( + br#"class EvalReflectObjectBase { + public function inherited(): string { + return "base"; + } +} +class EvalReflectObjectChild extends EvalReflectObjectBase { + public int $count = 3; +} +$ref = new ReflectionObject(new EvalReflectObjectChild()); +echo get_class($ref); echo ":"; +echo ($ref instanceof ReflectionObject) ? "O" : "o"; +echo ($ref instanceof ReflectionClass) ? "C" : "c"; echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->getParentClass()->getName(); echo ":"; +echo $ref->hasMethod("inherited") ? "M" : "m"; +echo $ref->hasProperty("count") ? "P" : "p"; echo ":"; +$object = $ref->newInstanceWithoutConstructor(); +echo get_class($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionObject:OC:EvalReflectObjectChild:EvalReflectObjectBase:MP:EvalReflectObjectChild" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject lists dynamic public properties from the reflected instance. +#[test] +fn execute_program_reflection_object_lists_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectObjectDynamicTarget { + public $declared = "declared"; +} +$object = new EvalReflectObjectDynamicTarget(); +$object->dynamic = "value"; +$ref = new ReflectionObject($object); +$properties = $ref->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->isDynamic() ? "D" : "d"; echo "|"; +} +echo ":"; +$dynamic = $ref->getProperty("dynamic"); +echo $dynamic->isDynamic() ? "D" : "d"; echo ":"; +echo $dynamic->getValue($object); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); echo ":"; +echo $ref->hasProperty("dynamic") ? "H" : "h"; +echo $ref->hasProperty("declared") ? "D" : "d"; +echo $ref->hasProperty("missing") ? "M" : "m"; +echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0:HDmc"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject constructor type errors are catchable. +#[test] +fn execute_program_reflection_object_constructor_throws_type_errors() { + let program = parse_fragment( + br#"try { + new ReflectionObject("EvalReflectObjectChild"); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionObject([]); + echo "bad"; +} catch (TypeError $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies unsupported attribute argument metadata remains name-visible but not materializable. +#[test] +fn execute_program_rejects_unsupported_class_attribute_args_metadata() { + for source in [ + br#"#[Tag($dynamic)] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"# as &[u8], + br#"#[Tag(["fixed" => "ok", $dynamic => "bad"])] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"#, + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unsupported attribute metadata should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.output, "1:Tag:"); + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs new file mode 100644 index 0000000000..a7b32fdf4c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs @@ -0,0 +1,418 @@ +//! Purpose: +//! Interpreter tests for Reflection member constructors and constructor failures. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constructor target normalization and PHP-visible exceptions are asserted together. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionMethod preserves declared method case after case-insensitive lookup. +#[test] +fn execute_program_reflection_method_preserves_declared_name_case() { + let program = parse_fragment( + br#"class EvalReflectMethodCaseBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodCaseChild extends EvalReflectMethodCaseBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodCaseChild(); +$direct = new ReflectionMethod("EvalReflectMethodCaseChild", "mixedcase"); +echo $direct->getName(); echo ":"; +echo $direct->getShortName(); echo ":"; +echo $direct->invoke($object); echo ":"; +$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); +echo $listed->getName(); echo ":"; +echo $listed->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MiXeDCase:MiXeDCase:base:childCase:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod accepts object targets and reflects the runtime class. +#[test] +fn execute_program_reflection_method_accepts_object_targets() { + let program = parse_fragment( + br#"class EvalReflectMethodObjectBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodObjectChild extends EvalReflectMethodObjectBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodObjectChild(); +$inherited = new ReflectionMethod($object, "mixedcase"); +echo $inherited->getName(); echo ":"; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->invoke($object); echo ":"; +$own = new ReflectionMethod($object, "CHILDCASE"); +echo $own->getName(); echo ":"; +echo $own->getDeclaringClass()->getName(); echo ":"; +echo $own->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::createFromMethodName resolves eval method strings. +#[test] +fn execute_program_reflection_method_create_from_method_name() { + let program = parse_fragment( + br#"class EvalReflectCreateMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = ReflectionMethod::createFromMethodName("EvalReflectCreateMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCreateMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCreateMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod accepts PHP's deprecated one-string method target. +#[test] +fn execute_program_reflection_method_accepts_single_method_string() { + let program = parse_fragment( + br#"class EvalReflectCtorMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCtorMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCtorMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_method_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingMethodTarget {} +try { + new ReflectionMethod("EvalReflectMissingMethodTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingClass", "run"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("not-a-method"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Class \"EvalReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_property_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingPropertyTarget {} +$object = new EvalReflectMissingPropertyTarget(); +$object->dynamic = 1; +try { + new ReflectionProperty("EvalReflectMissingPropertyTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty("EvalReflectMissingPropertyClass", "value"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty($object, "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Property EvalReflectMissingPropertyTarget::$missing does not exist|Class \"EvalReflectMissingPropertyClass\" does not exist|Property EvalReflectMissingPropertyTarget::$missing does not exist|dynamic" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_class_constant_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingConstantTarget { + public const OK = 1; +} +try { + new ReflectionClassConstant("EvalReflectMissingConstantTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionClassConstant("EvalReflectMissingConstantClass", "VALUE"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionClassConstant("EvalReflectMissingConstantTarget", "OK"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingConstantTarget::missing does not exist|Class \"EvalReflectMissingConstantClass\" does not exist|OK" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnumUnitCase/BackedCase construction throws PHP reflection errors. +#[test] +fn execute_program_reflection_enum_case_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"enum EvalReflectMissingCaseUnit { + case Ready; + public const TOKEN = 1; +} +enum EvalReflectMissingCaseBacked: string { + case Ready = "ready"; + public const TOKEN = 1; +} +class EvalReflectMissingCaseClass { + public const TOKEN = 1; +} +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseClass", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseUnit", "Ready"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseClass", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnumUnitCase("EvalReflectMissingCaseBacked", "Ready"))->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Ready"))->getBackingValue(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingCaseUnit::Missing does not exist|Constant EvalReflectMissingCaseClass::TOKEN is not a case|Constant EvalReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalReflectMissingCaseUnit::Ready is not a backed case|Constant EvalReflectMissingCaseBacked::Missing does not exist|Constant EvalReflectMissingCaseClass::Missing does not exist|Ready:ready" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies eval member and enum-case reflectors expose their declaring class. +#[test] +fn execute_program_reflects_eval_declaring_class_metadata() { + let program = parse_fragment( + br#"class EvalDeclaringBase { + public $baseProp = 1; + public function inherited() { return "base"; } + public const BASE_CONST = 10; +} +class EvalDeclaringChild extends EvalDeclaringBase { + public $childProp = 2; + public function own() { return "child"; } + public const CHILD_CONST = 20; +} +enum EvalDeclaringEnum: string { + case Ready = "ready"; + public const LEVEL = 3; +} +echo (new ReflectionMethod("EvalDeclaringChild", "inherited"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass stringifies retained eval class metadata. +#[test] +fn execute_program_reflection_class_to_string() { + let program = parse_fragment( + br#"class EvalReflectClassStringTarget { + public const ANSWER = 42; + public int $id = 7; + public function read(string $name = "Ada"): ?string { return $name; } +} +$ref = new ReflectionClass("EvalReflectClassStringTarget"); +echo $ref; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Class [ class EvalReflectClassStringTarget ] {\n - Constants [1] {\n Constant [ public int ANSWER ] { 42 }\n }\n - Properties [1] {\n Property [ public int $id = 7 ]\n }\n - Methods [1] {\n Method [ public method read ]\n }\n}\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs new file mode 100644 index 0000000000..ac1bf4bb41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs @@ -0,0 +1,453 @@ +//! Purpose: +//! Interpreter tests for reflected member discovery, flags, and declaring metadata. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Methods, properties, class constants, and enum cases share this metadata layer. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass reports eval class-like method, property, and constant membership. +#[test] +fn execute_program_reflects_eval_class_member_existence() { + let program = parse_fragment( + br#"class EvalMemberParent { + const PARENT_CONST = 1; + private function hiddenParent() {} + protected static function parentStatic() {} + private $hiddenProp; + protected static $parentStaticProp; +} +interface EvalMemberClassIface { + const CLASS_LIMIT = 10; +} +class EvalMemberChild extends EvalMemberParent implements EvalMemberClassIface { + const CHILD_CONST = 2; + public function ChildMethod() {} + public $childProp; +} +interface EvalMemberIfaceParent { + const PARENT_LIMIT = 10; + public function parentRequirement(); +} +interface EvalMemberIface extends EvalMemberIfaceParent { + const CHILD_LIMIT = 20; + public function childRequirement(); + public string $hook { get; } +} +trait EvalMemberTrait { + const TRAIT_CONST = 30; + private function traitHidden() {} + public $traitProp; +} +enum EvalMemberPureEnum { + case Ready; + const LEVEL = 40; + public function label() { return "ok"; } +} +enum EvalMemberBackedEnum: string { + case Ready = "ready"; +} +$child = new ReflectionClass("EvalMemberChild"); +echo $child->hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; +echo ":"; +$iface = new ReflectionClass("EvalMemberIface"); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; +echo ":"; +$trait = new ReflectionClass("EvalMemberTrait"); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; +echo ":"; +$pure = new ReflectionClass("EvalMemberPureEnum"); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; +echo ":"; +$backed = new ReflectionClass("EvalMemberBackedEnum"); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass returns eval class-like constant values and enum cases. +#[test] +fn execute_program_reflects_eval_class_constant_values() { + let program = parse_fragment( + br#"class EvalReflectConstBase { + public const BASE = 1; +} +interface EvalReflectConstIface { + public const LIMIT = 2; +} +trait EvalReflectConstTrait { + public const TRAIT_VALUE = 3; +} +class EvalReflectConstChild extends EvalReflectConstBase implements EvalReflectConstIface { + private const SECRET = 9; + public const OWN = "own"; + public const SUM = 5; +} +enum EvalReflectConstEnum { + case Ready; + public const LEVEL = 40; +} +$ref = new ReflectionClass("EvalReflectConstChild"); +$all = $ref->getConstants(); +$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); +$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); +$none = $ref->getConstants(0); +$null = $ref->getConstants(null); +echo $ref->getConstant("OWN"); echo ":"; +echo $ref->getConstant("BASE"); echo ":"; +echo $ref->getConstant("LIMIT"); echo ":"; +echo $ref->getConstant("SECRET"); echo ":"; +echo $ref->getConstant("SUM"); echo ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":"; echo count($all); echo ":"; echo $all["OWN"]; echo ":"; echo $all["BASE"]; echo ":"; echo $all["LIMIT"]; +echo ":"; echo count($public); echo ":"; echo $public["OWN"]; echo ":"; echo $public["BASE"]; +echo ":"; echo count($private); echo ":"; echo $private["SECRET"]; +echo ":"; echo count($none); echo ":"; echo count($null); +$trait = new ReflectionClass("EvalReflectConstTrait"); +$traitAll = $trait->getConstants(); +echo ":"; echo $trait->getConstant("TRAIT_VALUE"); echo ":"; echo count($traitAll); echo ":"; echo $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass("EvalReflectConstEnum"); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":"; echo $case->name; +echo ":"; echo $enum->getConstant("LEVEL"); echo ":"; echo $enumAll["LEVEL"]; echo ":"; echo count($enumAll); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass returns eval class-constant reflector objects. +#[test] +fn execute_program_reflects_eval_class_constant_reflector_objects() { + let program = parse_fragment( + br#"class EvalReflectConstMarker { + public $label; + public function __construct($label) { + $this->label = $label; + } + public function label() { + return $this->label; + } +} +class EvalReflectConstObjectTarget { + #[EvalReflectConstMarker("const")] + final public const ANSWER = 42; +} +enum EvalReflectConstObjectEnum { + #[EvalReflectConstMarker("case")] + case Ready; + final public const LEVEL = 7; +} +$ref = new ReflectionClass("EvalReflectConstObjectTarget"); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); +echo $single->getName(); echo ":"; +echo count($all); echo ":"; echo $all[0]->getName(); echo ":"; +echo $single->getAttributes()[0]->newInstance()->label(); echo ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +echo ":"; echo count($public); echo ":"; echo $public[0]->getName(); +echo ":"; echo count($final); echo ":"; echo $final[0]->getName(); +$enum = new ReflectionClass("EvalReflectConstObjectEnum"); +$enumAll = $enum->getReflectionConstants(); +$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":"; echo count($enumAll); echo ":"; echo $enumAll[0]->getName(); echo ":"; echo $enumAll[1]->getName(); +echo ":"; echo $case->getAttributes()[0]->newInstance()->label(); echo ":"; +echo count($level->getAttributes()); echo ":"; echo count($enumFinal); echo ":"; echo $enumFinal[0]->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ANSWER:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:1:LEVEL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. +#[test] +fn execute_program_reflects_eval_member_predicates() { + let program = parse_fragment( + br#"abstract class EvalReflectMemberBase { + protected static function baseStatic() {} + abstract protected function mustImplement(); + final public function locked() {} +} +readonly class EvalReflectReadonlyClass { + public int $classReadonly; +} +abstract class EvalReflectAbstractProperty { + abstract public int $mustRead { get; } +} +class EvalReflectMemberChild extends EvalReflectMemberBase { + public function mustImplement() {} + private static $token; + final public static $staticSeal; + protected $visible; + public readonly int $locked; + final public int $sealed; +} +$baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); +echo $baseStatic->isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; +echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->isProtectedSet() ? "T" : "t"; +echo $staticProp->isPrivateSet() ? "D" : "d"; +echo $staticProp->getModifiers(); +echo ":"; +$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->isProtectedSet() ? "T" : "t"; +echo $visibleProp->isPrivateSet() ? "D" : "d"; +echo $visibleProp->getModifiers(); +echo ":"; +$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->isProtectedSet() ? "T" : "t"; +echo $readonlyProp->isPrivateSet() ? "D" : "d"; +echo $readonlyProp->getModifiers(); +echo ":"; +$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); +echo ":"; +$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); +echo ":"; +$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); +echo ":"; +$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; +echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; +echo $classReadonlyProp->getModifiers(); +echo ":"; +echo $visibleProp->isDynamic() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports eval-declared asymmetric set visibility. +#[test] +fn execute_program_reflects_eval_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"class EvalReflectAsymmetricProperty { + public private(set) int $privateSet = 1; + public protected(set) int $protectedSet = 2; + protected private(set) int $protectedPrivateSet = 3; +} +$private = new ReflectionProperty("EvalReflectAsymmetricProperty", "privateSet"); +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers(); echo ":"; +$protected = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers(); echo ":"; +$protectedPrivate = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedPrivateSet"); +echo $protectedPrivate->isPrivateSet() ? "P" : "p"; +echo $protectedPrivate->isProtectedSet() ? "T" : "t"; +echo $protectedPrivate->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Pt4129:pT2049:Pt4130"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports asymmetric set visibility on eval interface contracts. +#[test] +fn execute_program_reflects_eval_interface_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"interface EvalReflectAsymmetricIfaceProperty { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +} +$protected = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "name"); +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isFinal() ? "F" : "f"; +echo $protected->getModifiers(); echo ":"; +$private = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "id"); +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isFinal() ? "F" : "f"; +echo $private->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Tpf2625:tPF4705"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_property_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedTarget { + public function __construct(public int $id, private string $name = "Ada") {} + public string $plain = "x"; +} +$id = new ReflectionProperty("EvalReflectPromotedTarget", "id"); +$name = new ReflectionProperty("EvalReflectPromotedTarget", "name"); +$plain = new ReflectionProperty("EvalReflectPromotedTarget", "plain"); +echo $id->isPromoted() ? "I" : "i"; +echo $name->isPromoted() ? "N" : "n"; +echo $plain->isPromoted() ? "P" : "p"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "INp"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod constructor/destructor predicates for eval methods. +#[test] +fn execute_program_reflection_method_reports_constructor_and_destructor() { + let program = parse_fragment( + br#"class EvalReflectLifecycle { + public function __construct() {} + public function __destruct() {} + public function run() {} +} +$ctor = new ReflectionMethod("EvalReflectLifecycle", "__CONSTRUCT"); +echo $ctor->isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod("EvalReflectLifecycle", "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Cd:cD:cd:Cd"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs new file mode 100644 index 0000000000..b7cf7ab37f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Organizes interpreter coverage for eval-backed class metadata and Reflection. +//! Each child module owns one coherent PHP-visible metadata surface. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - The split mirrors the production Reflection boundaries without sharing fixtures. + +mod attributes; +mod callable_types; +mod class_capabilities; +mod class_identity; +mod class_operations; +mod constants_enums_objects; +mod member_constructors; +mod member_metadata; +mod parameter_defaults; +mod property_values; +mod relations; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs new file mode 100644 index 0000000000..c11a611a93 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs @@ -0,0 +1,154 @@ +//! Purpose: +//! Interpreter tests for reflected parameter and property default values. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constant and magic-constant defaults are resolved in their declaring scope. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionParameter reports eval constant-default metadata. +#[test] +fn execute_program_reflection_parameter_reports_default_constant_metadata() { + let program = parse_fragment( + br##"define("EVAL_REFLECT_PARAM_DEFAULT_GLOBAL", "G"); +class EvalReflectParamDefaultBase { + const BASE = "B"; +} +class EvalReflectParamDefaultTarget extends EvalReflectParamDefaultBase { + const LABEL = "L"; + public function run($required, $global = EVAL_REFLECT_PARAM_DEFAULT_GLOBAL, $self = self::LABEL, $parent = parent::BASE, $literal = 7) {} +} +$params = (new ReflectionMethod("EvalReflectParamDefaultTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->getName(); echo ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueAvailable()) { + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + } + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter default magic constants use declaring callable scopes. +#[test] +fn execute_program_reflection_parameter_resolves_default_magic_constants() { + let program = parse_fragment( + br##"namespace EvalReflectParamMagicNs; +function eval_reflect_param_magic($fn = __FUNCTION__, $m = __METHOD__, $c = __CLASS__, $t = __TRAIT__, $n = __NAMESPACE__) {} +interface EvalReflectParamMagicIface { + public function read($c = __CLASS__, $m = __METHOD__, $fn = __FUNCTION__, $t = __TRAIT__, $n = __NAMESPACE__); +} +trait EvalReflectParamMagicTrait { + public function source($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +class EvalReflectParamMagicBox { + use EvalReflectParamMagicTrait { source as aliasSource; } + public function own($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +function eval_param_magic_dump($ref) { + foreach ($ref->getParameters() as $param) { + echo "[" . $param->getDefaultValue() . "]"; + } + echo ":"; +} +eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\eval_reflect_param_magic")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read")); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[][][EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", + "[own]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", + "[source]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", + "[read]", + "[]", + "[EvalReflectParamMagicNs]:" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval property default metadata as an associative map. +#[test] +fn execute_program_reflection_class_get_default_properties_metadata() { + let program = parse_fragment( + br#"class EvalReflectDefaultBase { + public int $base = 1; + protected string $prot = "p"; + private int $shadow = 3; + public $implicit; + public int $typed; + public static string $baseStatic = "bs"; +} +class EvalReflectDefaultChild extends EvalReflectDefaultBase { + public int $child = 5; + private int $shadow = 9; + public static int $childStatic = 7; + public ?int $nullable = null; +} +$defaults = (new ReflectionClass("EvalReflectDefaultChild"))->getDefaultProperties(); +echo $defaults["childStatic"]; echo ":"; +echo $defaults["baseStatic"]; echo ":"; +echo $defaults["child"]; echo ":"; +echo $defaults["shadow"]; echo ":"; +echo $defaults["base"]; echo ":"; +echo $defaults["prot"]; echo ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo array_key_exists("typed", $defaults) ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:bs:5:9:1:p:I:N:t"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs new file mode 100644 index 0000000000..bf5ed2dfae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs @@ -0,0 +1,474 @@ +//! Purpose: +//! Interpreter tests for ReflectionProperty value access and parameter construction. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Static, instance, dynamic, hooked, raw, and initialized-state paths are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionProperty can read and write eval instance and static property values. +#[test] +fn execute_program_reflection_property_gets_and_sets_eval_values() { + let program = parse_fragment( + br#"class EvalReflectValueBase { + private $secret = "base"; + public static $count = 1; +} +class EvalReflectValueChild extends EvalReflectValueBase { + protected $name = "Ada"; +} +class EvalReflectValueHook { + public $raw = 2; + public $doubled { + get => $this->raw * 2; + set { $this->raw = $value + 1; } + } + public $backed { + get { return $this->backed * 2; } + set { $this->backed = $value; } + } + public $virtual { + get => $this->raw + 100; + } + public function __construct() { + $this->backed = 2; + } +} +$child = new EvalReflectValueChild(); +$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); +echo $secret->getValue($child); echo ":"; +$secret->setValue($child, "changed"); +echo $secret->getValue(object: $child); echo ":"; +$name = new ReflectionProperty("EvalReflectValueChild", "name"); +echo $name->getValue($child); echo ":"; +$name->setValue(objectOrValue: $child, value: "Grace"); +echo $name->getValue($child); echo ":"; +$count = new ReflectionProperty("EvalReflectValueBase", "count"); +echo $count->getValue(); echo ":"; +$count->setValue(5); +echo EvalReflectValueChild::$count; echo ":"; +$count->setValue(null, 6); +echo $count->getValue($child); echo ":"; +$hook = new EvalReflectValueHook(); +$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); +echo $doubled->getValue($hook); echo ":"; +$doubled->setValue($hook, 4); +echo $hook->raw; echo ":"; +echo $doubled->getValue($hook); echo ":"; +$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setValue($hook, 4); +echo $backed->getRawValue(object: $hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setRawValue(object: $hook, value: 7); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +echo $backed->isLazy($hook) ? "L" : "l"; echo ":"; +$backed->skipLazyInitialization(object: $hook); +$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +echo $backed->getModifiers(); echo ":"; +echo $backed->isVirtual() ? "V" : "b"; echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V" : "b"; echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty raw APIs reject virtual eval property hooks. +#[test] +fn execute_program_reflection_property_rejects_virtual_raw_value() { + let program = parse_fragment( + br#"class EvalReflectVirtualRawHook { + public $raw = 2; + public $virtual { + get => $this->raw * 2; + } +} +$object = new EvalReflectVirtualRawHook(); +$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); +$property->getRawValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("virtual raw property read should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies ReflectionProperty reports eval instance and static initialization state. +#[test] +fn execute_program_reflection_property_reports_initialized_state() { + let program = parse_fragment( + br#"class EvalReflectInitializedTarget { + public int $typed; + public ?int $nullable; + public $plain; + public static int $staticTyped; + public static $staticPlain; + public $virtual { + get => 42; + } +} +$object = new EvalReflectInitializedTarget(); +$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); +$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); +$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); +$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); +$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); +$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $plain->isInitialized(object: $object) ? "P" : "p"; echo ":"; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +echo $staticPlain->isInitialized() ? "N" : "n"; echo ":"; +EvalReflectInitializedTarget::$staticTyped = 3; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +$object->typed = 5; +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +unset($object->typed); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +$typed->setRawValue(object: $object, value: 9); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +$nullable->setValue($object, null); +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +echo $virtual->isInitialized($object) ? "V" : "v"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "t:P:s:N:S:T:t:T:y:Y:V"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty materializes and operates on public dynamic properties. +#[test] +fn execute_program_reflection_property_supports_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectDynamicBase {} +class EvalReflectDynamicChild extends EvalReflectDynamicBase {} +$object = new EvalReflectDynamicBase(); +$object->dynamic = "first"; +$child = new EvalReflectDynamicChild(); +$child->dynamic = "child"; +$empty = new EvalReflectDynamicChild(); +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); echo ":"; +echo $property->isDynamic() ? "D" : "d"; echo ":"; +echo $property->isDefault() ? "Y" : "N"; echo ":"; +echo $property->getModifiers(); echo ":"; +echo is_null($property->getType()) ? "T" : "t"; echo ":"; +echo is_null($property->getSettableType()) ? "S" : "s"; echo ":"; +echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; +echo is_null($property->getDefaultValue()) ? "V" : "v"; echo ":"; +echo $property->isLazy($object) ? "L" : "l"; echo ":"; +echo $property->isInitialized($object) ? "I" : "i"; echo ":"; +echo $property->getValue($object); echo ":"; +echo $property->getValue($child); echo ":"; +echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; +echo is_null($property->getValue($empty)) ? "null" : "bad"; echo ":"; +$property->setValue($empty, "filled"); +echo $property->getValue($empty); echo ":"; +$property->setRawValue($object, "raw"); +echo $property->getRawValue($object); echo ":"; +echo str_replace("\n", "\\n", $property->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dynamic:D:N:1:T:S:h:V:l:I:first:child:e:null:filled:raw:Property [ public $dynamic ]\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property hook metadata and methods. +#[test] +fn execute_program_reflection_property_gets_eval_hook_metadata() { + let program = parse_fragment( + br#"class EvalReflectHookedProperty { + public int $raw = 2; + public int $doubled { + get { return $this->raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} +abstract class EvalReflectAbstractHookProperty { + abstract public int $contract { get; set; } +} +interface EvalReflectInterfaceHookProperty { + public int $iface { get; } +} +$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); +$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); +$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); +$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; +$caseList = PropertyHookType::cases(); +echo count($caseList); echo ":"; echo $caseList[0]->name; echo ":"; echo $caseList[1]->value; echo ":"; +echo PropertyHookType::from("set")->name; echo ":"; +echo PropertyHookType::tryFrom("missing") === null ? "T" : "t"; echo ":"; +echo $hooked->hasHooks() ? "H" : "h"; echo ":"; +echo $hooked->hasHook($getCase) ? "G" : "g"; echo ":"; +echo $hooked->hasHook(type: $setCase) ? "S" : "s"; echo ":"; +$hooks = $hooked->getHooks(); +echo count($hooks); echo ":"; echo $hooks["get"]->getName(); echo ":"; echo $hooks["set"]->getName(); echo ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getDeclaringClass()->getName(); echo ":"; echo $get->getNumberOfParameters(); echo ":"; +echo $set->getNumberOfParameters(); echo ":"; echo $set->getParameters()[0]->getName(); echo ":"; +$box = new EvalReflectHookedProperty(); +echo $get->invoke($box); echo ":"; +$set->invoke($box, 7); +echo $box->raw; echo ":"; +echo $readonly->hasHook($getCase) ? "R" : "r"; echo ":"; +echo $readonly->hasHook($setCase) ? "w" : "W"; echo ":"; +echo $readonly->getHook($setCase) === null ? "N" : "n"; echo ":"; +echo $plain->hasHooks() ? "bad" : "plain"; echo ":"; +echo count($plain->getHooks()); echo ":"; +$abstractHooks = $abstract->getHooks(); +echo count($abstractHooks); echo ":"; +echo $abstract->hasHook($getCase) ? "AG" : "ag"; echo ":"; +echo $abstract->hasHook($setCase) ? "AS" : "as"; echo ":"; +echo $abstractHooks["get"]->getName(); echo ":"; echo $abstractHooks["get"]->isAbstract() ? "A" : "a"; echo ":"; +echo $abstractHooks["set"]->getName(); echo ":"; echo $abstractHooks["set"]->isAbstract() ? "A" : "a"; echo ":"; +$ifaceHook = $iface->getHook($getCase); +echo count($iface->getHooks()); echo ":"; +echo $iface->hasHook($getCase) ? "IG" : "ig"; echo ":"; +echo $iface->hasHook($setCase) ? "bad" : "is"; echo ":"; +echo $ifaceHook->isAbstract() ? "IA" : "ia"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes and mutates eval static property values. +#[test] +fn execute_program_reflection_class_static_property_values() { + let program = parse_fragment( + br#"class EvalReflectStaticBase { + public static $base = "b"; + protected static $prot = "p"; + private static $shadow = "base-hidden"; + public $instance = "i"; +} +class EvalReflectStaticChild extends EvalReflectStaticBase { + public static $child = "c"; + private static $shadow = "child-hidden"; + public static int $count = 1; +} +EvalReflectStaticChild::$child = "mut"; +$ref = new ReflectionClass("EvalReflectStaticChild"); +$statics = $ref->getStaticProperties(); +echo count($statics); echo ":"; +echo $statics["child"]; echo ":"; +echo $statics["base"]; echo ":"; +echo $statics["prot"]; echo ":"; +echo $statics["shadow"]; echo ":"; +echo $ref->getStaticPropertyValue("count"); echo ":"; +$ref->setStaticPropertyValue("shadow", "changed"); +echo $ref->getStaticPropertyValue("shadow"); echo ":"; +$ref->setStaticPropertyValue(name: "count", value: 5); +echo EvalReflectStaticChild::$count; echo ":"; +echo $ref->getStaticPropertyValue("instance", "fallback"); echo ":"; +echo $ref->getStaticPropertyValue("missing", "fallback"); echo ":"; +try { + $ref->getStaticPropertyValue("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval ReflectionParameter exposes declaring class metadata. +#[test] +fn execute_program_reflects_eval_parameter_declaring_class() { + let program = parse_fragment( + br#"class EvalDeclaringParamBase { + public function inherited($base) {} +} +class EvalDeclaringParamChild extends EvalDeclaringParamBase { + public function own($child) {} +} +$inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName(); echo ":"; +$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName(); echo ":"; +echo $listed->getDeclaringFunction()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies direct ReflectionParameter construction accepts runtime object method targets. +#[test] +fn execute_program_reflection_parameter_accepts_object_expression_target() { + let program = parse_fragment( + br#"class EvalDirectParamObjectTarget { + public function run(int $id, ?string $name = null) {} +} +$param = new ReflectionParameter([new EvalDirectParamObjectTarget(), "run"], "name"); +echo $param->getName(); echo ":"; +echo $param->getPosition(); echo ":"; +echo $param->getDeclaringClass()->getName(); echo ":"; +echo $param->getDeclaringFunction()->getName(); echo ":"; +echo $param->isOptional() ? "O" : "R"; echo ":"; +echo $param->getType()->getName(); echo ":"; +echo $param->allowsNull() ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "name:1:EvalDirectParamObjectTarget:run:O:string:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_parameter_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"function eval_reflect_param_error_function($known) {} +class EvalReflectParamErrorTarget { + public function run($known) {} +} +try { + new ReflectionParameter("eval_reflect_param_error_function", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], 3); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "missing"], "known"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], -1); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "known"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs new file mode 100644 index 0000000000..f0d284640c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Interpreter tests for class relations, visible members, and class variables. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval and AOT class targets share relation-builtin behavior. +//! - Visibility-sensitive OOP probes run through the fake interpreter runtime. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies class-relation helpers handle eval class and trait targets. +#[test] +fn execute_program_dispatches_class_relation_builtins() { + let program = parse_fragment( + br#"class EvalMeta {} +trait EvalMetaInnerTrait {} +trait EvalMetaOuterTrait { + use EvalMetaInnerTrait; +} +$object = new EvalMeta(); +$implements = class_implements("EvalMeta"); +echo is_array($implements) && count($implements) === 0 ? "impl" : "bad"; echo ":"; +$parents = class_parents($object); +echo is_array($parents) && count($parents) === 0 ? "parents" : "bad"; echo ":"; +$uses = class_uses("EvalMeta"); +echo is_array($uses) && count($uses) === 0 ? "uses" : "bad"; echo ":"; +echo class_implements("MissingMeta") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("class_implements", "EvalMeta"); +echo is_array($call) && count($call) === 0 ? "call" : "bad"; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "EvalMeta"]); +echo is_array($named) && count($named) === 0 ? "named" : "bad"; echo ":"; +$trait_uses = class_uses("EvalMetaOuterTrait"); +echo $trait_uses["EvalMetaInnerTrait"]; echo ":"; +class_alias("EvalMetaOuterTrait", "EvalMetaOuterTraitAlias"); +$alias_uses = class_uses("EvalMetaOuterTraitAlias"); +echo $alias_uses["EvalMetaInnerTrait"]; echo ":"; +echo function_exists("class_implements"); echo function_exists("class_parents"); +echo function_exists("class_uses"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "impl:parents:uses:missing:call:named:EvalMetaInnerTrait:EvalMetaInnerTrait:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval-declared parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_eval_class_relation_metadata() { + let program = parse_fragment( + br#"class EvalMetaBase {} +class EvalMetaChild extends EvalMetaBase implements KnownInterface {} +$object = new EvalMetaChild(); +$implements = class_implements($object); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +echo $implements["Traversable"]; echo ":"; +$parents = class_parents("EvalMetaChild"); +echo count($parents); echo ":"; +echo $parents["EvalMetaBase"]; echo ":"; +$call = call_user_func("class_implements", "EvalMetaChild"); +echo $call["KnownInterface"]; echo ":"; +echo $call["Traversable"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => $object]); +echo $named["EvalMetaBase"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:KnownInterface:Traversable:1:EvalMetaBase:KnownInterface:Traversable:EvalMetaBase" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies generated/AOT parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_aot_class_relation_metadata() { + let program = parse_fragment( + br#"$implements = class_implements("KnownClass"); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +$parents = class_parents("KnownClass"); +echo count($parents); echo ":"; +echo $parents["ParentClass"]; echo ":"; +$call = call_user_func("class_implements", "KnownClass"); +echo $call["KnownInterface"]; echo ":"; +$interfaceParents = class_implements("KnownInterface"); +echo $interfaceParents["Traversable"]; echo ":"; +$uses = class_uses("KnownClass"); +echo count($uses); echo ":"; echo $uses["KnownTrait"]; echo ":"; +$traitUses = class_uses("KnownTrait"); +echo $traitUses["KnownInnerTrait"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "KnownClass"]); +echo $named["ParentClass"]; echo ":"; +class_alias("KnownClass", "KnownAlias"); +$aliasImplements = class_implements("KnownAlias"); +echo $aliasImplements["KnownInterface"]; echo ":"; +$aliasParents = class_parents("KnownAlias"); +echo $aliasParents["ParentClass"]; echo ":"; +$aliasUses = class_uses("KnownAlias"); +echo $aliasUses["KnownTrait"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + assert!(context.define_native_class_parent("KnownClass", "ParentClass")); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:1:KnownTrait:KnownInnerTrait:ParentClass:KnownInterface:ParentClass:KnownTrait" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies PHP OOP introspection builtins follow eval visibility and scope rules. +#[test] +fn execute_program_dispatches_oop_introspection_builtins() { + let program = parse_fragment( + br#"class EvalOopIntrospectBase { + private $baseSecret = "bp"; + protected $baseProtected = "bq"; + public $basePublic = "br"; + private function basePrivate() {} + protected function baseProtectedMethod() {} + public function basePublicMethod() {} + public function parentView() { + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +class EvalOopIntrospectChild extends EvalOopIntrospectBase { + private $childSecret = "cp"; + protected $childProtected = "cq"; + public $childPublic = "cr"; + private function childPrivate() {} + protected function childProtectedMethod() {} + public function childPublicMethod() {} + public function childView() { + $methods = get_class_methods($this); + sort($methods); + echo implode(",", $methods); echo "|"; + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +$object = new EvalOopIntrospectChild(); +$object->dynamic = "dyn"; +echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; +echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; +echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; +echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; +echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; +echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; +$methods = get_class_methods("EvalOopIntrospectChild"); +sort($methods); +echo implode(",", $methods); echo ":"; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +$object->childView(); echo ":"; +$object->parentView(); echo ":"; +echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; +echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; +echo function_exists("method_exists"); echo function_exists("property_exists"); +echo function_exists("get_class_methods"); echo function_exists("get_object_vars"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `get_class_vars()` materializes visible defaults for eval class-like metadata. +#[test] +fn execute_program_dispatches_get_class_vars_builtin() { + let program = parse_fragment( + br#"trait EvalClassVarsTrait { + public $traitPublic = "tp"; + protected $traitProtected = "tq"; +} +enum EvalClassVarsBacked: int { case Ready = 1; } +class EvalClassVarsBase { + public $basePublic = "bp"; + protected $baseProtected = "bq"; + private $basePrivate = "bs"; + public static $baseStatic = "static"; + public int $typed; +} +class EvalClassVarsChild extends EvalClassVarsBase { + use EvalClassVarsTrait; + public $childPublic = "cp"; + protected $childProtected = "cq"; + private $childPrivate = "cs"; + public function childView() { + $vars = get_class_vars(self::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } + public function baseView() { + $vars = get_class_vars(EvalClassVarsBase::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } +} +$outside = get_class_vars("EvalClassVarsChild"); +ksort($outside); +foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +(new EvalClassVarsChild())->childView(); +echo ":"; +(new EvalClassVarsChild())->baseView(); +echo ":"; +$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); +ksort($trait); +foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); +ksort($enum); +foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +echo function_exists("get_class_vars") ? "F" : "f"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs b/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs new file mode 100644 index 0000000000..73cb0be50a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs @@ -0,0 +1,194 @@ +//! Purpose: +//! Interpreter tests for eval debug-output builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These tests cover `print_r()` capture mode, variadic `var_dump()`, and +//! object property/reference rendering without growing the generic expression suite. + +use super::super::*; +use super::support::*; + +/// Verifies eval `print_r()` emits values, returns true, and captures output when requested. +#[test] +fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; +print_r(value: false); echo ":"; +print_r([1, 2]); echo ":"; +$call = call_user_func("print_r", true); +$spread = call_user_func_array("print_r", ["value" => "z"]); +$captured = print_r(["k" => 1], true); +$captured_call = call_user_func_array("print_r", ["value" => ["q" => 2], "return" => true]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +echo $captured === "Array\n(\n [k] => 1\n)\n" ? "captured" : "bad"; echo ":"; +echo $captured_call === "Array\n(\n [q] => 2\n)\n" ? "callcaptured" : "bad"; echo ":"; +return function_exists("print_r");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "x::", + "Array\n", + "(\n", + " [0] => 1\n", + " [1] => 2\n", + ")\n", + ":1z:call:spread:captured:callcaptured:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies captured `print_r()` output includes eval object properties. +#[test] +fn execute_program_print_r_captures_object_properties() { + let program = parse_fragment( + br#"class EvalPrintRProps { + public $a = 1; + protected $b = "p"; + private $c = false; + public function __construct(&$ref) { + $this->a =& $ref; + $this->dyn = [2]; + } +} +$x = 9; +$o = new EvalPrintRProps($x); +$x = 10; +return print_r($o, true);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let FakeValue::String(output) = values.get(result) else { + panic!("expected captured print_r object output"); + }; + assert_eq!(values.output, ""); + assert!(output.contains("EvalPrintRProps Object\n")); + assert!(output.contains(" [a] => 10\n")); + assert!(output.contains(" [b:protected] => p\n")); + assert!(output.contains(" [c:EvalPrintRProps:private] => \n")); + assert!(output.contains(" [dyn] => Array\n")); +} + +/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. +#[test] +fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( + br#"var_dump(42); +var_dump("hi"); +var_dump(false); +var_dump(null); +var_dump([10, 20]); +var_dump(["x" => true]); +var_dump(1, "m", [2]); +$call = call_user_func("var_dump", 3.5, false); +$spread = call_user_func_array("var_dump", ["value" => "z", "values" => true]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +return function_exists("var_dump");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "int(1)\n", + "string(1) \"m\"\n", + "array(1) {\n", + " [0]=>\n", + " int(2)\n", + "}\n", + "float(3.5)\n", + "bool(false)\n", + "string(1) \"z\"\n", + "bool(true)\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `var_dump()` keeps eval-declared and runtime object class names. +#[test] +fn execute_program_var_dump_prints_object_class_names() { + let program = parse_fragment( + br#"class EvalDumpBox {} +var_dump(new EvalDumpBox()); +var_dump(new KnownClass());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert!(values.output.contains("object(EvalDumpBox)#")); + assert!(values.output.contains(" (0) {\n}\n")); + assert!(values.output.contains("object(KnownClass)#")); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies object dumps include eval-declared properties, dynamic properties, and refs. +#[test] +fn execute_program_var_dump_prints_object_properties_and_references() { + let program = parse_fragment( + br#"class EvalDumpProps { + public $a = 1; + protected $b = "p"; + private $c = false; + public function __construct(&$ref) { + $this->a =& $ref; + $this->dyn = [2]; + } +} +$x = 9; +$o = new EvalDumpProps($x); +var_dump($o); +$x = 10; +var_dump($o);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert!(values.output.contains("object(EvalDumpProps)#")); + assert!(values.output.contains(" (4) {\n")); + assert!(values.output.contains(" [\"a\"]=>\n &int(9)\n")); + assert!(values.output.contains(" [\"b\":protected]=>\n string(1) \"p\"\n")); + assert!(values.output.contains(" [\"c\":\"EvalDumpProps\":private]=>\n bool(false)\n")); + assert!(values.output.contains(" [\"dyn\"]=>\n array(1) {\n")); + assert!(values.output.contains(" [\"a\"]=>\n &int(10)\n")); + assert_eq!(values.get(result), FakeValue::Null); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs new file mode 100644 index 0000000000..48771e860f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Interpreter tests for eval local directory resource builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Each test uses a process-unique directory and removes it before and after execution. +//! - Directory resources share eval's generic resource cell representation. + +use super::super::*; +use super::support::*; + +/// Verifies eval directory resources support open/read/rewind/close operations. +#[test] +fn execute_program_dispatches_directory_stream_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_magician_dir_stream_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$dh = opendir("{dir}"); +echo is_resource($dh) ? "open" : "bad"; echo ":"; +echo get_resource_type($dh) === "stream" ? "rtype" : "bad"; echo ":"; +echo readdir($dh) === "." ? "dot" : "bad"; echo ":"; +echo readdir($dh) === ".." ? "dotdot" : "bad"; echo ":"; +$entries = [readdir($dh), readdir($dh)]; +echo in_array("a.txt", $entries) && in_array("b.txt", $entries) ? "entries" : "bad"; echo ":"; +echo readdir($dh) === false ? "eof" : "bad"; echo ":"; +rewinddir($dh); +echo readdir($dh) === "." ? "rewind" : "bad"; echo ":"; +call_user_func("rewinddir", $dh); +echo call_user_func("readdir", $dh) === "." ? "callread" : "bad"; echo ":"; +call_user_func("closedir", $dh); +echo readdir($dh) === false ? "closed" : "bad"; echo ":"; +$call = call_user_func_array("opendir", ["directory" => "{dir}"]); +echo call_user_func("readdir", $call) === "." ? "callopen" : "bad"; echo ":"; +closedir($call); +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("opendir"); echo function_exists("readdir"); +echo function_exists("rewinddir"); echo function_exists("closedir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_dir_all(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_dir_all(&dir); + assert_eq!( + values.output, + "open:rtype:dot:dotdot:entries:eof:rewind:callread:closed:callopen:cleanup:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs new file mode 100644 index 0000000000..0a49862ef9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs @@ -0,0 +1,309 @@ +//! Purpose: +//! Interpreter tests for eval local file stream resource builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases use process-unique local files and clean them before and after execution. +//! - Stream resources are eval-owned fake/runtime cells whose ids map into context state. + +use super::super::*; +use super::support::*; + +/// Verifies eval file stream resources support open/read/write/seek/stat/close operations. +#[test] +fn execute_program_dispatches_file_stream_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_file_{pid}.txt"); + let copy = format!("elephc_magician_stream_copy_{pid}.txt"); + let call = format!("elephc_magician_stream_call_{pid}.txt"); + let source = format!( + r#"$h = fopen(filename: "{file}", mode: "w+"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo get_resource_type($h) === "stream" ? "rtype" : "bad"; echo ":"; +echo get_resource_id($h) >= 1 ? "rid" : "bad"; echo ":"; +echo fwrite($h, "abcdef") === 6 ? "write" : "bad"; echo ":"; +echo ftell($h) === 6 ? "tell" : "bad"; echo ":"; +echo rewind($h) ? "rewind" : "bad"; echo ":"; +echo fread($h, 2) === "ab" ? "read" : "bad"; echo ":"; +echo fseek($h, 1) === 0 ? "seek" : "bad"; echo ":"; +echo stream_get_contents($h, 3) === "bcd" ? "bounded" : "bad"; echo ":"; +rewind($h); +echo stream_get_contents($h) === "abcdef" ? "contents" : "bad"; echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +$meta = stream_get_meta_data($h); +echo $meta["wrapper_type"] === "plainfile" && $meta["stream_type"] === "STDIO" && $meta["mode"] === "w+" ? "meta" : "bad"; echo ":"; +echo ftruncate($h, 3) ? "truncate" : "bad"; echo ":"; +$stat = fstat($h); +echo $stat["size"] === 3 ? "fstat" : "bad"; echo ":"; +echo fflush($h) && fsync($h) && fdatasync($h) ? "sync" : "bad"; echo ":"; +echo fclose($h) ? "close" : "bad"; echo ":"; +echo file_get_contents("{file}") === "abc" ? "truncated" : "bad"; echo ":"; +$src = fopen("{file}", "r"); +$dst = fopen("{copy}", "w+"); +echo stream_copy_to_stream($src, $dst, null, 1) === 2 ? "copy" : "bad"; echo ":"; +rewind($dst); +echo stream_get_contents($dst) === "bc" ? "copied" : "bad"; echo ":"; +fclose($src); +fclose($dst); +$tmp = tmpfile(); +echo is_resource($tmp) ? "tmp" : "bad"; echo ":"; +fwrite($tmp, "xy"); +rewind($tmp); +echo fread($tmp, 2) === "xy" ? "tmpread" : "bad"; echo ":"; +fclose($tmp); +$call = call_user_func_array("fopen", ["filename" => "{call}", "mode" => "w+"]); +echo call_user_func_array("fwrite", ["stream" => $call, "data" => "zz"]) === 2 ? "callwrite" : "bad"; echo ":"; +call_user_func("rewind", $call); +echo call_user_func("fread", $call, 2) === "zz" ? "callread" : "bad"; echo ":"; +echo call_user_func("fclose", $call) ? "callclose" : "bad"; echo ":"; +echo unlink("{file}") && unlink("{copy}") && unlink("{call}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fopen"); echo function_exists("fclose"); echo function_exists("fread"); +echo function_exists("fwrite"); echo function_exists("feof"); echo function_exists("fflush"); +echo function_exists("ftell"); echo function_exists("fseek"); echo function_exists("rewind"); +echo function_exists("ftruncate"); echo function_exists("fsync"); echo function_exists("fdatasync"); +echo function_exists("fstat"); echo function_exists("stream_get_contents"); +echo function_exists("stream_copy_to_stream"); echo function_exists("stream_get_meta_data"); +echo function_exists("tmpfile"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, ©, &call] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, ©, &call] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "open:rtype:rid:write:tell:rewind:read:seek:bounded:contents:eof:meta:truncate:fstat:sync:close:truncated:copy:copied:tmp:tmpread:callwrite:callread:callclose:cleanup:11111111111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `fstat()` returns PHP's complete numeric and string-keyed array. +#[test] +fn execute_program_dispatches_complete_fstat_array() { + let pid = std::process::id(); + let file = format!("elephc_magician_complete_fstat_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "abcdef"); +$h = fopen("{file}", "r"); +$stat = fstat($h); +echo count($stat) === 26 ? "count" : "bad"; echo ":"; +echo $stat[0] === $stat["dev"] && $stat[1] === $stat["ino"] && $stat[2] === $stat["mode"] ? "head" : "bad"; echo ":"; +echo $stat[3] === $stat["nlink"] && $stat[4] === $stat["uid"] && $stat[5] === $stat["gid"] ? "owner" : "bad"; echo ":"; +echo $stat[6] === $stat["rdev"] && $stat[7] === $stat["size"] && $stat["size"] === 6 ? "size" : "bad"; echo ":"; +echo $stat[8] === $stat["atime"] && $stat[9] === $stat["mtime"] && $stat[10] === $stat["ctime"] ? "time" : "bad"; echo ":"; +echo $stat[11] === $stat["blksize"] && $stat[12] === $stat["blocks"] ? "blocks" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "count:head:owner:size:time:blocks:cleanup" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `flock()` applies advisory locks to local file stream resources. +#[test] +fn execute_program_dispatches_file_stream_flock_builtin() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_lock_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "x"); +$h = fopen("{file}", "r+"); +$would = true; +echo flock($h, LOCK_EX, $would) ? "lock" : "bad"; echo ":"; +echo $would === false ? "would0" : "bad"; echo ":"; +echo flock(stream: $h, operation: LOCK_UN, would_block: $would) ? "unlock" : "bad"; echo ":"; +echo $would === false ? "would1" : "bad"; echo ":"; +class EvalFlockWouldBlockBox {{ + public bool $value = true; + public static bool $staticValue = true; +}} +$box = new EvalFlockWouldBlockBox(); +echo flock($h, LOCK_SH, $box->value) ? "proplock" : "bad"; echo ":"; +echo $box->value === false ? "prop0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$name = "value"; +echo flock($h, LOCK_EX, $box->{{$name}}) ? "dynlock" : "bad"; echo ":"; +echo $box->value === false ? "dyn0" : "bad"; echo ":"; +flock($h, LOCK_UN); +echo flock($h, LOCK_SH, EvalFlockWouldBlockBox::$staticValue) ? "staticlock" : "bad"; echo ":"; +echo EvalFlockWouldBlockBox::$staticValue === false ? "static0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$class = "EvalFlockWouldBlockBox"; +$staticName = "staticValue"; +echo flock($h, LOCK_EX, $class::${{$staticName}}) ? "dynstaticlock" : "bad"; echo ":"; +echo EvalFlockWouldBlockBox::$staticValue === false ? "dynstatic0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$lock = "flock"; +$dynamicWould = true; +echo $lock($h, LOCK_SH, $dynamicWould) ? "dynfnlock" : "bad"; echo ":"; +echo $dynamicWould === false ? "dynfn0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$firstClassLock = flock(...); +$firstClassWould = true; +echo $firstClassLock($h, LOCK_EX, $firstClassWould) ? "fcclock" : "bad"; echo ":"; +echo $firstClassWould === false ? "fcc0" : "bad"; echo ":"; +flock($h, LOCK_UN); +echo call_user_func("flock", $h, LOCK_SH) ? "calllock" : "bad"; echo ":"; +flock($h, LOCK_UN); +echo flock($h, 99) === false ? "invalid" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("flock"); +echo defined("LOCK_SH"); echo defined("LOCK_EX"); echo defined("LOCK_UN"); echo defined("LOCK_NB"); +echo ":locks=" . LOCK_SH . LOCK_EX . LOCK_UN . LOCK_NB; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "lock:would0:unlock:would1:proplock:prop0:dynlock:dyn0:staticlock:static0:dynstaticlock:dynstatic0:dynfnlock:dynfn0:fcclock:fcc0:calllock:invalid:cleanup:11111:locks=1234" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval line, character, and passthrough stream builtins. +#[test] +fn execute_program_dispatches_file_stream_line_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_lines_{pid}.txt"); + let ending = format!("elephc_magician_stream_ending_{pid}.txt"); + let formatted = format!("elephc_magician_stream_formatted_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "a\nbc\nxyz"); +$h = fopen("{file}", "r"); +echo fgetc($h) === "a" ? "char" : "bad"; echo ":"; +echo fgets($h) === "\n" ? "line1" : "bad"; echo ":"; +echo fgets($h) === "bc\n" ? "line2" : "bad"; echo ":"; +echo stream_get_line($h, 2) === "xy" ? "getline" : "bad"; echo ":"; +echo "["; +$passed = fpassthru($h); +echo "]"; +echo $passed === 1 ? "passthru" : "bad"; echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +fclose($h); +file_put_contents("{ending}", "leftENDright"); +$e = fopen("{ending}", "r"); +echo stream_get_line($e, 20, "END") === "left" ? "ending" : "bad"; echo ":"; +echo stream_get_contents($e) === "right" ? "after" : "bad"; echo ":"; +fclose($e); +$call = call_user_func("fopen", "{file}", "r"); +echo call_user_func("fgetc", $call) === "a" ? "callchar" : "bad"; echo ":"; +echo call_user_func_array("stream_get_line", ["stream" => $call, "length" => 2]) === "\nb" ? "callline" : "bad"; echo ":"; +call_user_func("fclose", $call); +$fmt = fopen("{formatted}", "w+"); +echo fprintf($fmt, "%s-%d", "n", 7) === 3 ? "fprintf" : "bad"; echo ":"; +echo vfprintf($fmt, "-%s", ["x"]) === 2 ? "vfprintf" : "bad"; echo ":"; +rewind($fmt); +echo stream_get_contents($fmt) === "n-7-x" ? "formatted" : "bad"; echo ":"; +echo call_user_func("fprintf", $fmt, "-%s", "c") === 2 ? "callfprintf" : "bad"; echo ":"; +echo call_user_func_array("vfprintf", ["stream" => $fmt, "format" => "-%d", "values" => [5]]) === 2 ? "callvfprintf" : "bad"; echo ":"; +fclose($fmt); +echo unlink("{file}") && unlink("{ending}") && unlink("{formatted}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fgetc"); echo function_exists("fgets"); echo function_exists("fpassthru"); +echo function_exists("fprintf"); echo function_exists("stream_get_line"); echo function_exists("vfprintf"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, &ending, &formatted] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, &ending, &formatted] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "char:line1:line2:getline:[z]passthru:eof:ending:after:callchar:callline:fprintf:vfprintf:formatted:callfprintf:callvfprintf:cleanup:111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval CSV stream builtins format and parse local stream records. +#[test] +fn execute_program_dispatches_file_stream_csv_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_csv_{pid}.txt"); + let semi = format!("elephc_magician_stream_csv_semi_{pid}.txt"); + let call = format!("elephc_magician_stream_csv_call_{pid}.txt"); + let scan = format!("elephc_magician_stream_scan_{pid}.txt"); + let source = format!( + r#"$h = fopen("{file}", "w+"); +echo fputcsv($h, ["a", "b,c", "d\"e"]) > 0 ? "put" : "bad"; echo ":"; +rewind($h); +$row = fgetcsv($h); +echo $row[0] === "a" && $row[1] === "b,c" && $row[2] === "d\"e" ? "get" : "bad"; echo ":"; +echo fgetcsv($h) === false ? "eof" : "bad"; echo ":"; +fclose($h); +$semi = fopen("{semi}", "w+"); +echo fputcsv($semi, ["x;y", "z"], ";") > 0 ? "putsemi" : "bad"; echo ":"; +rewind($semi); +$semi_row = fgetcsv($semi, null, ";"); +echo $semi_row[0] === "x;y" && $semi_row[1] === "z" ? "getsemi" : "bad"; echo ":"; +fclose($semi); +$call = fopen("{call}", "w+"); +echo call_user_func_array("fputcsv", ["stream" => $call, "fields" => ["m", "n"]]) > 0 ? "callput" : "bad"; echo ":"; +rewind($call); +$call_row = call_user_func("fgetcsv", $call); +echo $call_row[0] === "m" && $call_row[1] === "n" ? "callget" : "bad"; echo ":"; +fclose($call); +file_put_contents("{scan}", "42 alpha\n7 beta\n"); +$scan = fopen("{scan}", "r"); +$matched = fscanf($scan, "%d %s"); +echo $matched[0] === "42" && $matched[1] === "alpha" ? "scan" : "bad"; echo ":"; +$call_matched = call_user_func("fscanf", $scan, "%d %s"); +echo $call_matched[0] === "7" && $call_matched[1] === "beta" ? "callscan" : "bad"; echo ":"; +fclose($scan); +echo unlink("{file}") && unlink("{semi}") && unlink("{call}") && unlink("{scan}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fgetcsv"); echo function_exists("fputcsv"); echo function_exists("fscanf"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, &semi, &call, &scan] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, &semi, &call, &scan] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "put:get:eof:putsemi:getsemi:callput:callget:scan:callscan:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs new file mode 100644 index 0000000000..2c18e4bcf8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs @@ -0,0 +1,274 @@ +//! Purpose: +//! Interpreter tests for filesystem path, metadata, stat, and space builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases use temporary files while keeping filesystem metadata assertions focused. + +use super::super::*; +use super::support::*; + +/// Verifies eval path component builtins mirror static basename/dirname edge cases. +#[test] +fn execute_program_dispatches_path_component_builtins() { + let program = parse_fragment( + br#"echo basename("/var/log/syslog.log", ".log") . ":"; +echo basename(path: "/usr///") . ":"; +echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; +echo dirname("/usr/local/bin/tool", 2) . ":"; +echo dirname(path: "/usr///local///bin") . ":"; +echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; +echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); +return function_exists("dirname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `realpath()` resolves existing paths and returns false for misses. +#[test] +fn execute_program_dispatches_realpath_builtin() { + let program = parse_fragment( + br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; +echo realpath(path: "elephc-magician-missing-path") === false ? "false" : "bad"; echo ":"; +echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; +echo call_user_func_array("realpath", ["path" => "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; +return function_exists("realpath");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "resolved:false:call:array-false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. +#[test] +fn execute_program_dispatches_fnmatch_builtin() { + let program = parse_fragment( + br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; +echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; +echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; +echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; +echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; +$flags = FNM_PATHNAME | FNM_CASEFOLD; +echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; +echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; +echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); +return FNM_CASEFOLD;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); +} +/// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. +#[test] +fn execute_program_dispatches_pathinfo_builtin() { + let program = parse_fragment( + br#"$info = pathinfo("/var/log/syslog.log"); +echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; +echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; +echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; +echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; +echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; +$plain = pathinfo("/etc/hosts"); +echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; +echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; +$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); +echo $call["basename"] . ":"; +echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); +return PATHINFO_ALL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); +} +/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. +#[test] +fn execute_program_dispatches_filesystem_builtins() { + let filename = format!("elephc_magician_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_magician_fs_missing_{}.txt", std::process::id()); + let source = format!( + r#"echo file_put_contents("{filename}", "hello") . ":"; +echo file_get_contents("{filename}") . ":"; +echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; +echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; +echo is_dir(".") ? "dir" : "bad"; echo ":"; +echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; +echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; +echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; +echo filesize("{filename}") . ":"; +echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; +echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; +echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; +echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); +return function_exists("unlink");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + assert_eq!( + values.output, + "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. +#[test] +fn execute_program_dispatches_disk_space_builtins() { + let program = parse_fragment( + br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-magician") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; +echo function_exists("disk_free_space"); +return function_exists("disk_total_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval stat metadata builtins expose scalar file metadata and link probes. +#[test] +fn execute_program_dispatches_stat_metadata_builtins() { + let filename = format!("elephc_magician_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_magician_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_magician_stat_link_{}.txt", std::process::id()); + let source = format!( + r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; +echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("{filename}") . ":"; +echo filetype(".") . ":"; +echo filetype("{link}") . ":"; +echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; +echo is_link("{link}") ? "link" : "bad"; echo ":"; +echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; +echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; +echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; +echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; +echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; +echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; +echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "{filename}") . ":"; +echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. +#[test] +fn execute_program_dispatches_stat_array_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_magician_stat_array_{pid}.txt"); + let link = format!("elephc_magician_lstat_array_{pid}.txt"); + let missing = format!("elephc_magician_stat_array_missing_{pid}.txt"); + let source = format!( + r#"$stat = stat("{filename}"); +$lstat = lstat("{link}"); +echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; +echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; +echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; +echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("stat", "{filename}"); +echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; +$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat array fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs new file mode 100644 index 0000000000..fc25eb689d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs @@ -0,0 +1,355 @@ +//! Purpose: +//! Interpreter tests for filesystem listing and mutating builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover directory traversal, globbing, file changes, and touch behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval local path operation builtins mutate filesystem state. +#[test] +fn execute_program_dispatches_path_operation_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_magician_ops_dir_{pid}"); + let call_dir = format!("elephc_magician_ops_call_dir_{pid}"); + let src = format!("elephc_magician_ops_src_{pid}.txt"); + let copy = format!("elephc_magician_ops_copy_{pid}.txt"); + let moved = format!("elephc_magician_ops_moved_{pid}.txt"); + let symlink = format!("elephc_magician_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_magician_ops_hardlink_{pid}.txt"); + let source = format!( + r#"file_put_contents("{src}", "hello"); +echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; +echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; +echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; +echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; +echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; +echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; +echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; +echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; +echo clearstatcache() === null ? "cache" : "bad"; echo ":"; +echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +return true;"#, + missing = format!("elephc_magician_ops_missing_{pid}.txt"), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + assert_eq!( + values.output, + "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `stream_resolve_include_path()` mirrors elephc realpath semantics. +#[test] +fn execute_program_dispatches_stream_resolve_include_path_builtin() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_resolve_{pid}.txt"); + let missing = format!("elephc_magician_stream_resolve_missing_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "payload"); +$resolved = stream_resolve_include_path("{file}"); +echo is_string($resolved) && basename($resolved) === "{file}" && file_get_contents($resolved) === "payload" ? "resolved" : "bad"; echo ":"; +echo stream_resolve_include_path("{missing}") === false ? "missing" : "bad"; echo ":"; +$named = stream_resolve_include_path(filename: "{file}"); +echo is_string($named) && basename($named) === "{file}" ? "named" : "bad"; echo ":"; +$call = call_user_func("stream_resolve_include_path", "{file}"); +echo is_string($call) && basename($call) === "{file}" ? "call" : "bad"; echo ":"; +$spread = call_user_func_array("stream_resolve_include_path", ["filename" => "{file}"]); +echo is_string($spread) && basename($spread) === "{file}" ? "spread" : "bad"; echo ":"; +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +return function_exists("stream_resolve_include_path");"#, + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let _ = std::fs::remove_file(&missing); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "resolved:missing:named:call:spread:cleanup:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. +#[test] +fn execute_program_dispatches_file_listing_builtins() { + let pid = std::process::id(); + let lines = format!("elephc_magician_listing_lines_{pid}.txt"); + let empty = format!("elephc_magician_listing_empty_{pid}.txt"); + let missing = format!("elephc_magician_listing_missing_{pid}.txt"); + let dir = format!("elephc_magician_listing_dir_{pid}"); + let source = format!( + r#"file_put_contents("{lines}", "one\ntwo"); +file_put_contents("{empty}", ""); +$lines = file("{lines}"); +echo count($lines) . ":"; +echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; +echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; +echo "["; +$bytes = readfile(filename: "{empty}"); +echo "]" . $bytes . ":"; +echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; +echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; +mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$scan = scandir(directory: "{dir}"); +echo count($scan) . ":"; +echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; +$call_lines = call_user_func("file", "{lines}"); +echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; +$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); +echo count($call_scan) . ":"; +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `glob()` expands local patterns and dispatches dynamically. +#[test] +fn execute_program_dispatches_glob_builtin() { + let pid = std::process::id(); + let dir = format!("elephc_magician_glob_dir_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.log", "b"); +file_put_contents("{dir}/c.txt", "c"); +file_put_contents("{dir}/.hidden.txt", "h"); +$matches = glob("{dir}/*.txt"); +echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; +echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; +$literal = glob("{dir}/a.txt"); +echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; +$all = glob("{dir}/*"); +echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; +$call = call_user_func("glob", "{dir}/*.log"); +echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; +$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("{dir}/.hidden.txt"); +unlink("{dir}/c.txt"); +unlink("{dir}/b.log"); +unlink("{dir}/a.txt"); +echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. +#[test] +fn execute_program_dispatches_file_modify_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_magician_modify_{pid}.txt"); + let missing = format!("elephc_magician_modify_missing_{pid}.txt"); + let prefix = format!("evm{pid}_"); + let call_prefix = format!("evc{pid}_"); + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; +echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; +echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; +$tmp = tempnam(directory: ".", prefix: "{prefix}"); +echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; +unlink($tmp); +$previous = umask(mask: 18); +$set = umask($previous); +echo $set === 18 ? "umask" : "bad"; echo ":"; +$before = umask(18); +$probe = umask(); +$restore = umask($before); +echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; +echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; +$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + assert_eq!( + values.output, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval ownership builtins mutate local files and dispatch dynamically. +#[test] +fn execute_program_dispatches_file_ownership_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_magician_ownership_{pid}.txt"); + let link = format!("elephc_magician_ownership_link_{pid}.txt"); + let missing = format!("elephc_magician_ownership_missing_{pid}.txt"); + let uid = unsafe { libc::geteuid() }; + let gid = unsafe { libc::getegid() }; + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo symlink("{filename}", "{link}") ? "symlink" : "bad"; echo ":"; +echo chown("{filename}", {uid}) ? "chown" : "bad"; echo ":"; +echo chgrp(filename: "{filename}", group: {gid}) ? "chgrp" : "bad"; echo ":"; +echo lchown("{link}", {uid}) ? "lchown" : "bad"; echo ":"; +echo lchgrp(filename: "{link}", group: {gid}) ? "lchgrp" : "bad"; echo ":"; +echo chown("{missing}", {uid}) ? "bad" : "missing"; echo ":"; +echo chown("{filename}", "__elephc_eval_missing_user__") ? "bad" : "user-false"; echo ":"; +echo chgrp("{filename}", "__elephc_eval_missing_group__") ? "bad" : "group-false"; echo ":"; +echo call_user_func("chgrp", "{filename}", {gid}) ? "callchgrp" : "bad"; echo ":"; +echo call_user_func_array("lchown", ["filename" => "{link}", "user" => {uid}]) ? "arraylchown" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chown"); echo function_exists("chgrp"); echo function_exists("lchown"); +return function_exists("lchgrp");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&link); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&link); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + assert_eq!( + values.output, + "symlink:chown:chgrp:lchown:lchgrp:missing:user-false:group-false:callchgrp:arraylchown:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. +#[test] +fn execute_program_dispatches_touch_builtin() { + let pid = std::process::id(); + let created = format!("elephc_magician_touch_created_{pid}.txt"); + let stamped = format!("elephc_magician_touch_stamped_{pid}.txt"); + let missing = format!("elephc_magician_touch_missing_{pid}/x.txt"); + let source = format!( + r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; +file_put_contents("{stamped}", "x"); +echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; +echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; +echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; +echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; +echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + assert_eq!( + values.output, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_json.rs b/crates/elephc-magician/src/interpreter/tests/builtins_json.rs new file mode 100644 index 0000000000..77f52908cf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_json.rs @@ -0,0 +1,311 @@ +//! Purpose: +//! Interpreter tests for core and JSON builtin dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases assert JSON state, flags, throwable behavior, and callable dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. +#[test] +fn execute_program_dispatches_simple_builtins() { + let program = parse_fragment( + br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; +echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; +echo call_user_func("count", [1, [2]]) . ":"; +echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +return defined("COUNT_RECURSIVE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:6:2:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `count()` dispatches to eval-declared `Countable` objects. +#[test] +fn execute_program_counts_eval_countable_objects() { + let program = parse_fragment( + br#"class EvalCountableBag implements Countable { + private int $n; + public function __construct($n) { $this->n = $n; } + public function count(): int { echo "count:"; return $this->n; } +} +$bag = new EvalCountableBag(4); +echo count($bag); echo ":"; +echo count($bag, COUNT_RECURSIVE); echo ":"; +echo call_user_func_array("count", ["value" => $bag]); +return function_exists("count");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "count:4:count:4:count:4"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. +#[test] +fn execute_program_dispatches_json_encode_builtin() { + let program = parse_fragment( + br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . hex2bin("80") . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; +return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. +#[test] +fn execute_program_dispatches_json_decode_builtin() { + let program = parse_fragment( + br#"echo json_decode("\"hello\"") . ":"; +echo json_decode("42") . ":"; +echo (json_decode("true") ? "T" : "bad") . ":"; +echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; +$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); +echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; +$call = call_user_func("json_decode", "[3,4]"); +echo $call[1] . ":"; +$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +$badJson = "\"a" . hex2bin("80") . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; +return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. +#[test] +fn execute_program_dispatches_json_decode_stdclass_default() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo $object->a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:x:2:3:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_encode()` serializes stdClass dynamic properties. +#[test] +fn execute_program_dispatches_json_encode_stdclass_object() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo json_encode($object) . ":"; +echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; +$empty = json_decode("{}"); +echo json_encode($empty) . ":"; +$empty->a = 7; +echo json_encode($empty); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. +#[test] +fn execute_program_dispatches_json_last_error_builtins() { + let program = parse_fragment( + br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("bad"); +echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; +json_validate("[1]", 1); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"ok\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"a" . chr(10) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"\\uD83D\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"a" . chr(128) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("[0]"); +echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; +return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval JSON throw flags raise catchable Throwable objects. +#[test] +fn execute_program_dispatches_json_throw_on_error() { + let program = parse_fragment( + br#"try { + json_decode("bad", true, 512, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "decode:"; +} +try { + json_encode(INF, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "encode:"; +} +echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +return defined("JSON_THROW_ON_ERROR");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "decode:encode:0:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. +#[test] +fn execute_program_dispatches_json_validate_builtin() { + let program = parse_fragment( + br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; +echo (json_validate("bad") ? "bad" : "N") . ":"; +echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; +echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; +echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; +return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies direct eval builtin calls bind named and unpacked arguments. +#[test] +fn execute_program_dispatches_named_and_spread_builtins() { + let program = parse_fragment( + br#"echo strlen(string: "abcd"); +echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); +return round(precision: 1, num: 3.14);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:Y:Y"); + assert_eq!(values.get(result), FakeValue::Float(3.1)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs b/crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs new file mode 100644 index 0000000000..1bb7073ee6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Interpreter tests for PHP language-construct builtins that are also visible +//! through eval's builtin registry. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `exit` and `die` are probed for visibility only because executing them +//! terminates the current process. + +use super::super::*; +use super::support::*; + +/// Verifies eval language-construct builtins are visible and direct semantics still work. +#[test] +fn execute_program_language_construct_builtins_are_visible() { + let program = parse_fragment( + br#"$x = 1; +$y = 2; +unset($x); +unset($y, $missing); +echo isset($x) ? "bad" : "unset"; echo ":"; +echo isset($y) ? "bad" : "multi"; echo ":"; +echo empty($missing) ? "empty" : "bad"; echo ":"; +echo empty(0) ? "zero" : "bad"; echo ":"; +echo function_exists("isset") ? "I" : "i"; +echo function_exists("empty") ? "E" : "e"; +echo function_exists("unset") ? "U" : "u"; +echo function_exists("exit") ? "X" : "x"; +echo function_exists("die") ? "D" : "d"; +echo is_callable("isset") ? "i" : "!"; +echo is_callable("empty") ? "e" : "!"; +echo is_callable("unset") ? "u" : "!"; +echo is_callable("exit") ? "x" : "!"; +return is_callable("die");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "unset:multi:empty:zero:IEUXDieux"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies callable dispatch for safe language-construct builtins uses materialized values. +#[test] +fn execute_program_language_construct_builtins_dispatch_as_callables() { + let program = parse_fragment( + br#"echo call_user_func("isset", 1, null) ? "bad" : "null"; echo ":"; +echo call_user_func("isset", 1, "x") ? "isset" : "bad"; echo ":"; +echo call_user_func("empty", 0) ? "empty" : "bad"; echo ":"; +echo call_user_func_array("empty", ["value" => "x"]) ? "bad" : "filled"; echo ":"; +$result = call_user_func("unset", 1); +echo is_null($result) ? "unset-null" : "bad"; echo ":"; +echo call_user_func_array("isset", ["var" => 1, "vars" => "x"]) ? "named" : "bad";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "null:isset:empty:filled:unset-null:named"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies `isset()` without operands is rejected like the main compiler. +#[test] +fn execute_program_isset_without_arguments_fails() { + let program = parse_fragment(br#"isset();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("isset arity fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs b/crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs new file mode 100644 index 0000000000..768c9dfd30 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs @@ -0,0 +1,323 @@ +//! Purpose: +//! Interpreter tests for math and formatting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover numeric hooks, printf-family formatting, min/max, clamp, and constants. + +use super::super::*; +use super::support::*; + +/// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. +#[test] +fn execute_program_dispatches_abs_builtin() { + let program = parse_fragment( + br#"echo abs(-5); echo ":"; +echo abs(-2.5); echo ":"; +echo gettype(abs(-2.5)); echo ":"; +echo call_user_func("abs", -7); echo ":"; +echo call_user_func_array("abs", [-9]); +return function_exists("abs");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:2.5:double:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. +#[test] +fn execute_program_dispatches_floor_and_ceil_builtins() { + let program = parse_fragment( + br#"echo floor(3.7); echo ":"; +echo gettype(floor(3)); echo ":"; +echo ceil(3.2); echo ":"; +echo gettype(ceil(3)); echo ":"; +echo call_user_func("floor", 4.9); echo ":"; +echo call_user_func_array("ceil", [4.1]); +echo ":"; echo function_exists("floor"); +return function_exists("ceil");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:double:4:double:4:5:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. +#[test] +fn execute_program_dispatches_float_binary_builtins() { + let program = parse_fragment( + br#"echo round(fdiv(10, 4), 2); echo ":"; +echo gettype(fdiv(10, 4)); echo ":"; +echo round(fmod(10.5, 3.2), 1); echo ":"; +echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; +echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; +echo function_exists("fdiv"); +return function_exists("fmod");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. +#[test] +fn execute_program_dispatches_extended_math_builtins() { + let program = parse_fragment( + br#"echo sin(0); echo ":"; +echo cos(0); echo ":"; +echo tan(0); echo ":"; +echo round(asin(1), 2); echo ":"; +echo acos(1); echo ":"; +echo round(atan(1), 2); echo ":"; +echo sinh(0); echo ":"; +echo cosh(0); echo ":"; +echo tanh(0); echo ":"; +echo log2(8); echo ":"; +echo log10(100); echo ":"; +echo exp(0); echo ":"; +echo round(deg2rad(180), 2); echo ":"; +echo round(rad2deg(pi()), 0); echo ":"; +echo log(num: 8, base: 2); echo ":"; +echo atan2(y: 0, x: 1); echo ":"; +echo hypot(3, 4); echo ":"; +echo intdiv(7, 2); echo ":"; +echo round(call_user_func("sin", pi() / 2), 0); echo ":"; +echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); +return function_exists("hypot");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. +#[test] +fn execute_program_dispatches_pow_builtin() { + let program = parse_fragment( + br#"echo pow(2, 3); echo ":"; +echo gettype(pow(2, 3)); echo ":"; +echo call_user_func("pow", 2, 5); echo ":"; +echo call_user_func_array("pow", [3, 3]); +return function_exists("pow");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:double:32:27"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `round()` supports default and explicit precision through callable paths. +#[test] +fn execute_program_dispatches_round_builtin() { + let program = parse_fragment( + br#"echo round(3.5); echo ":"; +echo round(3.14159, 2); echo ":"; +echo gettype(round(3)); echo ":"; +echo call_user_func("round", 2.5); echo ":"; +echo call_user_func_array("round", [1.55, 1]); +return function_exists("round");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:3.14:double:3:1.6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `number_format()` groups and rounds numbers through callable paths. +#[test] +fn execute_program_dispatches_number_format_builtin() { + let program = parse_fragment( + br#"echo number_format(1234567); echo ":"; +echo number_format(1234.5678, 2); echo ":"; +echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; +echo number_format(1234567.89, 2, ".", ""); echo ":"; +echo call_user_func("number_format", -1234.5, 1); echo ":"; +echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; +return function_exists("number_format");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval printf-family builtins format, print, and dispatch through callables. +#[test] +fn execute_program_dispatches_printf_family_builtins() { + let program = parse_fragment( + br#"echo sprintf("Hello %s", "World"); echo ":"; +echo sprintf("%05d", 42); echo ":"; +echo sprintf("%.2f", 3.14159); echo ":"; +echo sprintf("%-6s|", "hi"); echo ":"; +$printed = printf("%s=%d", "n", 42); +echo ":" . $printed . ":"; +echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; +$vprinted = vprintf("%s-%d", ["v", 7]); +echo ":" . $vprinted . ":"; +echo call_user_func("sprintf", "%+d", 42); echo ":"; +echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); +return is_callable("vprintf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sscanf()` returns indexed string matches through callable paths. +#[test] +fn execute_program_dispatches_sscanf_builtin() { + let program = parse_fragment( + br#"$result = sscanf("John 1.5 30", "%s %f %d"); +echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; +$named = sscanf(string: "Age: -25", format: "Age: %d"); +echo $named[0] . ":"; +$call = call_user_func("sscanf", "-2.5e3", "%f"); +echo $call[0] . ":"; +$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +return function_exists("sscanf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `min()` and `max()` select numeric values directly and by callable. +#[test] +fn execute_program_dispatches_min_max_builtins() { + let program = parse_fragment( + br#"echo min(3, 1, 2); echo ":"; +echo max(1, 3, 2); echo ":"; +echo min(2.5, 1.5); echo ":"; +echo max(1.5, 2.5); echo ":"; +echo call_user_func("min", 9, 4, 7); echo ":"; +echo call_user_func_array("max", [4, 8, 6]); echo ":"; +echo function_exists("min"); +return function_exists("max");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_clamp_builtin() { + let program = parse_fragment( + br#"echo clamp(5, 0, 10); echo ":"; +echo clamp(15, 0, 10); echo ":"; +echo clamp(-5, 0, 10); echo ":"; +echo clamp(2.75, 1.5, 2.5); echo ":"; +echo clamp(value: 8, min: 0, max: 5); echo ":"; +echo call_user_func("clamp", -1, 0, 10); echo ":"; +echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; +echo function_exists("clamp"); +return is_callable("clamp");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. +#[test] +fn execute_program_rejects_clamp_invalid_bounds() { + let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid clamp bounds should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval `pi()` returns a double constant directly and through callable paths. +#[test] +fn execute_program_dispatches_pi_builtin() { + let program = parse_fragment( + br#"echo round(pi(), 2); echo ":"; +echo gettype(pi()); echo ":"; +echo round(call_user_func("pi"), 3); echo ":"; +echo round(call_user_func_array("pi", []), 4); echo ":"; +return function_exists("pi");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3.14:double:3.142:3.1416:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. +#[test] +fn execute_program_dispatches_sqrt_builtin() { + let program = parse_fragment( + br#"echo sqrt(16); echo ":"; +echo gettype(sqrt(9)); echo ":"; +echo call_user_func("sqrt", 25); echo ":"; +echo call_user_func_array("sqrt", [36]); +return function_exists("sqrt");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:double:5:6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs new file mode 100644 index 0000000000..e517fad9aa --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Interpreter tests for eval process pipe stream builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests use shell commands that exit immediately and clean up their temp file. +//! - `popen()` resources are normal eval streams until `pclose()` waits for the child. + +use super::super::*; +use super::support::*; + +/// Verifies `popen()` and `pclose()` support read/write pipes and dynamic calls. +#[test] +fn execute_program_dispatches_process_pipe_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_popen_{pid}.txt"); + let source = format!( + r#"$h = popen("printf eval-popen", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 64) === "eval-popen" ? "read" : "bad"; echo ":"; +echo pclose($h) === 0 ? "closed" : "bad"; echo ":"; +$w = popen("cat > {file}", "w"); +echo fwrite($w, "pipeout") === 7 ? "write" : "bad"; echo ":"; +echo pclose($w) === 0 ? "wclosed" : "bad"; echo ":"; +echo file_get_contents("{file}") === "pipeout" ? "file" : "bad"; echo ":"; +$call = call_user_func("popen", "printf call-pipe", "r"); +echo stream_get_contents($call) === "call-pipe" ? "callread" : "bad"; echo ":"; +echo call_user_func("pclose", $call) === 0 ? "callclose" : "bad"; echo ":"; +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("popen"); echo function_exists("pclose"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "open:read:closed:write:wclosed:file:callread:callclose:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs b/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs new file mode 100644 index 0000000000..ccd469c797 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Interpreter tests for eval raw pointer and buffer extension builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Buffer tests use the AOT-shaped header pointer and offset by 16 bytes for +//! payload pointer reads and writes. + +use super::super::*; +use super::support::*; + +/// Verifies pointer probes, type sizes, and callable metadata for raw-memory builtins. +#[test] +fn execute_program_dispatches_pointer_null_offset_and_sizeof_builtins() { + let program = parse_fragment( + br#"class EvalPtrSizeBox { + public $x; + public $y; + public static $z; +} +$p = ptr_null(); +echo ptr_is_null($p) ? "N" : "bad"; +echo ":" . ptr_is_null(ptr_offset($p, 0)); +echo ":" . ptr_sizeof("int"); +echo ":" . ptr_sizeof("string"); +echo ":" . ptr_sizeof("ptr"); +echo ":" . ptr_sizeof("EvalPtrSizeBox"); +echo ":" . function_exists("ptr_read16") . function_exists("buffer_new"); +return is_callable("ptr_write_string");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "N:1:8:16:8:40:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval can allocate buffers and use pointer memory helpers on their payload. +#[test] +fn execute_program_dispatches_buffer_and_pointer_memory_builtins() { + let program = parse_fragment( + br#"$buf = buffer_new(4); +$payload = ptr_offset($buf, 16); +echo buffer_len($buf) . ":"; +ptr_set($payload, 123456789); +echo ptr_get($payload) . ":"; +ptr_write8($payload, 255); +ptr_write8(ptr_offset($payload, 1), 1); +echo ptr_read8($payload) . "," . ptr_read8(ptr_offset($payload, 1)) . ":"; +call_user_func_array("ptr_write16", ["pointer" => $payload, "value" => 4660]); +echo ptr_read16($payload) . ":"; +ptr_write32($payload, 305419896); +echo ptr_read32($payload) . ":"; +$written = ptr_write_string($payload, "GET /"); +echo $written . ":" . ptr_read_string($payload, $written) . ":"; +echo strlen(ptr_read_string($payload, 0)); +buffer_free($buf); +echo ":" . (ptr_is_null($buf) ? "freed" : "live"); +return function_exists("buffer_free");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "4:123456789:255,1:4660:305419896:5:GET /:0:freed" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `ptr($var)` remains rejected until eval has an lvalue-aware pointer path. +#[test] +fn execute_program_rejects_ptr_lvalue_builtin_without_storage_address() { + let program = parse_fragment(br#"$value = 1; return ptr($value);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("reject ptr lvalue"); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_readline.rs b/crates/elephc-magician/src/interpreter/tests/builtins_readline.rs new file mode 100644 index 0000000000..455358420e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_readline.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Interpreter tests for eval's `readline()` builtin. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - The test harness runs with stdin at EOF, so `readline()` returns false +//! without blocking for terminal input. + +use super::super::*; +use super::support::*; + +/// Verifies `readline()` reports EOF and participates in callable dispatch. +#[test] +fn execute_program_dispatches_readline_builtin() { + let program = parse_fragment( + br#"echo readline() === false ? "eof" : "bad"; echo ":"; +echo call_user_func("readline") === false ? "call" : "bad"; echo ":"; +echo function_exists("readline"); echo is_callable("readline"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eof:call:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs new file mode 100644 index 0000000000..4e5721bf9e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -0,0 +1,387 @@ +//! Purpose: +//! Interpreter tests for eval-backed ReflectionFunction objects. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Free eval functions retain function and parameter metadata used by +//! ReflectionFunction and ReflectionParameter. + +use super::super::*; +use super::support::*; + +/// Verifies eval-declared functions materialize ReflectionFunction parameter metadata. +#[test] +fn execute_program_reflects_eval_declared_function_parameters() { + let program = parse_fragment( + br#"function eval_reflect_free($left, $right) { return $left; } +$ref = new ReflectionFunction("eval_reflect_free"); +$params = $ref->getParameters(); +echo $ref->getName(); echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +echo count($params); echo ":"; +echo $params[0]->getName(); echo ":"; +echo $params[1]->getPosition(); echo ":"; +$declaring = $params[0]->getDeclaringFunction(); +echo get_class($declaring); echo ":"; +echo $declaring->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "eval_reflect_free:2:2:2:left:1:ReflectionFunction:eval_reflect_free" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared function metadata includes attributes, types, defaults, and flags. +#[test] +fn execute_program_reflects_eval_function_signature_metadata() { + let program = parse_fragment( + br#"class EvalFuncAttr { + public $label; + public function __construct($label) { $this->label = $label; } + public function label() { return $this->label; } +} +#[EvalFuncAttr("free")] +function eval_reflect_rich(#[EvalFuncAttr("first")] string $name, int $count = 3, &...$items) { + return $count; +} +$ref = new ReflectionFunction("eval_reflect_rich"); +$attrs = $ref->getAttributes(); +$params = $ref->getParameters(); +echo count($attrs); echo ":"; +echo $attrs[0]->getName(); echo ":"; +echo $attrs[0]->newInstance()->label(); echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +echo $params[0]->hasType() ? "T" : "t"; echo ":"; +echo $params[0]->getType()->getName(); echo ":"; +$paramAttrs = $params[0]->getAttributes(); +echo count($paramAttrs); echo ":"; +echo $paramAttrs[0]->newInstance()->label(); echo ":"; +echo $params[1]->isOptional() ? "O" : "o"; echo ":"; +echo $params[1]->getDefaultValue(); echo ":"; +echo $params[2]->isVariadic() ? "V" : "v"; echo ":"; +echo $params[2]->isPassedByReference() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalFuncAttr:free:3:1:T:string:1:first:O:3:V:R" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction exposes eval-declared return type metadata. +#[test] +fn execute_program_reflection_function_reports_return_type_metadata() { + let program = parse_fragment( + br#"function eval_reflect_return_named(): ?int { return 1; } +function eval_reflect_return_union(): int|string { return 1; } +function eval_reflect_return_never(): never { throw new Exception("stop"); } +function eval_reflect_return_plain() {} +$namedRef = new ReflectionFunction("eval_reflect_return_named"); +$named = $namedRef->getReturnType(); +echo $namedRef->hasReturnType() ? "T" : "t"; echo ":"; +echo $named->getName(); echo ":"; +echo $named->allowsNull() ? "N" : "n"; echo ":"; +echo $named->isBuiltin() ? "B" : "b"; echo ":"; +$union = (new ReflectionFunction("eval_reflect_return_union"))->getReturnType(); +echo count($union->getTypes()); echo ":"; +foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; +} +echo ":"; +$never = (new ReflectionFunction("eval_reflect_return_never"))->getReturnType(); +echo $never->getName(); echo ":"; +echo $never->allowsNull() ? "N" : "n"; echo ":"; +echo $never->isBuiltin() ? "B" : "b"; echo ":"; +$plain = new ReflectionFunction("eval_reflect_return_plain"); +echo $plain->hasReturnType() ? "P" : "p"; echo ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "T:int:N:B:2:intBstringB:never:n:B:p:Q"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction formats retained eval function metadata through `__toString()`. +#[test] +fn execute_program_reflection_function_to_string() { + let program = parse_fragment( + br#"function eval_reflect_string(string $name, int $count = 3, &...$items): ?string { + return $name; +} +$ref = new ReflectionFunction("eval_reflect_string"); +echo str_replace("\n", "|", $ref->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Function [ function eval_reflect_string ] {| - Parameters [3] {| Parameter #0 [ string $name ]| Parameter #1 [ int $count = 3 ]| Parameter #2 [ &...$items ]| }| - Return [ ?string ]|}|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction origin metadata APIs report eval user-defined defaults. +#[test] +fn execute_program_reflection_function_reports_origin_metadata_defaults() { + let program = parse_fragment( + br#"function eval_reflect_origin_defaults() {} +$ref = new ReflectionFunction("eval_reflect_origin_defaults"); +echo ($ref->getDocComment() === false) ? "D" : "d"; echo ":"; +echo ($ref->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($ref->getExtension() === null) ? "X" : "x"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:E:X"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction derives `isDeprecated()` from eval-retained attributes. +#[test] +fn execute_program_reflection_function_reports_deprecated_attribute() { + let program = parse_fragment( + br#"#[Deprecated] +function eval_reflect_deprecated_function() {} +function eval_reflect_plain_function() {} +$deprecated = new ReflectionFunction("eval_reflect_deprecated_function"); +$plain = new ReflectionFunction("eval_reflect_plain_function"); +echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; +echo $plain->isDeprecated() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:d"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction exposes PHP-compatible name and origin predicate metadata. +#[test] +fn execute_program_reflection_function_reports_name_and_origin_predicates() { + let program = parse_fragment( + br#"namespace EvalReflectFnNs; +function sample(...$items) {} +$ref = new \ReflectionFunction('EvalReflectFnNs\\sample'); +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; echo ":"; +echo $ref->isInternal() ? "I" : "i"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $ref->returnsReference() ? "R" : "r"; echo ":"; +echo $ref->hasReturnType() ? "T" : "t"; echo ":"; +echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; +echo $ref->isGenerator() ? "G" : "g"; echo ":"; +echo $ref->isVariadic() ? "V" : "v"; echo ":"; +echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; +echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; +echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; +echo $ref->isDisabled() ? "X" : "x"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "sample:EvalReflectFnNs:Y:iU:a:c:d:s:r:t:N:g:V:h:Q:T:S:L:x" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction recognizes eval closure literals and dispatches them. +#[test] +fn execute_program_reflection_function_supports_eval_closure_literals() { + let program = parse_fragment( + br#"$seed = 4; +$fn = function($delta = 1) use ($seed) { return $seed + $delta; }; +$ref = new ReflectionFunction($fn); +$staticFn = static function() {}; +$staticRef = new ReflectionFunction($staticFn); +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $staticRef->isClosure() ? "C" : "c"; echo ":"; +echo $staticRef->isStatic() ? "S" : "s"; echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +$vars = $ref->getClosureUsedVariables(); +echo count($vars); echo ":"; +echo $vars["seed"]; echo ":"; +echo $ref->invoke(3); echo ":"; +echo $ref->invokeArgs(["delta" => 5]); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "C:A:U:s:C:S:1:0:1:4:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction reports eval source file and line metadata. +#[test] +fn execute_program_reflection_function_reports_source_location() { + let program = parse_fragment( + br#"function eval_reflect_source_fn() { + return 1; +} +$ref = new ReflectionFunction("eval_reflect_source_fn"); +echo $ref->getFileName(); echo ":"; +echo $ref->getStartLine(); echo ":"; +echo $ref->getEndLine(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/eval-source.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "/tmp/eval-source.php(17) : eval()'d code:1:3"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction exposes eval static locals before and after execution. +#[test] +fn execute_program_reflection_function_reports_static_variables() { + let program = parse_fragment( + br#"function eval_reflect_static_vars() { + static $count = 1; + static $label = "fn"; + $count = $count + 1; + return $count; +} +$ref = new ReflectionFunction("eval_reflect_static_vars"); +$before = $ref->getStaticVariables(); +echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; +echo eval_reflect_static_vars(); echo ":"; +$after = $ref->getStaticVariables(); +echo $after["count"]; echo ":"; echo $after["label"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:fn:2:2:fn"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. +#[test] +fn execute_program_calls_eval_function_with_rich_argument_binding() { + let program = parse_fragment( + br#"function eval_signature_call(string $name, &$value, int $count = 2, ...$rest) { + $value = $value + $count; + echo $name; echo ":"; + echo $count; echo ":"; + echo count($rest); echo ":"; +} +function eval_signature_array(string $name, int $count = 2, ...$rest) { + echo $name; echo ":"; + echo $count; echo ":"; + echo count($rest); echo ":"; + echo $rest["extra"]; +} +$seed = 4; +eval_signature_call(name: "ok", value: $seed, extra: "z"); +echo $seed; echo ":"; +call_user_func_array("eval_signature_array", ["extra" => "z", "name" => "cb"]); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ok:2:1:6:cb:2:1:z"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionFunction invocation dispatches eval functions with forwarded arguments. +#[test] +fn execute_program_reflection_function_invokes_eval_function() { + let program = parse_fragment( + br#"function eval_reflect_invoke($left = "A", $right = "B", ...$rest) { + return $left . $right . count($rest) . $rest["extra"]; +} +function eval_reflect_no_writeback(&$value) { + $value = $value . "!"; + return $value; +} +$ref = new ReflectionFunction("eval_reflect_invoke"); +echo $ref->invoke(right: "2", left: "1", extra: "X"); echo ":"; +echo $ref->invokeArgs(["extra" => "Y", "left" => "3", "right" => "4"]); echo ":"; +$value = "Q"; +$mutate = new ReflectionFunction("eval_reflect_no_writeback"); +echo $mutate->invoke($value); echo ":"; echo $value; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "121X:341Y:Q!:Q"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs new file mode 100644 index 0000000000..64d7ecf487 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs @@ -0,0 +1,331 @@ +//! Purpose: +//! Interpreter tests for scalar type, resource, cast, and class-introspection builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases check runtime tag inspection and mutating scalar conversions. + +use super::super::*; +use super::support::*; + +/// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. +#[test] +fn execute_program_dispatches_type_predicate_builtins() { + let program = parse_fragment( + br#"class EvalPredicateIterator implements Iterator { + public function current() { return null; } + public function key() { return null; } + public function next() {} + public function valid() { return false; } + public function rewind() {} +} +$iterator = new EvalPredicateIterator(); +echo is_int(1); echo is_integer(1); echo is_long(1); +echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); +echo is_string("x"); echo is_bool(false); echo is_null(null); +echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable($iterator) ? "I" : "bad"; +echo is_iterable($object) ? "bad" : "s"; +echo is_iterable(1) ? "bad" : "T"; +echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; +echo is_resource(1) ? "bad" : "R"; +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; +echo ":"; echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func("is_iterable", $iterator) ? "C" : "bad"; +echo call_user_func_array("is_iterable", ["value" => $iterator]) ? "D" : "bad"; +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; +echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); +echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); +echo function_exists("is_iterable"); +return function_exists("is_infinite");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1111111111111IsTok11111NBROoNIiFf:1111CDtOo1111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `is_scalar()` matches PHP scalar-tag membership directly and by callable. +#[test] +fn execute_program_dispatches_is_scalar_builtin() { + let program = parse_fragment( + br#"echo is_scalar(1) ? "i" : "bad"; +echo is_scalar(1.5) ? "f" : "bad"; +echo is_scalar("x") ? "s" : "bad"; +echo is_scalar(false) ? "b" : "bad"; +echo is_scalar(null) ? "bad" : "n"; +echo is_scalar([1]) ? "bad" : "a"; +echo is_scalar($object) ? "bad" : "o"; +echo ":"; +echo call_user_func("is_scalar", "x") ? "call" : "bad"; +echo ":"; +return call_user_func_array("is_scalar", ["value" => [1]]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ifsbnao:call:"); + assert_eq!(values.get(result), FakeValue::Bool(false)); +} +/// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. +#[test] +fn execute_program_dispatches_is_resource_true() { + let program = parse_fragment( + br#"echo is_resource($handle) ? "R" : "bad"; +echo ":" . gettype($handle); +return call_user_func("is_resource", $handle);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "R:resource"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval resource introspection builtins expose stream type and one-based id. +#[test] +fn execute_program_dispatches_resource_introspection_builtins() { + let program = parse_fragment( + br#"echo get_resource_type($handle); +echo ":" . get_resource_id($handle); +echo ":" . call_user_func("get_resource_type", $handle); +echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); +echo ":" . function_exists("get_resource_type"); +return function_exists("get_resource_id");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stream:7:stream:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval cast builtins return boxed scalar cells directly and by callable. +#[test] +fn execute_program_dispatches_cast_builtins() { + let program = parse_fragment( + br#"echo intval("42"); echo ":"; +echo floatval("3.5"); echo ":"; +echo strval(12); echo ":"; +echo boolval("0") ? "bad" : "false"; +echo ":"; echo (string)12; +echo ":"; echo (int)"9"; +echo ":"; echo (float)"3.5"; +echo ":"; echo (bool)"0" ? "bad" : "false"; +echo ":"; echo call_user_func("strval", 7); +return call_user_func_array("intval", ["9"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "42:3.5:12:false:12:9:3.5:false:7"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. +#[test] +fn execute_program_dispatches_settype_builtin() { + let program = parse_fragment( + br#"$x = 42; +echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; +echo ":"; +$y = "0"; +echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; +echo ":"; +echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; +echo ":"; +$z = 3.8; +echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; +echo ":"; +$items = ["k" => "6"]; +echo settype($items["k"], "integer") ? gettype($items["k"]) . ":" . $items["k"] : "bad"; +echo ":"; +class EvalSettypePropertyBox { + public $value = 42; + public static $staticValue = "5"; +} +$box = new EvalSettypePropertyBox(); +echo settype($box->value, "string") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +$name = "value"; +echo settype($box->{$name}, "integer") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +echo settype(EvalSettypePropertyBox::$staticValue, "integer") ? gettype(EvalSettypePropertyBox::$staticValue) . ":" . EvalSettypePropertyBox::$staticValue : "bad"; +echo ":"; +$class = "EvalSettypePropertyBox"; +$staticName = "staticValue"; +echo settype($class::${$staticName}, "bool") ? gettype(EvalSettypePropertyBox::$staticValue) . ":" . (EvalSettypePropertyBox::$staticValue ? "true" : "false") : "bad"; +echo ":"; +return function_exists("settype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "string:42:boolean:false:integer:0:double:3.8:integer:6:string:42:integer:42:integer:5:boolean:true:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + ["settype(): Argument #1 ($var) must be passed by reference, value given"] + ); +} +/// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. +#[test] +fn execute_program_dispatches_gettype_builtin() { + let program = parse_fragment( + br#"echo gettype(1); echo ":"; +echo gettype(1.5); echo ":"; +echo gettype("x"); echo ":"; +echo gettype(false); echo ":"; +echo gettype(null); echo ":"; +echo gettype([1]); echo ":"; +echo gettype(["a" => 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +return function_exists("gettype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "integer:double:string:boolean:NULL:array:array:boolean:NULL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `get_class()` reads object class names directly and by callable. +#[test] +fn execute_program_dispatches_get_class_builtin() { + let program = parse_fragment( + br#"echo get_class($object); echo ":"; +echo call_user_func("get_class", $object); echo ":"; +return call_user_func_array("get_class", ["object" => $object]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stdClass:stdClass:"); + assert_eq!( + values.get(result), + FakeValue::String("stdClass".to_string()) + ); +} + +/// Verifies eval no-arg `get_class()` and `get_parent_class()` read method class scope. +#[test] +fn execute_program_dispatches_no_arg_class_name_builtins() { + let program = parse_fragment( + br#"class EvalNoArgClassParent {} +class EvalNoArgClassBase extends EvalNoArgClassParent { + public function inherited() { + return get_class() . ":" . get_parent_class(); + } + public function inheritedCallable() { + return call_user_func("get_class") . ":" . call_user_func_array("get_parent_class", []); + } +} +class EvalNoArgClassChild extends EvalNoArgClassBase { + public function own() { + return get_class() . ":" . get_parent_class(); + } +} +$child = new EvalNoArgClassChild(); +echo $child->inherited(); echo ":"; +echo $child->inheritedCallable(); echo ":"; +echo $child->own(); echo ":"; +try { + get_class(); +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; +} +return get_parent_class();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalNoArgClassBase:EvalNoArgClassParent:EvalNoArgClassBase:EvalNoArgClassParent:EvalNoArgClassChild:EvalNoArgClassBase:Error:get_class() without arguments must be called from within a class:" + ); + assert_eq!(values.get(result), FakeValue::String(String::new())); +} +/// Verifies eval `get_parent_class()` reads object and class-string parents by callable. +#[test] +fn execute_program_dispatches_get_parent_class_builtin() { + let program = parse_fragment( + br#"echo get_parent_class($object); echo ":"; +echo get_parent_class("ChildClass"); echo ":"; +echo call_user_func("get_parent_class", $object); echo ":"; +return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); + assert_eq!( + values.get(result), + FakeValue::String("ParentClass".to_string()) + ); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs b/crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs new file mode 100644 index 0000000000..4e789b9cf7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Interpreter tests for eval SPL autoload helper builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Autoload registration/call helpers mirror the main backend's conservative stubs. +//! - `spl_autoload_extensions()` persists an eval-local extension string. + +use super::super::*; +use super::support::*; + +/// Verifies eval SPL autoload helpers expose stubbed behavior and extension state. +#[test] +fn execute_program_dispatches_spl_autoload_builtins() { + let program = parse_fragment( + br#"echo spl_autoload_extensions() === ".inc,.php" ? "default" : "bad"; echo ":"; +echo spl_autoload_extensions(".php,.inc") === ".php,.inc" ? "set" : "bad"; echo ":"; +echo spl_autoload_extensions(null) === ".php,.inc" ? "read" : "bad"; echo ":"; +echo spl_autoload_register("missing_loader") ? "register" : "bad"; echo ":"; +echo spl_autoload_unregister("missing_loader") ? "unregister" : "bad"; echo ":"; +$funcs = spl_autoload_functions(); +echo is_array($funcs) && count($funcs) === 0 ? "functions" : "bad"; echo ":"; +echo spl_autoload("MissingClass") === null ? "autoload" : "bad"; echo ":"; +echo spl_autoload_call("MissingClass") === null ? "call" : "bad"; echo ":"; +echo call_user_func("spl_autoload_register", "missing_loader") ? "callregister" : "bad"; echo ":"; +$named = call_user_func_array("spl_autoload_extensions", ["file_extensions" => ".class.php"]); +echo $named === ".class.php" ? "namedext" : "bad"; echo ":"; +echo function_exists("spl_autoload"); echo function_exists("spl_autoload_call"); +echo function_exists("spl_autoload_extensions"); echo function_exists("spl_autoload_functions"); +echo function_exists("spl_autoload_register"); echo function_exists("spl_autoload_unregister"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "default:set:read:register:unregister:functions:autoload:call:", + "callregister:namedext:111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs new file mode 100644 index 0000000000..5869b223da --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Interpreter tests for eval stream context metadata builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Context resources are eval-owned resource cells with inspectable options. +//! - Params currently mirror the main backend's empty-array behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval stream context builtins create resources and persist options. +#[test] +fn execute_program_dispatches_stream_context_builtins() { + let program = parse_fragment( + br#"$ctx = stream_context_create(["http" => ["method" => "POST"]]); +echo is_resource($ctx) ? "ctx" : "bad"; echo ":"; +echo get_resource_type($ctx) === "stream" ? "rtype" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["http"]["method"] === "POST" ? "initial" : "bad"; echo ":"; +echo stream_context_set_option($ctx, "http", "header", "X-Test: 1") ? "setone" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["http"]["header"] === "X-Test: 1" ? "gotone" : "bad"; echo ":"; +echo stream_context_set_option($ctx, ["ssl" => ["verify_peer" => false]]) ? "setall" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["ssl"]["verify_peer"] === false ? "gotall" : "bad"; echo ":"; +echo stream_context_set_params($ctx, ["notification" => "noop"]) ? "paramsset" : "bad"; echo ":"; +$params = stream_context_get_params($ctx); +echo count($params) === 0 ? "params" : "bad"; echo ":"; +$default = stream_context_get_default(); +echo is_resource($default) ? "default" : "bad"; echo ":"; +$set_default = stream_context_set_default(["http" => ["timeout" => "1"]]); +echo is_resource($set_default) ? "setdefault" : "bad"; echo ":"; +$call = call_user_func_array("stream_context_create", ["options" => ["ftp" => ["user" => "u"]]]); +$call_opts = call_user_func("stream_context_get_options", $call); +echo $call_opts["ftp"]["user"] === "u" ? "callcreate" : "bad"; echo ":"; +echo call_user_func("stream_context_set_option", $call, "ftp", "mode", "passive") ? "callset" : "bad"; echo ":"; +$call_opts = call_user_func("stream_context_get_options", $call); +echo $call_opts["ftp"]["mode"] === "passive" ? "callgot" : "bad"; echo ":"; +echo function_exists("stream_context_create"); echo function_exists("stream_context_get_default"); +echo function_exists("stream_context_set_default"); echo function_exists("stream_context_set_option"); +echo function_exists("stream_context_set_params"); echo function_exists("stream_context_get_options"); +echo function_exists("stream_context_get_params"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "ctx:rtype:initial:setone:gotone:setall:gotall:paramsset:params:", + "default:setdefault:callcreate:callset:callgot:1111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs new file mode 100644 index 0000000000..387514fb48 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Interpreter tests for eval stream wrapper, filter, and bucket helper builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Filter resources are eval-local handles and do not transform stream bytes. +//! - Buckets use stdClass properties so user-filter style code can inspect them. + +use super::super::*; +use super::support::*; + +/// Verifies stream wrapper/filter/bucket helper builtins are callable in eval. +#[test] +fn execute_program_dispatches_stream_extension_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_extensions_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "abc"); +$h = fopen("{file}", "r+"); +echo stream_wrapper_register("evaltest", "stdClass") ? "wreg" : "bad"; echo ":"; +echo stream_wrapper_unregister("evaltest") ? "wunreg" : "bad"; echo ":"; +echo stream_wrapper_restore("evaltest") ? "wrestore" : "bad"; echo ":"; +echo stream_filter_register("eval.filter", "stdClass") ? "freg" : "bad"; echo ":"; +$filter = stream_filter_append($h, "string.toupper"); +echo is_resource($filter) ? "fappend" : "bad"; echo ":"; +echo stream_filter_remove($filter) ? "fremove" : "bad"; echo ":"; +$filter = call_user_func("stream_filter_prepend", $h, "string.tolower"); +echo is_resource($filter) ? "fprepend" : "bad"; echo ":"; +echo call_user_func("stream_filter_remove", $filter) ? "fcallremove" : "bad"; echo ":"; +$bucket = stream_bucket_new($h, "payload"); +echo is_object($bucket) && $bucket->data === "payload" && $bucket->datalen === 7 ? "bucket" : "bad"; echo ":"; +$brigade = new stdClass(); +stream_bucket_append($brigade, $bucket); +$out = stream_bucket_make_writeable($brigade); +echo is_object($out) && $out->data === "payload" ? "make" : "bad"; echo ":"; +$brigade2 = new stdClass(); +$first = stream_bucket_new($h, "first"); +$second = stream_bucket_new($h, "second"); +stream_bucket_append($brigade2, $second); +stream_bucket_prepend($brigade2, $first); +$out = stream_bucket_make_writeable($brigade2); +echo is_object($out) && $out->data === "first" ? "prepend" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stream_bucket_append"); echo function_exists("stream_bucket_make_writeable"); +echo function_exists("stream_bucket_new"); echo function_exists("stream_bucket_prepend"); +echo function_exists("stream_filter_append"); echo function_exists("stream_filter_prepend"); +echo function_exists("stream_filter_register"); echo function_exists("stream_filter_remove"); +echo function_exists("stream_wrapper_register"); echo function_exists("stream_wrapper_restore"); +echo function_exists("stream_wrapper_unregister"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + concat!( + "wreg:wunreg:wrestore:freg:fappend:fremove:fprepend:fcallremove:", + "bucket:make:prepend:cleanup:11111111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs new file mode 100644 index 0000000000..35437a5806 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Interpreter tests for eval stream descriptor setting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Local file streams expose terminal/blocking probes through host libc. +//! - Timeout support currently returns false for regular files, matching the +//! socket-only behavior of the main backend. + +use super::super::*; +use super::support::*; + +/// Verifies eval stream setting builtins work directly and through dynamic calls. +#[test] +fn execute_program_dispatches_stream_setting_builtins() { + let pid = std::process::id(); + let file = format!("elephc_magician_stream_settings_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "x"); +$h = fopen("{file}", "r+"); +echo stream_isatty($h) ? "bad" : "notty"; echo ":"; +echo stream_set_blocking($h, false) ? "nonblock" : "bad"; echo ":"; +echo stream_set_blocking($h, true) ? "block" : "bad"; echo ":"; +echo stream_set_chunk_size($h, 1024) === 8192 ? "chunk1" : "bad"; echo ":"; +echo stream_set_chunk_size($h, 2048) === 1024 ? "chunk2" : "bad"; echo ":"; +echo stream_set_read_buffer($h, 0) === 0 ? "readbuf" : "bad"; echo ":"; +echo stream_set_write_buffer($h, 0) === 0 ? "writebuf" : "bad"; echo ":"; +echo stream_set_timeout($h, 1) === false ? "notimeout" : "bad"; echo ":"; +echo call_user_func("stream_isatty", $h) === false ? "calltty" : "bad"; echo ":"; +echo call_user_func("stream_set_chunk_size", $h, 4096) === 2048 ? "callchunk" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stream_isatty"); echo function_exists("stream_set_blocking"); +echo function_exists("stream_set_chunk_size"); echo function_exists("stream_set_read_buffer"); +echo function_exists("stream_set_timeout"); echo function_exists("stream_set_write_buffer"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + concat!( + "notty:nonblock:block:chunk1:chunk2:readbuf:writebuf:notimeout:", + "calltty:callchunk:cleanup:111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs new file mode 100644 index 0000000000..f8b9a11e58 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! Interpreter tests for eval TCP stream socket builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests bind localhost port 0 so the OS chooses available ports. +//! - Socket resources are then exercised through regular eval stream reads/writes. + +use super::super::*; +use super::support::*; + +/// Verifies stream socket helpers support local TCP and socket pairs. +#[test] +fn execute_program_dispatches_stream_socket_builtins() { + let program = parse_fragment( + br#"$server = stream_socket_server("tcp://127.0.0.1:0"); +echo is_resource($server) ? "server" : "bad"; echo ":"; +$addr = stream_socket_get_name($server, false); +echo $addr !== false ? "name" : "bad"; echo ":"; +$client = stream_socket_client("tcp://" . $addr); +echo is_resource($client) ? "client" : "bad"; echo ":"; +$peerName = ""; +$peer = stream_socket_accept($server, null, $peerName); +echo is_resource($peer) ? "accept" : "bad"; echo ":"; +echo $peerName !== "" ? "peerout" : "bad"; echo ":"; +echo stream_socket_get_name($client, true) !== false ? "peername" : "bad"; echo ":"; +echo stream_socket_sendto($client, "ping") === 4 ? "send" : "bad"; echo ":"; +$remoteAddr = ""; +echo stream_socket_recvfrom($peer, 4, 0, $remoteAddr) === "ping" ? "recv" : "bad"; echo ":"; +echo $remoteAddr !== "" ? "addrout" : "bad"; echo ":"; +fwrite($peer, "pong"); +echo fread($client, 4) === "pong" ? "roundtrip" : "bad"; echo ":"; +echo stream_socket_enable_crypto($client, false) ? "cryptooff" : "bad"; echo ":"; +echo stream_socket_shutdown($client, 2) ? "shutdown" : "bad"; echo ":"; +fclose($peer); fclose($client); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); +$parts = explode(":", $addr); +$errno = 9; $errstr = "x"; +$fs = fsockopen("127.0.0.1", intval($parts[1]), $errno, $errstr); +$peer = stream_socket_accept($server); +echo is_resource($fs) && is_resource($peer) && $errno === 0 && $errstr === "" ? "fsock" : "bad"; echo ":"; +fclose($fs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = call_user_func("stream_socket_get_name", $server, false); +$parts = explode(":", $addr); +$perrno = 9; $perrstr = "x"; +$pfs = pfsockopen("127.0.0.1", intval($parts[1]), $perrno, $perrstr); +$peer = stream_socket_accept($server); +echo is_resource($pfs) && is_resource($peer) && $perrno === 0 && $perrstr === "" ? "pfsock" : "bad"; echo ":"; +fclose($pfs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); +$parts = explode(":", $addr); +$open = "fsockopen"; +$dynErrno = 9; $dynErrstr = "x"; +$fs = $open("127.0.0.1", intval($parts[1]), $dynErrno, $dynErrstr); +$peer = stream_socket_accept($server); +echo is_resource($fs) && is_resource($peer) && $dynErrno === 0 && $dynErrstr === "" ? "dynfsock" : "bad"; echo ":"; +fclose($fs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); +$accept = "stream_socket_accept"; +$client = stream_socket_client("tcp://" . $addr); +$dynPeer = ""; +$peer = $accept($server, null, $dynPeer); +echo is_resource($peer) && $dynPeer !== "" ? "dynaccept" : "bad"; echo ":"; +fwrite($client, "call"); +$recv = stream_socket_recvfrom(...); +$dynAddr = ""; +echo $recv($peer, 4, 0, $dynAddr) === "call" && $dynAddr !== "" ? "dynrecv" : "bad"; echo ":"; +fclose($client); fclose($peer); fclose($server); +$pair = stream_socket_pair(1, 1, 0); +echo is_array($pair) && is_resource($pair[0]) && is_resource($pair[1]) ? "pair" : "bad"; echo ":"; +fwrite($pair[0], "xy"); +echo fread($pair[1], 2) === "xy" ? "pairio" : "bad"; echo ":"; +$read = [$pair[1]]; $write = [$pair[0]]; $except = [$pair[0]]; +echo stream_select($read, $write, $except, 0) === 0 && count($read) === 0 && count($write) === 0 && count($except) === 0 ? "selectclear" : "bad"; echo ":"; +$read = [$pair[1]]; $write = []; $except = []; +$select = "stream_select"; +echo $select($read, $write, $except, 0) === 0 && count($read) === 0 ? "dynselect" : "bad"; echo ":"; +fclose($pair[0]); fclose($pair[1]); +$read = []; $write = []; $except = []; +echo stream_select($read, $write, $except, 0) === 0 ? "select" : "bad"; echo ":"; +echo function_exists("fsockopen"); echo function_exists("pfsockopen"); +echo function_exists("stream_select"); echo function_exists("stream_socket_accept"); +echo function_exists("stream_socket_client"); echo function_exists("stream_socket_enable_crypto"); +echo function_exists("stream_socket_get_name"); echo function_exists("stream_socket_pair"); +echo function_exists("stream_socket_recvfrom"); echo function_exists("stream_socket_sendto"); +echo function_exists("stream_socket_server"); echo function_exists("stream_socket_shutdown"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "server:name:client:accept:peerout:peername:send:recv:addrout:", + "roundtrip:cryptooff:shutdown:fsock:pfsock:dynfsock:dynaccept:dynrecv:", + "pair:pairio:selectclear:dynselect:select:111111111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs new file mode 100644 index 0000000000..741b72f78c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper `stream_cast()` dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Magician keeps `stream_select()` conservative, but still invokes +//! `stream_cast(STREAM_CAST_FOR_SELECT)` for PHP-observable wrapper effects. + +use super::super::*; +use super::support::*; + +/// Verifies `stream_select()` invokes wrapper `stream_cast()` for each stream array. +#[test] +fn execute_program_dispatches_user_stream_wrapper_cast_for_select() { + let program = parse_fragment( + br#"class EvalCastWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_cast($cast_as) { + echo "cast(" . $cast_as . ")"; + return false; + } +} +stream_wrapper_register("castw", "EvalCastWrapperW"); +$h = fopen("castw://one", "r"); +$read = [$h]; $write = []; $except = []; +echo stream_select($read, $write, $except, 0) === 0 ? "select" : "bad"; echo ":"; +$read = []; $write = [$h]; $except = [$h]; +echo stream_select($read, $write, $except, 0, 0) === 0 ? "select2" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "cast(3)select:cast(3)cast(3)select2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs new file mode 100644 index 0000000000..21c0b710d5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper directory handles. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Wrapper directory resources hold the wrapper object across read, rewind, +//! and close calls so cursor state stays on the userspace instance. + +use super::super::*; +use super::support::*; + +/// Verifies directory stream builtins dispatch to userspace wrapper methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_directories() { + let program = parse_fragment( + br#"class EvalDirWrapperW { + public $entries; + public $pos; + public function dir_opendir($path, $options): bool { + echo "O(" . $path . "," . $options . ")"; + $this->entries = ["one", "two"]; + $this->pos = 0; + return $path === "dirw://ok"; + } + public function dir_readdir(): string { + if ($this->pos >= count($this->entries)) { + return ""; + } + $entry = $this->entries[$this->pos]; + $this->pos += 1; + return $entry; + } + public function dir_rewinddir(): bool { + echo "W"; + $this->pos = 0; + return true; + } + public function dir_closedir(): bool { + echo "C"; + return true; + } +} +stream_wrapper_register("dirw", "EvalDirWrapperW"); +$dh = opendir("dirw://ok"); +echo is_resource($dh) ? "open" : "bad"; echo ":"; +echo readdir($dh) === "one" ? "one" : "bad"; echo ":"; +echo readdir($dh) === "two" ? "two" : "bad"; echo ":"; +echo readdir($dh) === false ? "eof" : "bad"; echo ":"; +rewinddir($dh); +echo readdir($dh) === "one" ? "rewind" : "bad"; echo ":"; +call_user_func("rewinddir", $dh); +echo call_user_func("readdir", $dh) === "one" ? "callread" : "bad"; echo ":"; +call_user_func("closedir", $dh); +echo readdir($dh) === false ? "closed" : "bad"; echo ":"; +echo opendir("dirw://bad") === false ? "openfalse" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(dirw://ok,0)open:one:two:eof:Wrewind:Wcallread:Cclosed:O(dirw://bad,0)openfalse" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs new file mode 100644 index 0000000000..d1b6d1555f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Interpreter tests for one-shot file I/O through userspace stream wrappers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `file_get_contents()`, `file()`, `readfile()`, and `file_put_contents()` +//! should use `stream_open()` plus stream read/write/close methods. + +use super::super::*; +use super::support::*; + +/// Verifies one-shot file I/O builtins dispatch through userspace stream wrappers. +#[test] +fn execute_program_dispatches_user_stream_wrapper_file_io() { + let program = parse_fragment( + br#"class EvalFileIoWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + echo "O(" . $mode . ")"; + $this->data = "aa\nbb"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_write($data): int { + echo "W(" . $data . ")"; + return strlen($data); + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_close(): void { + echo "C"; + } +} +stream_wrapper_register("fio", "EvalFileIoWrapperW"); +echo file_get_contents("fio://read") === "aa\nbb" ? "fgc" : "bad"; echo ":"; +$lines = file("fio://read"); +echo count($lines) === 2 && $lines[0] === "aa\n" && $lines[1] === "bb" ? "file" : "bad"; echo ":"; +echo readfile("fio://read") === 5 ? "readfile" : "bad"; echo ":"; +echo file_put_contents("fio://write", "xyz") === 3 ? "put" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(r)Cfgc:O(r)Cfile:O(r)aa\nbbCreadfile:O(w)W(xyz)Cput" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs new file mode 100644 index 0000000000..c92ba28df2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Interpreter tests for path metadata operations on eval userspace stream wrappers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `stream_metadata()` option/value mapping for path-based +//! filesystem mutation builtins. + +use super::super::*; +use super::support::*; + +/// Verifies path metadata builtins dispatch to wrapper `stream_metadata()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_metadata() { + let program = parse_fragment( + br##"class EvalMetadataWrapperW { + public function stream_metadata($path, $option, $value): bool { + echo $path . "#" . $option . "#"; + if ($option === 1) { + echo $value[0] . "/" . $value[1]; + return true; + } + echo $value; + return true; + } +} +stream_wrapper_register("metaw", "EvalMetadataWrapperW"); +echo chmod("metaw://file", 384) ? "chmod" : "bad"; echo ":"; +echo touch("metaw://file", 100, 200) ? "touch" : "bad"; echo ":"; +echo chown("metaw://file", 501) ? "chown" : "bad"; echo ":"; +echo chgrp("metaw://file", "staff") ? "chgrp" : "bad"; echo ":"; +echo lchown("metaw://file", 501) === false ? "lskip" : "bad"; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "metaw://file#6#384chmod:metaw://file#1#100/200touch:metaw://file#3#501chown:metaw://file#4#staffchgrp:lskip" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs new file mode 100644 index 0000000000..ffecc8396c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper option dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `stream_set_blocking()` and `stream_set_timeout()` map to +//! `stream_set_option($option, $arg1, $arg2)` on wrapper streams. + +use super::super::*; +use super::support::*; + +/// Verifies stream setting builtins dispatch to wrapper `stream_set_option()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_options() { + let program = parse_fragment( + br#"class EvalOptionWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_set_option($option, $arg1, $arg2): bool { + echo "O(" . $option . "," . $arg1 . "," . $arg2 . ")"; + if ($option === 1) { + return $arg1 === 1; + } + if ($option === 4) { + return $arg2 === 7; + } + return false; + } +} +stream_wrapper_register("optw", "EvalOptionWrapperW"); +$h = fopen("optw://one", "r"); +echo stream_set_blocking($h, true) ? "block" : "bad"; echo ":"; +echo stream_set_blocking($h, false) === false ? "nonblockfalse" : "bad"; echo ":"; +echo stream_set_timeout($h, 3, 7) ? "timeout" : "bad"; echo ":"; +echo call_user_func("stream_set_timeout", $h, 5) === false ? "calltimeoutfalse" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(1,1,0)block:O(1,0,0)nonblockfalse:O(4,3,7)timeout:O(4,5,0)calltimeoutfalse" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs new file mode 100644 index 0000000000..58daf971cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper filesystem path operations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Path operations instantiate wrapper objects per operation, matching the +//! generated runtime path-op dispatch instead of open stream resource dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies path mutation builtins dispatch to userspace stream-wrapper methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_path_ops() { + let program = parse_fragment( + br#"class EvalPathOpWrapperW { + public function unlink($path): bool { + echo "U(" . $path . ")"; + return $path === "pathop://delete-ok"; + } + public function mkdir($path, $mode, $options): bool { + echo "M(" . $path . "," . $mode . "," . $options . ")"; + return true; + } + public function rmdir($path, $options): bool { + echo "R(" . $path . "," . $options . ")"; + return false; + } + public function rename($from, $to): bool { + echo "N(" . $from . "," . $to . ")"; + return $to === "pathop://dest"; + } +} +stream_wrapper_register("pathop", "EvalPathOpWrapperW"); +echo unlink("pathop://delete-ok") ? "unlink" : "bad"; echo ":"; +echo call_user_func("unlink", "pathop://delete-no") === false ? "unlinkfalse" : "bad"; echo ":"; +echo mkdir("pathop://dir") ? "mkdir" : "bad"; echo ":"; +echo rmdir("pathop://dir") === false ? "rmdirfalse" : "bad"; echo ":"; +echo rename("pathop://source", "pathop://dest") ? "rename" : "bad"; echo ":"; +echo call_user_func("rename", "pathop://source2", "pathop://dest") ? "callrename" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "U(pathop://delete-ok)unlink:U(pathop://delete-no)unlinkfalse:M(pathop://dir,0,0)mkdir:R(pathop://dir,0)rmdirfalse:N(pathop://source,pathop://dest)rename:N(pathop://source2,pathop://dest)callrename" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs new file mode 100644 index 0000000000..3ae271dc6f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -0,0 +1,473 @@ +//! Purpose: +//! Interpreter tests for eval-supported stream wrapper URL handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - PHAR fixtures are written through `elephc-phar` so tests exercise the same +//! archive bridge used by generated-runtime paths. +//! - HTTP tests use a one-shot localhost server to avoid external network dependencies. + +use super::super::*; +use super::support::*; +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread::JoinHandle; + +/// Verifies eval `fopen()` and one-shot file builtins handle supported wrappers. +#[test] +fn execute_program_dispatches_supported_stream_wrapper_urls() { + let pid = std::process::id(); + let local = format!("elephc_magician_wrapper_local_{pid}.txt"); + let archive = format!("elephc_magician_wrapper_{pid}.phar"); + let read_url = format!("phar://{archive}/dir/read.txt"); + let put_url = format!("phar://{archive}/dir/put.txt"); + let stream_url = format!("phar://{archive}/dir/stream.txt"); + let _ = std::fs::remove_file(&local); + let _ = std::fs::remove_file(&archive); + std::fs::write(&local, b"local").expect("write local wrapper fixture"); + elephc_phar::put_url_bytes(read_url.as_bytes(), b"from-phar") + .expect("write phar wrapper fixture"); + let source = format!( + r#"echo file_get_contents("file://{local}") === "local" ? "fileurl" : "bad"; echo ":"; +$memory = fopen("php://memory", "w+"); +fwrite($memory, "mem"); +rewind($memory); +echo fread($memory, 3) === "mem" ? "memory" : "bad"; echo ":"; +fclose($memory); +$data = fopen("data://text/plain;base64,SGVsbG8=", "r"); +echo fread($data, 5) === "Hello" ? "data" : "bad"; echo ":"; +fclose($data); +$phar = fopen("{read_url}", "r"); +echo fread($phar, 32) === "from-phar" ? "pharopen" : "bad"; echo ":"; +fclose($phar); +echo file_get_contents("{read_url}") === "from-phar" ? "pharget" : "bad"; echo ":"; +echo file_exists("{read_url}") && is_file("{read_url}") && is_readable("{read_url}") ? "pharprobe" : "bad"; echo ":"; +echo filetype("{read_url}") === "file" ? "phartype" : "bad"; echo ":"; +echo filesize("{read_url}") === 9 ? "pharsize" : "bad"; echo ":"; +echo file_put_contents("{put_url}", "put") === 3 ? "pharput" : "bad"; echo ":"; +echo file_get_contents("{put_url}") === "put" ? "putread" : "bad"; echo ":"; +$out = fopen("{stream_url}", "w"); +fwrite($out, "stream"); +echo fclose($out) ? "streamclose" : "bad"; echo ":"; +echo file_get_contents("{stream_url}") === "stream" ? "streamread" : "bad"; echo ":"; +echo unlink("{stream_url}") ? "unlink" : "bad"; echo ":"; +echo file_get_contents("{stream_url}") === false ? "deleted" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&local); + let _ = std::fs::remove_file(&archive); + assert_eq!( + values.output, + "fileurl:memory:data:pharopen:pharget:pharprobe:phartype:pharsize:pharput:putread:streamclose:streamread:unlink:deleted" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `http://` URLs feed `file_get_contents()` and `fopen()`. +#[test] +fn execute_program_dispatches_http_stream_wrapper_urls() { + let (fgc_port, fgc_server) = spawn_http_once("fgc-body"); + let (fopen_port, fopen_server) = spawn_http_once("stream-body"); + let source = format!( + r#"echo file_get_contents("http://127.0.0.1:{fgc_port}/body?x=1") === "fgc-body" ? "fgc" : "bad"; echo ":"; +$h = fopen("http://127.0.0.1:{fopen_port}/stream", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 64) === "stream-body" ? "read" : "bad"; echo ":"; +echo fclose($h) ? "close" : "bad"; echo ":"; +echo file_get_contents("http://") === false ? "invalid" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + fgc_server.join().expect("join file_get_contents HTTP fixture"); + fopen_server.join().expect("join fopen HTTP fixture"); + assert_eq!(values.output, "fgc:open:read:close:invalid"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval stream wrapper registration changes the visible wrapper list. +#[test] +fn execute_program_tracks_stream_wrapper_registry_state() { + let program = parse_fragment( + br#"$before = stream_get_wrappers(); +echo in_array("evaltest", $before) ? "bad" : "missing"; echo ":"; +echo stream_wrapper_register("evaltest", "stdClass") ? "reg" : "bad"; echo ":"; +$after = stream_get_wrappers(); +echo in_array("evaltest", $after) ? "listed" : "bad"; echo ":"; +echo stream_wrapper_unregister("evaltest") ? "unreg" : "bad"; echo ":"; +$removed = call_user_func("stream_get_wrappers"); +echo in_array("evaltest", $removed) ? "bad" : "removed"; echo ":"; +echo stream_wrapper_unregister("file") ? "unfile" : "bad"; echo ":"; +$without_file = stream_get_wrappers(); +echo in_array("file", $without_file) ? "bad" : "nofile"; echo ":"; +echo stream_wrapper_restore("file") ? "restore" : "bad"; echo ":"; +$restored = call_user_func_array("stream_get_wrappers", []); +echo in_array("file", $restored) ? "fileback" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "missing:reg:listed:unreg:removed:unfile:nofile:restore:fileback" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval userspace stream wrappers dispatch core stream methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_methods() { + let program = parse_fragment( + br#"class EvalUserWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdef"; + $this->pos = 0; + $opened_path = "ignored"; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_write($data): int { + echo "[" . $data . "]"; + return strlen($data); + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_close(): void { + echo "C"; + } +} +echo stream_wrapper_register("uw", "EvalUserWrapperW") ? "reg" : "bad"; echo ":"; +$h = fopen("uw://read", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 2); echo ":"; +echo feof($h) ? "bad" : "not"; echo ":"; +echo fread($h, 4); echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +echo fclose($h) ? "closed" : "bad"; echo ":"; +$w = fopen("uw://write", "w"); +echo fwrite($w, "xyz") === 3 ? "wrote" : "bad"; echo ":"; +echo fclose($w) ? "closed2" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "reg:open:ab:not:cdef:eof:Cclosed:[xyz]wrote:Cclosed2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval userspace stream wrappers dispatch stream control methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_control_methods() { + let program = parse_fragment( + br#"class EvalControlWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdef"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_tell(): int { + echo "T"; + return $this->pos; + } + public function stream_seek($offset, $whence): bool { + echo "S" . $whence; + if ($whence !== 0) { return false; } + $this->pos = $offset; + return true; + } + public function stream_truncate($new_size): bool { + echo "R" . $new_size; + $this->data = substr($this->data, 0, $new_size); + if ($this->pos > $new_size) { $this->pos = $new_size; } + return true; + } + public function stream_flush(): bool { + echo "F"; + return true; + } + public function stream_lock($operation): bool { + echo "L" . $operation; + return $operation !== 99; + } +} +stream_wrapper_register("ctrlw", "EvalControlWrapperW"); +$h = fopen("ctrlw://one", "r+"); +echo ftell($h) === 0 ? "tell0" : "bad"; echo ":"; +echo fread($h, 2) === "ab" ? "read" : "bad"; echo ":"; +echo ftell($h) === 2 ? "tell2" : "bad"; echo ":"; +echo fseek($h, 1) === 0 ? "seek" : "bad"; echo ":"; +echo ftell($h) === 1 ? "tell1" : "bad"; echo ":"; +echo ftruncate($h, 3) ? "trunc" : "bad"; echo ":"; +echo stream_get_contents($h) === "bc" ? "contents" : "bad"; echo ":"; +echo fflush($h) ? "flush" : "bad"; echo ":"; +$lock_ok = flock($h, LOCK_EX, $would); +echo $lock_ok && $would === false ? "lock" : "bad"; echo ":"; +echo flock($h, 99) === false ? "lockfalse" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Ttell0:read:Ttell2:S0seek:Ttell1:R3trunc:contents:Fflush:L2lock:L99lockfalse" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies a wrapper whose `stream_open()` returns false makes `fopen()` false. +#[test] +fn execute_program_user_stream_wrapper_open_false_returns_false() { + let program = parse_fragment( + br#"class EvalRejectWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return false; + } +} +stream_wrapper_register("rejectw", "EvalRejectWrapperW"); +echo fopen("rejectw://x", "r") === false ? "false" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "false"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies aggregate stream helpers drain and copy userspace wrapper streams. +#[test] +fn execute_program_dispatches_user_stream_wrapper_aggregate_reads() { + let program = parse_fragment( + br#"class EvalSlowWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdefghi"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $limit = min(2, $count); + $chunk = substr($this->data, $this->pos, $limit); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_seek($offset, $whence): bool { + if ($whence !== 0) { return false; } + $this->pos = $offset; + return true; + } +} +class EvalSinkWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_write($data): int { + echo "<" . $data . ">"; + return strlen($data); + } +} +stream_wrapper_register("sloww", "EvalSlowWrapperW"); +stream_wrapper_register("sinkw", "EvalSinkWrapperW"); +$h = fopen("sloww://read", "r"); +echo stream_get_contents($h, 5) === "abcde" ? "bounded" : "bad"; echo ":"; +echo stream_get_contents($h) === "fghi" ? "rest" : "bad"; echo ":"; +echo stream_get_contents($h, 3, 2) === "cde" ? "offset" : "bad"; echo ":"; +$src = fopen("sloww://copy", "r"); +$dst = fopen("php://memory", "w+"); +echo stream_copy_to_stream($src, $dst, 5) === 5 ? "copy" : "bad"; echo ":"; +rewind($dst); +echo stream_get_contents($dst) === "abcde" ? "copied" : "bad"; echo ":"; +$raw = fopen("php://memory", "w+"); +fwrite($raw, "sinkdata"); +rewind($raw); +$sink = fopen("sinkw://out", "w"); +echo stream_copy_to_stream($raw, $sink) === 8 ? "sinkcopy" : "bad"; echo ":"; +$pass = fopen("sloww://pass", "r"); +echo fpassthru($pass) === 9 ? "pass" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "bounded:rest:offset:copy:copied:sinkcopy:abcdefghipass" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies line-oriented reads dispatch through userspace stream wrappers. +#[test] +fn execute_program_dispatches_user_stream_wrapper_line_reads() { + let program = parse_fragment( + br#"class EvalLineWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "aa\nbbENDcc"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } +} +stream_wrapper_register("linew", "EvalLineWrapperW"); +$h = fopen("linew://one", "r"); +echo fgets($h) === "aa\n" ? "fgets" : "bad"; echo ":"; +echo stream_get_line($h, 10, "END") === "bb" ? "getline" : "bad"; echo ":"; +echo stream_get_contents($h) === "cc" ? "rest" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fgets:getline:rest"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies path-based filesystem builtins dispatch to wrapper `url_stat()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_url_stat() { + let program = parse_fragment( + br#"class EvalUrlStatWrapperW { + public function url_stat($path, $flags) { + if (str_contains($path, "missing")) { + return false; + } + if (str_contains($path, "dir")) { + return ["mode" => 16877, "size" => 0, "mtime" => 5, "uid" => 10]; + } + return [ + "mode" => 33188, + "size" => 123, + "mtime" => 77, + "atime" => 66, + "ctime" => 88, + "uid" => 501, + "gid" => 20, + "ino" => 9 + ]; + } + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_stat() { + return ["mode" => 33188, "size" => 321]; + } +} +stream_wrapper_register("ustat", "EvalUrlStatWrapperW"); +echo file_exists("ustat://file") ? "exists" : "bad"; echo ":"; +echo is_file("ustat://file") ? "file" : "bad"; echo ":"; +echo is_dir("ustat://file") ? "bad" : "notdir"; echo ":"; +echo filetype("ustat://file") === "file" ? "type" : "bad"; echo ":"; +echo filesize("ustat://file") === 123 ? "size" : "bad"; echo ":"; +echo filemtime("ustat://file") === 77 ? "mtime" : "bad"; echo ":"; +echo fileowner("ustat://file") === 501 ? "owner" : "bad"; echo ":"; +$stat = stat("ustat://file"); +echo $stat["size"] === 123 && $stat["mode"] === 33188 ? "stat" : "bad"; echo ":"; +$h = fopen("ustat://file", "r"); +$fstat = fstat($h); +echo $fstat["size"] === 321 && $fstat["mode"] === 33188 ? "fstat" : "bad"; echo ":"; +echo call_user_func("filesize", "ustat://file") === 123 ? "callsize" : "bad"; echo ":"; +echo file_exists("ustat://missing") ? "bad" : "missing"; echo ":"; +echo filetype("ustat://dir") === "dir" ? "dirtype" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "exists:file:notdir:type:size:mtime:owner:stat:fstat:callsize:missing:dirtype" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Starts a localhost HTTP server that returns one fixed body and then exits. +fn spawn_http_once(body: &'static str) -> (u16, JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP wrapper fixture"); + let port = listener + .local_addr() + .expect("read HTTP wrapper fixture address") + .port(); + let handle = std::thread::spawn(move || { + let (mut socket, _) = listener.accept().expect("accept HTTP wrapper request"); + let mut request = [0_u8; 1024]; + let _ = socket.read(&mut request).expect("read HTTP wrapper request"); + let response = format!( + "HTTP/1.0 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + socket + .write_all(response.as_bytes()) + .expect("write HTTP wrapper response"); + }); + (port, handle) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs new file mode 100644 index 0000000000..d425a64573 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs @@ -0,0 +1,290 @@ +//! Purpose: +//! Interpreter tests for binary string conversion and escaping builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover byte-preserving string helpers and base64 conversion. + +use super::super::*; +use super::support::*; + +/// Verifies eval `strrev()` dispatches through direct and callable paths. +#[test] +fn execute_program_dispatches_strrev_builtin() { + let program = parse_fragment( + br#"echo strrev("Hello"); echo ":"; +echo strrev(123); echo ":"; +echo call_user_func("strrev", "ABC"); echo ":"; +echo call_user_func_array("strrev", ["def"]); echo ":"; +return function_exists("strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "olleH:321:CBA:fed:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `grapheme_strrev()` reverses UTF-8 grapheme clusters. +#[test] +fn execute_program_dispatches_grapheme_strrev_builtin() { + let program = parse_fragment( + br#"echo grapheme_strrev("ABCDE"); echo ":"; +echo bin2hex(grapheme_strrev(hex2bin("4165cc8142"))); echo ":"; +echo bin2hex(grapheme_strrev(hex2bin("41f09f91a9f09f8fbde2808df09f92bb42"))); echo ":"; +echo grapheme_strrev(chr(255)) === false ? "false" : "bad"; echo ":"; +echo call_user_func("grapheme_strrev", "xy"); echo ":"; +echo call_user_func_array("grapheme_strrev", ["string" => "pq"]); echo ":"; +return function_exists("grapheme_strrev") && is_callable("grapheme_strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EDCBA:4265cc8141:42f09f91a9f09f8fbde2808df09f92bb41:false:yx:qp:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `chr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_chr_builtin() { + let program = parse_fragment( + br#"echo chr(65); echo ":"; +echo bin2hex(chr(codepoint: 256)); echo ":"; +echo bin2hex(call_user_func("chr", 257)); echo ":"; +echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; +return function_exists("chr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:00:01:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval gzip/zlib builtins round-trip strings and return false on invalid data. +#[test] +fn execute_program_dispatches_gzip_builtins() { + let program = parse_fragment( + br#"$data = "hello\0world"; +$zlib = gzcompress($data); +echo gzuncompress($zlib) === $data ? "zc" : "bad"; echo ":"; +$raw = gzdeflate($data); +echo gzinflate($raw) === $data ? "df" : "bad"; echo ":"; +echo gzuncompress("not zlib") === false ? "bad-zlib" : "bad"; echo ":"; +echo gzinflate("not raw deflate") === false ? "bad-raw" : "bad"; echo ":"; +echo gzuncompress(gzcompress(data: "abc", level: 1)); echo ":"; +echo call_user_func("gzuncompress", call_user_func("gzcompress", "call")); echo ":"; +echo call_user_func_array("gzinflate", ["data" => call_user_func_array("gzdeflate", ["data" => "spread", "level" => 1])]); echo ":"; +echo function_exists("gzcompress"); +echo function_exists("gzdeflate"); +echo function_exists("gzinflate"); +return is_callable("gzuncompress");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "zc:df:bad-zlib:bad-raw:abc:call:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_str_repeat_builtin() { + let program = parse_fragment( + br#"echo str_repeat("ha", 3); echo ":"; +echo strlen(str_repeat(string: "x", times: 0)); echo ":"; +echo call_user_func("str_repeat", "ab", 2); echo ":"; +echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; +return function_exists("str_repeat");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hahaha:0:abab:zzz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `substr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_builtin() { + let program = parse_fragment( + br#"echo substr("abcdef", 2); echo ":"; +echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; +echo substr("abcdef", -2); echo ":"; +echo call_user_func("substr", "abcdef", 2, -2); echo ":"; +echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; +return function_exists("substr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_replace_builtin() { + let program = parse_fragment( + br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; +echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; +echo substr_replace("abcdef", "X", -2); echo ":"; +echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; +echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; +return function_exists("substr_replace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_nl2br_builtin() { + let program = parse_fragment( + br#"echo bin2hex(nl2br("a\nb")); echo ":"; +echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; +echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; +echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; +return function_exists("nl2br");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_bin2hex_builtin() { + let program = parse_fragment( + br#"echo bin2hex("Az"); echo ":"; +echo bin2hex(string: "A\n"); echo ":"; +echo call_user_func("bin2hex", "!?"); echo ":"; +echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; +return function_exists("bin2hex");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "417a:410a:213f:6f6b:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_hex2bin_builtin() { + let program = parse_fragment( + br#"echo hex2bin("417a"); echo ":"; +echo bin2hex(hex2bin(string: "410a")); echo ":"; +echo call_user_func("hex2bin", "213f"); echo ":"; +echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; +echo hex2bin("4") ? "bad" : "false"; echo ":"; +return function_exists("hex2bin");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Az:410a:!?:ok:false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] + ); +} +/// Verifies eval slash escaping builtins use PHP byte-string semantics. +#[test] +fn execute_program_dispatches_slash_escape_builtins() { + let program = parse_fragment( + br#"$escaped = addslashes($source); +echo bin2hex($escaped); echo ":"; +echo bin2hex(stripslashes($escaped)); echo ":"; +echo call_user_func("addslashes", "x\"y"); echo ":"; +echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; +return function_exists("addslashes") && function_exists("stripslashes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let source = values.string("a\0b\\c\"d'").expect("create source"); + scope.set("source", source, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_encode_builtin() { + let program = parse_fragment( + br#"echo base64_encode("Hello"); echo ":"; +echo base64_encode(string: "Hi"); echo ":"; +echo call_user_func("base64_encode", "Test 123!"); echo ":"; +echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; +return function_exists("base64_encode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_decode_builtin() { + let program = parse_fragment( + br#"echo base64_decode("SGVsbG8="); echo ":"; +echo base64_decode(string: "SGk="); echo ":"; +echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; +echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; +return function_exists("base64_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hello:Hi:Test 123!::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs new file mode 100644 index 0000000000..d54d99fe32 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -0,0 +1,485 @@ +//! Purpose: +//! Interpreter tests for string splitting, replacing, regex, entity, URL, ctype, and hash builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover byte-string and encoded string builtin behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. +#[test] +fn execute_program_dispatches_explode_implode_builtins() { + let program = parse_fragment( + br#"$parts = explode(",", "a,b,"); +echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; +echo ":" . implode("|", $parts); +echo ":" . implode(separator: "-", array: ["x", 2, true, null]); +$call_parts = call_user_func("explode", ":", "m:n"); +echo ":" . $call_parts[1]; +echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +return function_exists("implode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. +#[test] +fn execute_program_dispatches_str_split_builtin() { + let program = parse_fragment( + br#"$letters = str_split("abc"); +echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; +$pairs = str_split(string: "abcd", length: 2); +echo $pairs[0] . "-" . $pairs[1]; echo ":"; +$empty = str_split(""); +echo count($empty); echo ":"; +$call = call_user_func("str_split", "xyz", 2); +echo $call[0] . "-" . $call[1]; echo ":"; +$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; echo ":"; +return function_exists("str_split");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. +#[test] +fn execute_program_dispatches_str_pad_builtin() { + let program = parse_fragment( + br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; +echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; +echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; +echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; +echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; +return function_exists("str_pad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string replacement builtins support direct, named, and callable dispatch. +#[test] +fn execute_program_dispatches_string_replace_builtins() { + let program = parse_fragment( + br#"echo str_replace("o", "0", "Hello World"); echo ":"; +echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; +echo str_replace("", "x", "abc"); echo ":"; +echo str_ireplace("HE", "ye", "Hello he"); echo ":"; +echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; +echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; +echo function_exists("str_replace"); +return function_exists("str_ireplace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. +#[test] +fn execute_program_dispatches_preg_builtins() { + let program = parse_fragment( + br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +echo preg_match("/xyz/", "id42") . ":"; +echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); +echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); +echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; +preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); +echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); +echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); +echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); +echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; +preg_match_all("/(x)(y)/", "abc", $none); +echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; +echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; +function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } +echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; +$limited = preg_split("/,/", "a,b,c", 2); +echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; +$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); +echo count($kept) . ":" . $kept[1] . ":"; +echo call_user_func("preg_match", "/x/", "x") . ":"; +$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +class EvalPregMatchesBox { + public array $matches = []; + public static array $staticMatches = []; +} +$box = new EvalPregMatchesBox(); +preg_match("/([a-z]+)([0-9]+)/", "ab12", $box->matches); +echo $box->matches[0] . ":" . $box->matches[1] . ":" . $box->matches[2] . ":"; +$name = "matches"; +preg_match_all("/([a-z])([0-9])/", "a1 b2", $box->{$name}, PREG_SET_ORDER); +echo count($box->matches) . ":" . $box->matches[1][0] . ":" . $box->matches[1][2] . ":"; +preg_match("/([A-Z]+)/", "ID", EvalPregMatchesBox::$staticMatches); +echo EvalPregMatchesBox::$staticMatches[0] . ":"; +$class = "EvalPregMatchesBox"; +$staticName = "staticMatches"; +preg_match_all("/([a-z])/", "xy", $class::${$staticName}); +echo count(EvalPregMatchesBox::$staticMatches[0]) . ":" . EvalPregMatchesBox::$staticMatches[0][1] . ":"; +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:ab12:ab:12:2:b2:2:ID:2:y:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `mb_ereg_match()` anchors at the subject start and honors the `i` option. +#[test] +fn execute_program_dispatches_mb_ereg_match() { + let program = parse_fragment( + br#"echo (mb_ereg_match('ab', 'abc') ? "y" : "n") . ":"; +echo (mb_ereg_match('bc', 'abc') ? "y" : "n") . ":"; +echo (mb_ereg_match('^[A-Z][A-Za-z0-9]*$', 'Foo') ? "y" : "n") . ":"; +echo (mb_ereg_match('[a-z]+\z', 'abc123') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB', 'i') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB', null) ? "y" : "n") . ":"; +echo (call_user_func("mb_ereg_match", "ab", "abx") ? "y" : "n") . ":"; +return function_exists("mb_ereg_match");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "y:n:y:n:n:y:n:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `preg_replace_callback()` accepts the same callable forms as other callback builtins. +#[test] +fn execute_program_preg_replace_callback_accepts_general_callables() { + let program = parse_fragment( + br#"class EvalPregCallbackBox { + public $prefix = ""; + public function __construct($prefix) { $this->prefix = $prefix; } + public function wrap($matches) { return $this->prefix . $matches[0]; } + public static function wrapStatic($matches) { return "S" . $matches[0]; } +} +$box = new EvalPregCallbackBox("O"); +echo preg_replace_callback("/[A-Z]/", [$box, "wrap"], "AB") . ":"; +echo preg_replace_callback("/[0-9]/", "EvalPregCallbackBox::wrapStatic", "12") . ":"; +echo preg_replace_callback("/[C]/", ["EvalPregCallbackBox", "wrapStatic"], "CC") . ":"; +$first = $box->wrap(...); +echo preg_replace_callback("/[a-z]/", $first, "xy") . ":"; +$static = EvalPregCallbackBox::wrapStatic(...); +return preg_replace_callback("/[m]/", $static, "mm");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "OAOB:S1S2:SCSC:OxOy:"); + assert_eq!(values.get(result), FakeValue::String("SmSm".to_string())); +} + +/// Verifies dynamic preg callables write by-reference `$matches` targets. +#[test] +fn execute_program_dispatches_dynamic_preg_match_ref_targets() { + let program = parse_fragment( + br#"$match = "preg_match"; +$ok = $match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +$matchAll = "preg_match_all"; +$count = $matchAll("/([a-z])([0-9])/", "a1 b2", $all, PREG_SET_ORDER); +echo $count . ":" . $all[1][0] . ":" . $all[1][2] . ":"; +$firstClass = preg_match(...); +$okAgain = $firstClass("/([A-Z]+)/", "ID", $firstClassMatches); +return $okAgain . ":" . $firstClassMatches[0];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:id42:id:42:2:b2:2:"); + assert_eq!(values.get(result), FakeValue::String("1:ID".to_string())); +} + +/// Verifies named direct preg calls write by-reference `$matches` targets. +#[test] +fn execute_program_dispatches_named_preg_match_ref_targets() { + let program = parse_fragment( + br#"$named = []; +$ok = preg_match(pattern: "/([a-z]+)([0-9]+)/", subject: "id42", matches: $named); +echo $ok . ":" . $named[0] . ":" . $named[1] . ":" . $named[2] . ":"; +$all = []; +$count = preg_match_all(pattern: "/([a-z])([0-9])/", subject: "a1 b2", matches: $all, flags: PREG_SET_ORDER); +echo $count . ":" . $all[1][0] . ":" . $all[1][2] . ":"; +return preg_match(pattern: "/x/", subject: "x", flags: PREG_OFFSET_CAPTURE);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:id42:id:42:2:b2:2:"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} + +/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. +#[test] +fn execute_program_dispatches_html_entity_builtins() { + let program = parse_fragment( + br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; +echo htmlentities(string: ""); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; +echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); +return function_exists("html_entity_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_url_codec_builtins() { + let program = parse_fragment( + br#"echo urlencode("a b&=~"); echo ":"; +echo rawurlencode(string: "a b&=~"); echo ":"; +echo urldecode("a+b%26%3D%7E"); echo ":"; +echo rawurldecode("a+b%26%3D%7E"); echo ":"; +echo call_user_func("urlencode", "%zz"); echo ":"; +echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; +echo function_exists("urlencode"); echo function_exists("rawurlencode"); +echo function_exists("urldecode"); +return function_exists("rawurldecode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval incremental hash context builtins use elephc-crypto state. +#[test] +fn execute_program_dispatches_hash_context_builtins() { + let program = parse_fragment( + br#"$ctx = hash_init("sha256"); +echo is_resource($ctx) ? "ctx" : "bad"; echo ":"; +echo get_resource_type($ctx) === "stream" ? "rtype" : "bad"; echo ":"; +echo hash_update($ctx, "ab") ? "up1" : "bad"; echo ":"; +$copy = hash_copy($ctx); +echo hash_update($ctx, "c") ? "up2" : "bad"; echo ":"; +echo hash_update($copy, "d") ? "upcopy" : "bad"; echo ":"; +echo hash_final($ctx); echo ":"; +echo hash_final($copy); echo ":"; +$raw = call_user_func("hash_init", "md5"); +hash_update(context: $raw, data: "abc"); +echo bin2hex(call_user_func("hash_final", $raw, true)); echo ":"; +$named = call_user_func_array("hash_init", ["algo" => "sha1"]); +call_user_func_array("hash_update", ["context" => $named, "data" => "abc"]); +echo call_user_func_array("hash_final", ["context" => $named]); echo ":"; +echo function_exists("hash_init"); echo function_exists("hash_update"); +echo function_exists("hash_final"); echo function_exists("hash_copy"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "ctx:rtype:up1:up2:upcopy:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_ctype_builtins() { + let program = parse_fragment( + br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; +echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; +echo ctype_alnum("a1") ? "N" : "-"; echo ":"; +echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; +echo ctype_alpha("") ? "bad" : "empty"; echo ":"; +echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; +echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; +echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); +echo function_exists("ctype_alnum"); +return function_exists("ctype_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. +#[test] +fn execute_program_dispatches_crc32_builtin() { + let program = parse_fragment( + br#"echo crc32(""); echo ":"; +echo crc32(string: "123456789"); echo ":"; +echo call_user_func("crc32", "hello"); echo ":"; +echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; +return function_exists("crc32");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. +#[test] +fn execute_program_dispatches_hash_algos_builtin() { + let program = parse_fragment( + br#"$algos = hash_algos(); +echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; +echo in_array("crc32c", $algos) ? "crc" : "bad"; +$call = call_user_func("hash_algos"); +echo ":" . $call[18]; +$spread = call_user_func_array("hash_algos", []); +echo ":" . $spread[27] . ":"; +echo function_exists("hash_algos") ? "exists" : "missing"; +return count($algos);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); + assert_eq!(values.get(result), FakeValue::Int(28)); +} +/// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. +#[test] +fn execute_program_dispatches_hash_digest_builtins() { + let filename = format!("elephc_magician_hash_file_{}.txt", std::process::id()); + let source = format!( + r#"echo md5("abc"); echo ":"; +echo sha1(string: "abc"); echo ":"; +echo hash("sha256", "abc"); echo ":"; +echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; +echo bin2hex(md5("abc", true)); echo ":"; +echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; +echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +file_put_contents("{filename}", "abc"); +echo hash_file("sha256", "{filename}"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; +echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; +unlink("{filename}"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); +return function_exists("hash_hmac");"#, + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs new file mode 100644 index 0000000000..ae40a2b1c1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs @@ -0,0 +1,225 @@ +//! Purpose: +//! Interpreter tests for text-case, wrapping, search, comparison, and trim string builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover PHP byte-string behavior for text-oriented helpers. + +use super::super::*; +use super::support::*; + +/// Verifies eval ASCII string case builtins work directly and through callable dispatch. +#[test] +fn execute_program_dispatches_string_case_builtins() { + let program = parse_fragment( + br#"echo strtoupper("Hello World"); echo ":"; +echo strtolower("LOUD"); echo ":"; +echo ucfirst("eval"); echo ":"; +echo lcfirst("LOUD"); echo ":"; +echo call_user_func("strtoupper", "xy"); echo ":"; +echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; +echo call_user_func("ucfirst", "case"); echo ":"; +echo call_user_func_array("lcfirst", ["CASE"]); +echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); +return function_exists("lcfirst");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. +#[test] +fn execute_program_dispatches_ucwords_builtin() { + let program = parse_fragment( + br#"echo ucwords("hello world"); echo ":"; +echo ucwords(string: "hello-world", separators: "-"); echo ":"; +echo ucwords("hello\tworld"); echo ":"; +echo call_user_func("ucwords", "a b"); echo ":"; +echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; +return function_exists("ucwords");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:Hello-World:Hello\tWorld:A B:A-B:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. +#[test] +fn execute_program_dispatches_wordwrap_builtin() { + let program = parse_fragment( + br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; +echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; +echo wordwrap("abcdefghij", 4, "|", true); echo ":"; +echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; +echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; +return function_exists("wordwrap");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. +#[test] +fn execute_program_dispatches_str_contains_builtin() { + let program = parse_fragment( + br#"echo str_contains("Hello World", "World") ? "Y" : "N"; +echo str_contains("Hello", "z") ? "bad" : ":N"; +echo str_contains("Hello", "") ? ":E" : "bad"; +echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; +echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; +return function_exists("str_contains");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:E:C:A"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string position builtins return byte offsets or PHP false. +#[test] +fn execute_program_dispatches_string_position_builtins() { + let program = parse_fragment( + br#"echo strpos("banana", "na"); +echo ":" . strrpos("banana", "na"); +echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; +echo ":" . strpos("abc", ""); +echo ":" . strrpos("abc", ""); +echo ":" . call_user_func("strpos", "abc", "b"); +echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); +echo ":"; echo function_exists("strpos"); +return function_exists("strrpos");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:4:F:0:3:1:3:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. +#[test] +fn execute_program_dispatches_strstr_builtin() { + let program = parse_fragment( + br#"echo strstr("user@example.com", "@"); echo ":"; +echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; +echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; +echo strstr("hello", ""); echo ":"; +echo call_user_func("strstr", "abcabc", "bc"); echo ":"; +echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; +return function_exists("strstr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval prefix/suffix string search builtins use byte-string semantics. +#[test] +fn execute_program_dispatches_string_boundary_builtins() { + let program = parse_fragment( + br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; +echo str_starts_with("Hello", "World") ? "bad" : ":s"; +echo str_starts_with("Hello", "") ? ":se" : "bad"; +echo str_ends_with("Hello World", "World") ? ":E" : "bad"; +echo str_ends_with("Hello", "World") ? "bad" : ":e"; +echo str_ends_with("Hello", "") ? ":ee" : "bad"; +echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; +echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; +echo ":"; echo function_exists("str_starts_with"); +return function_exists("str_ends_with");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string comparison builtins return PHP-compatible scalar results. +#[test] +fn execute_program_dispatches_string_compare_builtins() { + let program = parse_fragment( + br#"echo strcmp("abc", "abc"); +echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; +echo ":"; echo strcasecmp("Hello", "hello"); +echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); +return function_exists("hash_equals");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval trim-like builtins strip default and explicit byte masks. +#[test] +fn execute_program_dispatches_trim_like_builtins() { + let program = parse_fragment( + br#"echo "[" . trim(" hello ") . "]"; +echo ":[" . ltrim(" left") . "]"; +echo ":[" . rtrim("right ") . "]"; +echo ":[" . chop("tail... ", " .") . "]"; +echo ":[" . trim("**boxed**", "*") . "]"; +echo ":[" . call_user_func("trim", " cuf ") . "]"; +echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; +echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); +return function_exists("chop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs new file mode 100644 index 0000000000..72168913c5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -0,0 +1,469 @@ +//! Purpose: +//! Interpreter tests for isset, empty, function/class probes, dynamic constants, and class declarations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover symbol-table and metadata probes backed by the eval context. + +use super::super::*; +use super::support::*; +use std::ffi::c_void; + +/// Verifies `isset` distinguishes missing, null, and other falsey values. +#[test] +fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { + let program = parse_fragment( + br#"if (isset($missing)) { echo "1"; } else { echo "0"; } +if (isset($nullish)) { echo "1"; } else { echo "0"; } +if (isset($zero)) { echo "1"; } else { echo "0"; } +if (isset($empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty = values.string("").expect("create fake string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty", empty, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "001110"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies `empty` treats missing, null, and falsey values as empty. +#[test] +fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { + let program = parse_fragment( + br#"if (empty($missing)) { echo "1"; } else { echo "0"; } +if (empty($nullish)) { echo "1"; } else { echo "0"; } +if (empty($zero)) { echo "1"; } else { echo "0"; } +if (empty($empty_string)) { echo "1"; } else { echo "0"; } +if (empty($zero_string)) { echo "1"; } else { echo "0"; } +if (empty($value)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty_string = values.string("").expect("create fake empty string"); + let zero_string = values.string("0").expect("create fake zero string"); + let value = values.string("x").expect("create fake non-empty string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); + scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); + scope.set("value", value, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111110"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies `isset` and `empty` use PHP offset semantics for array reads. +#[test] +fn execute_program_isset_and_empty_support_array_offsets() { + let program = parse_fragment( + br#"$map = [ + "present" => "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +]; +echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1001100:01111011"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. +#[test] +fn execute_program_function_probes_use_eval_context() { + let program = parse_fragment( + br#"function dyn_probe() { return 1; } +echo function_exists("DYN_PROBE") . "x"; +echo is_callable("dyn_probe") . "x"; +echo function_exists("strlen") . "x"; +echo function_exists("native_probe") . "x"; +echo function_exists("eval") . "x"; +echo function_exists("missing_probe") . "x";"#, + ) + .expect("parse eval fragment"); + let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); + let mut context = ElephcEvalContext::new(); + assert!(context + .define_native_function("native_probe", native) + .is_ok()); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1x1xxx"); +} + +/// Verifies eval function probes recognize static DateTime/calendar procedural aliases. +#[test] +fn execute_program_function_probes_recognize_date_procedural_aliases() { + let program = parse_fragment( + br#"$aliases = [ + "idate", "mktime", "gmmktime", "date_create", "date_create_immutable", + "date_create_from_format", "date_create_immutable_from_format", + "date_parse_from_format", "date_parse", "date_sun_info", "date_sunrise", + "date_sunset", "strptime", "timezone_name_from_abbr", "cal_to_jd", + "cal_from_jd", "cal_days_in_month", "cal_info", "gregoriantojd", + "jdtogregorian", "juliantojd", "jdtojulian", "frenchtojd", + "jdtofrench", "jewishtojd", "jdtojewish", "jddayofweek", "jdmonthname", + "jdtounix", "unixtojd", "easter_days", "easter_date", "gettimeofday", + "date_get_last_errors", "strftime", "gmstrftime", "timezone_open", + "timezone_identifiers_list", "timezone_location_get", + "timezone_transitions_get", "timezone_abbreviations_list", + "timezone_version_get", "date_interval_create_from_date_string", + "date_diff", "date_format", "date_add", "date_sub", "date_modify", + "date_timestamp_get", "date_timestamp_set", "date_timezone_get", + "date_timezone_set", "date_offset_get", "date_date_set", + "date_isodate_set", "date_time_set", "date_interval_format", + "timezone_name_get", "timezone_offset_get" +]; +foreach ($aliases as $alias) { + echo function_exists($alias) ? "1" : "0"; +} +echo ":"; +echo function_exists("Date_Create") ? "C" : "c"; +echo function_exists("EvalAlias\\idate") ? "N" : "n"; +echo is_callable("timezone_version_get") ? "I" : "i"; +echo function_exists("does_not_exist_alias_xyz") ? "x" : "X";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1".repeat(59) + ":CNIX"); +} + +/// Verifies simple procedural date/time aliases execute directly and by callable. +#[test] +fn execute_program_dispatches_simple_date_procedural_aliases() { + let program = parse_fragment( + br#"echo timezone_version_get() === "" ? "v" : "V"; +echo call_user_func("timezone_version_get") === timezone_version_get() ? "C" : "c"; +echo ":"; +echo idate("Y", 0); +echo ":"; +echo call_user_func("idate", "Y", 0); +echo ":"; +$ids = timezone_identifiers_list(512); +echo count($ids) . ":" . $ids[0];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "VC:1970:1970:38:Pacific/Apia"); +} + +/// Verifies eval `interface_exists()` probes generated interface metadata by callable. +#[test] +fn execute_program_interface_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo interface_exists("KnownInterface") ? "Y" : "N"; +echo interface_exists("knowninterface") ? "Y" : "N"; +echo interface_exists("KnownClass") ? "Y" : "N"; +echo interface_exists("UnitEnum") ? "U" : "u"; +echo interface_exists("BackedEnum") ? "B" : "b"; +echo class_exists("UnitEnum") ? "C" : "c"; +echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; +echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYNUBcYYN"); +} +/// Verifies eval-declared interfaces are visible to interface symbol probes. +#[test] +fn execute_program_interface_exists_uses_dynamic_interface_table() { + let program = parse_fragment( + br#"interface DynEvalIface {} +echo interface_exists("DynEvalIface") ? "Y" : "N"; +echo interface_exists("dynevaliface") ? "Y" : "N"; +echo class_exists("DynEvalIface") ? "C" : "c"; +echo call_user_func("interface_exists", "DynEvalIface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "\DynEvalIface"]) ? "Y" : "N"; +$interfaces = get_declared_interfaces(); +echo count($interfaces); echo ":"; echo $interfaces[0];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYcYY1:DynEvalIface"); +} +/// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. +#[test] +fn execute_program_class_like_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo trait_exists("KnownTrait") ? "T" : "t"; +echo trait_exists("knowntrait") ? "T" : "t"; +echo trait_exists("KnownEnum") ? "T" : "t"; +echo enum_exists("KnownEnum") ? "E" : "e"; +echo enum_exists("\knownenum") ? "E" : "e"; +echo enum_exists("KnownTrait") ? "E" : "e"; +echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; +echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; +echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TTtEEeTEte"); +} +/// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. +#[test] +fn execute_program_is_a_relation_uses_runtime_probe() { + let program = parse_fragment( + br#"$object = new KnownClass(); +echo is_a($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; +echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; +echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YNYYYN"); +} +/// Verifies eval `define()` and `defined()` share a dynamic constant-name table. +#[test] +fn execute_program_define_and_defined_use_dynamic_constant_table() { + let program = parse_fragment( + br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; +echo DynEvalConst; +echo \DynEvalConst; +echo defined("DynEvalConst") ? "Y" : "N"; +echo defined("\\DynEvalConst") ? "Y" : "N"; +echo defined("dynevalconst") ? "Y" : "N"; +echo define("DynEvalConst", 2) ? "Y" : "N"; +echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; +echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YokokYYNNYY"); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} +/// Verifies eval predefined runtime constants are fetchable and cannot be redefined. +#[test] +fn execute_program_reads_predefined_runtime_constants() { + let program = parse_fragment( + br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; +echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; +echo DIRECTORY_SEPARATOR; echo ":"; +echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; +echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; +echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; +echo defined("php_os") ? "bad" : "case"; echo ":"; +echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; +return PHP_INT_MAX;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); + assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} +/// Verifies missing eval dynamic constants fail through runtime status. +#[test] +fn execute_program_missing_constant_fetch_fails() { + let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval class probes use the runtime class-name table. +#[test] +fn execute_program_class_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"class DynProbe {} +echo class_exists("DynProbe") ? "Y" : "N"; +echo class_exists("\dynprobe") ? "Y" : "N"; +echo class_exists("KnownClass") ? "Y" : "N"; +echo class_exists("\knownclass") ? "Y" : "N"; +echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYYYN"); +} +/// Verifies eval `class_alias()` registers dynamic and runtime-visible aliases. +#[test] +fn execute_program_class_alias_registers_aliases() { + let program = parse_fragment( + br#"class DynAliasBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +interface DynAliasIface {} +trait DynAliasTrait {} +enum DynAliasEnum: string { + case Ready = "ready"; +} +echo class_alias("DynAliasBox", "DynAliasCopy") ? "alias" : "bad"; echo ":"; +echo class_exists("DynAliasCopy") ? "exists" : "bad"; echo ":"; +$box = new DynAliasCopy(5); +echo get_class($box); echo ":"; +echo $box->bump(2); echo ":"; +echo is_a($box, "DynAliasCopy") ? "isa" : "bad"; echo ":"; +echo class_alias("DynAliasIface", "DynAliasIfaceCopy") ? "iface-alias" : "bad"; echo ":"; +echo interface_exists("DynAliasIfaceCopy") ? "iface-exists" : "bad"; echo ":"; +echo class_exists("DynAliasIfaceCopy") ? "bad" : "iface-not-class"; echo ":"; +echo is_a("DynAliasIfaceCopy", "DynAliasIface", true) ? "iface-isa" : "bad"; echo ":"; +echo (new ReflectionClass("DynAliasIfaceCopy"))->isInterface() ? "iface-reflect" : "bad"; echo ":"; +echo class_alias("UnitEnum", "DynAliasUnitEnum") ? "unit-alias" : "bad"; echo ":"; +echo interface_exists("DynAliasUnitEnum") ? "unit-exists" : "bad"; echo ":"; +echo class_exists("DynAliasUnitEnum") ? "bad" : "unit-not-class"; echo ":"; +echo class_alias("DynAliasTrait", "DynAliasTraitCopy") ? "trait-alias" : "bad"; echo ":"; +echo trait_exists("DynAliasTraitCopy") ? "trait-exists" : "bad"; echo ":"; +echo class_exists("DynAliasTraitCopy") ? "bad" : "trait-not-class"; echo ":"; +echo is_a("DynAliasTraitCopy", "DynAliasTrait", true) ? "trait-isa" : "bad"; echo ":"; +echo class_alias("DynAliasEnum", "DynAliasEnumCopy") ? "enum-alias" : "bad"; echo ":"; +echo enum_exists("DynAliasEnumCopy") ? "enum-exists" : "bad"; echo ":"; +echo class_exists("DynAliasEnumCopy") ? "enum-class" : "bad"; echo ":"; +echo (new ReflectionClass("DynAliasEnumCopy"))->getName(); echo ":"; +echo DynAliasEnumCopy::Ready->value; echo ":"; +echo class_alias("DynAliasBox", "DynAliasCopy") ? "bad" : "duplicate"; echo ":"; +echo class_alias("MissingAliasSource", "MissingAliasTarget") ? "bad" : "missing"; echo ":"; +echo call_user_func("class_alias", "DynAliasBox", "DynAliasCall") ? "call" : "bad"; echo ":"; +echo class_exists("DynAliasCall") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("class_alias", ["class" => "KnownClass", "alias" => "KnownAlias"]) ? "aot" : "bad"; echo ":"; +$known = new KnownAlias(); +echo is_a($known, "KnownAlias") ? "known" : "bad"; echo ":"; +echo function_exists("class_alias"); +return is_callable("class_alias");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "alias:exists:DynAliasBox:7:isa:iface-alias:iface-exists:iface-not-class:iface-isa:iface-reflect:unit-alias:unit-exists:unit-not-class:trait-alias:trait-exists:trait-not-class:trait-isa:enum-alias:enum-exists:enum-class:DynAliasEnum:ready:duplicate:missing:call:call-exists:aot:known:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `get_declared_*()` lists eval-visible class-like declarations. +#[test] +fn execute_program_get_declared_symbols_reports_eval_declarations() { + let program = parse_fragment( + br#"class DeclaredOne {} +class DeclaredTwo {} +class_alias("DeclaredOne", "DeclaredAlias"); +$classes = get_declared_classes(); +echo count($classes); echo ":"; +echo $classes[0]; echo ":"; +echo $classes[1]; echo ":"; +$call = call_user_func("get_declared_classes"); +echo count($call); echo ":"; +echo count(get_declared_interfaces()); echo ":"; +echo count(call_user_func_array("get_declared_traits", [])); echo ":"; +echo function_exists("get_declared_classes"); +echo function_exists("get_declared_interfaces"); +return is_callable("get_declared_traits");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:DeclaredOne:DeclaredTwo:2:0:0:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies duplicate eval-declared class names fail through runtime status. +#[test] +fn execute_program_duplicate_class_declaration_fails() { + let program = parse_fragment( + br#"class DynProbeDup {} +class dynprobedup {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs new file mode 100644 index 0000000000..f4fd060253 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs @@ -0,0 +1,537 @@ +//! Purpose: +//! Interpreter tests for time, system, SPL, environment, host, protocol, and IP builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases isolate platform-facing builtins behind deterministic fake assertions where possible. + +use super::super::*; +use super::support::*; + +/// Verifies eval zero-argument system builtins return native-compatible values. +#[test] +fn execute_program_dispatches_zero_arg_system_builtins() { + let program = parse_fragment( + br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +return function_exists("sys_get_temp_dir");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + eval_compiler_php_version(), + eval_compiler_php_version() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. +#[test] +fn execute_program_dispatches_date_mktime_builtins() { + let program = parse_fragment( + br#"$ts = mktime(13, 2, 3, 1, 2, 2024); +echo date("Y-m-d H:i:s", $ts); +echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); +echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); +echo ":" . call_user_func("date", "Y", $ts); +$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); +return function_exists("mktime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval UTC calendar builtins and timezone probes are callable-visible. +#[test] +fn execute_program_dispatches_extended_calendar_builtins() { + let program = parse_fragment( + br#"echo date_default_timezone_get(); echo ":"; +echo date_default_timezone_set("UTC") ? "set" : "bad"; echo ":"; +$ts = gmmktime(0, 0, 0, 1, 2, 2024); +echo gmdate("Y-m-d H:i:s", $ts); echo ":"; +echo call_user_func("gmdate", "Y", $ts); echo ":"; +echo checkdate(2, 29, 2024) ? "leap" : "bad"; echo ":"; +echo checkdate(2, 29, 2023) ? "bad" : "common"; echo ":"; +$g = getdate(0); +echo $g["year"] . "-" . $g["mon"] . "-" . $g["mday"] . " " . $g["hours"] . ":" . $g["minutes"] . ":" . $g["seconds"]; +echo ":" . $g["weekday"] . ":" . $g["month"] . ":" . $g[0]; +$l = localtime(0, true); +echo ":" . ($l["tm_year"] + 1900) . "-" . $l["tm_mon"] . "-" . $l["tm_mday"] . " " . $l["tm_hour"]; +$n = localtime(0); +$callLocal = call_user_func_array("localtime", ["timestamp" => 0, "associative" => true]); +echo ":" . ($n[5] + 1900) . "-" . $n[4] . ":" . $callLocal["tm_sec"]; +echo ":" . function_exists("gmdate") . function_exists("gmmktime") . function_exists("checkdate"); +echo function_exists("getdate") . function_exists("localtime") . function_exists("date_default_timezone_get"); +return function_exists("date_default_timezone_set");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "UTC:set:2024-01-02 00:00:00:2024:leap:common:1970-1-1 0:0:0:Thursday:January:0:1970-0-1 0:1970-0:0:111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. +#[test] +fn execute_program_dispatches_strtotime_builtin() { + let program = parse_fragment( + br#"$date = strtotime("2024-06-15"); +echo date("Y-m-d H:i:s", $date); +$full = strtotime("2024-06-15 12:30:45"); +echo ":" . date("Y-m-d H:i:s", $full); +$short = strtotime("2024-06-15T12:30"); +echo ":" . date("Y-m-d H:i:s", $short); +echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); +$call = call_user_func("strtotime", "2024-01-02 03:04:05"); +echo ":" . date("Y-m-d H:i:s", $call); +$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); +echo ":" . date("Y-m-d", $spread); +echo ":" . strtotime("now", 1700000000); +$base = call_user_func_array("strtotime", ["datetime" => "now", "baseTimestamp" => 1700000001]); +echo ":" . $base . ":"; +return function_exists("strtotime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:1700000000:1700000001:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. +#[test] +fn execute_program_dispatches_microtime_builtin() { + let program = parse_fragment( + br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; +return function_exists("microtime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `hrtime()`, `http_response_code()`, and `header()` dispatch paths. +#[test] +fn execute_program_dispatches_hrtime_and_http_header_builtins() { + let program = parse_fragment( + br#"$parts = hrtime(); +echo count($parts) === 2 ? "parts" : "bad"; echo ":"; +echo is_int($parts[0]) && is_int($parts[1]) ? "ints" : "bad"; echo ":"; +echo hrtime(true) > 0 ? "number" : "bad"; echo ":"; +echo http_response_code(); echo ":"; +echo http_response_code(404); echo ":"; +echo http_response_code(); echo ":"; +header("X-Test: 1", true, 201); +echo http_response_code(); echo ":"; +echo call_user_func("http_response_code", 202); echo ":"; +echo http_response_code(); echo ":"; +echo call_user_func_array("header", ["header" => "X-Test: 2", "replace" => false, "response_code" => 204]); +echo http_response_code(); echo ":"; +echo function_exists("hrtime") . function_exists("http_response_code") . function_exists("header"); +return is_callable("header");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "parts:ints:number:200:200:404:201:201:202:204:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. +#[test] +fn execute_program_dispatches_realpath_cache_builtins() { + let program = parse_fragment( + br#"$cache = realpath_cache_get(); +echo count($cache) . ":" . realpath_cache_size() . ":"; +$call_cache = call_user_func("realpath_cache_get"); +echo count($call_cache) . ":"; +echo call_user_func_array("realpath_cache_size", []) . ":"; +echo function_exists("realpath_cache_get"); +return function_exists("realpath_cache_size");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:0:0:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval stream introspection builtins return native-compatible static lists. +#[test] +fn execute_program_dispatches_stream_introspection_builtins() { + let program = parse_fragment( + br#"$wrappers = stream_get_wrappers(); +$transports = stream_get_transports(); +$filters = stream_get_filters(); +echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; +echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; +echo count($filters) . ":" . $filters[2] . ":"; +$call_wrappers = call_user_func("stream_get_wrappers"); +echo $call_wrappers[10] . ":"; +$call_transports = call_user_func_array("stream_get_transports", []); +echo $call_transports[11] . ":"; +$call_filters = call_user_func_array("stream_get_filters", []); +echo $call_filters[13] . ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); +return function_exists("stream_get_filters");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval stream predicate stubs match elephc's fixed stream metadata behavior. +#[test] +fn execute_program_dispatches_stream_predicate_builtins() { + let program = parse_fragment( + br#"echo stream_is_local("php://memory") ? "local" : "bad"; echo ":"; +echo stream_supports_lock($handle) ? "lock" : "bad"; echo ":"; +echo call_user_func("stream_is_local", "file://tmp") ? "call" : "bad"; echo ":"; +echo call_user_func_array("stream_supports_lock", ["stream" => $handle]) ? "spread" : "bad"; echo ":"; +echo function_exists("stream_is_local"); +return function_exists("stream_supports_lock");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle", handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "local:lock:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. +#[test] +fn execute_program_dispatches_spl_classes_builtin() { + let program = parse_fragment( + br#"$names = spl_classes(); +echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; +echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; +echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; +$call = call_user_func("spl_classes"); +echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; +$spread = call_user_func_array("spl_classes", []); +echo (count($spread) === count($names) ? "spread" : "bad") . ":"; +echo function_exists("spl_classes"); +return is_callable("spl_classes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "61:AppendIterator:Throwable:exception:list:call:spread:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval SPL object identity builtins are stable, unique, and callable. +#[test] +fn execute_program_dispatches_spl_object_identity_builtins() { + let program = parse_fragment( + br#"$a = new KnownClass(); +$b = new KnownClass(); +echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; +echo ":"; +echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; +echo ":"; +echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; +echo ":"; +echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; +echo ":"; +echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); +return function_exists("spl_object_hash");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stable:unique:hash:call:array:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval environment builtins read, write, unset, and dispatch dynamically. +#[test] +fn execute_program_dispatches_environment_builtins() { + let program = parse_fragment( + br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); +echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); +echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +return function_exists("putenv");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval shell process builtins capture or echo stdout across all call paths. +#[test] +fn execute_program_dispatches_process_builtins() { + let program = parse_fragment( + br#"echo shell_exec("printf shell"); echo ":"; +echo exec(command: "printf exec"); echo ":"; +echo system("printf system") === "" ? "empty" : "bad"; echo ":"; +echo passthru(command: "printf pass") === null ? "null" : "bad"; echo ":"; +echo call_user_func("shell_exec", "printf call"); echo ":"; +echo call_user_func_array("exec", ["command" => "printf spread"]); echo ":"; +echo call_user_func("system", "printf dynsys") === "" ? "dyn-empty" : "bad"; echo ":"; +echo call_user_func_array("passthru", ["command" => "printf dynpass"]) === null ? "dyn-null" : "bad"; echo ":"; +echo function_exists("exec"); echo function_exists("shell_exec"); echo function_exists("system"); +return function_exists("passthru");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "shell:exec:systemempty:passnull:call:spread:dynsysdyn-empty:dynpassdyn-null:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval sleep builtins dispatch without delaying focused tests. +#[test] +fn execute_program_dispatches_sleep_builtins() { + let program = parse_fragment( + br#"echo sleep(0) . ":"; +echo sleep(seconds: 0) . ":"; +usleep(0); +echo "u:"; +echo call_user_func("sleep", 0) . ":"; +echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +return function_exists("usleep");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:u:0:null:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. +#[test] +fn execute_program_dispatches_php_uname_builtin() { + let program = parse_fragment( + br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("php_uname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "all:same:sys:node:release:version:machine:call:spread:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. +#[test] +fn execute_program_dispatches_gethostbyname_builtin() { + let program = parse_fragment( + br#"echo gethostbyname("127.0.0.1") . ":"; +echo gethostbyname(hostname: "not a host") . ":"; +echo call_user_func("gethostbyname", "127.0.0.1") . ":"; +echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; +return function_exists("gethostbyname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. +#[test] +fn execute_program_dispatches_gethostname_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("gethostname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "host:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. +#[test] +fn execute_program_dispatches_gethostbyaddr_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +return function_exists("gethostbyaddr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:false:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval protocol and service database lookups dispatch dynamically. +#[test] +fn execute_program_dispatches_protocol_service_builtins() { + let program = parse_fragment( + br#"echo getprotobyname("TCP") . ":"; +echo getprotobynumber(6) . ":"; +echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; +echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; +echo getservbyname("www", "tcp") . ":"; +echo getservbyport(80, "tcp") . ":"; +echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; +echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; +echo call_user_func("getprotobyname", "udp") . ":"; +echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); +return function_exists("getservbyport");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. +#[test] +fn execute_program_dispatches_ip_conversion_builtins() { + let program = parse_fragment( + br#"echo long2ip(3232235777) . ":"; +echo long2ip(ip: 4294967295) . ":"; +echo ip2long("192.168.1.1") . ":"; +echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; +$packed = inet_pton("1.2.3.4"); +echo bin2hex($packed) . ":"; +echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; +echo inet_ntop($packed) . ":"; +echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; +echo call_user_func("long2ip", 2130706433) . ":"; +echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); +return function_exists("inet_ntop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs new file mode 100644 index 0000000000..05d8299ab7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -0,0 +1,458 @@ +//! Purpose: +//! Interpreter tests for eval-declared class constants. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover inherited lookup, scoped receivers, visibility, and dynamic storage. + +use super::super::*; +use super::support::*; + +/// Verifies class constants can be fetched directly and through scoped receivers. +#[test] +fn execute_program_reads_eval_class_constants() { + let program = parse_fragment( + br#"class EvalConstBase { + public const SEED = 2; + protected const HIDDEN = 5; + public static function read() { + return self::SEED + static::SEED; + } + public static function hidden() { + return self::HIDDEN; + } +} +class EvalConstChild extends EvalConstBase { + public const SEED = 7; + public static function readParent() { + return parent::SEED; + } +} +echo EvalConstBase::SEED; echo ":"; +echo EvalConstChild::SEED; echo ":"; +echo EvalConstChild::read(); echo ":"; +echo EvalConstChild::readParent(); echo ":"; +return EvalConstChild::hidden();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:7:9:2:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies comma-separated class-like constants are registered and fetchable. +#[test] +fn execute_program_reads_comma_separated_eval_class_like_constants() { + let program = parse_fragment( + br#"class EvalMultiConstClass { + public const A = 1, B = 2; +} +interface EvalMultiConstIface { + public const C = 3, D = 4; +} +trait EvalMultiConstTrait { + public const E = 5, F = 6; +} +class EvalMultiConstTraitBox { + use EvalMultiConstTrait; +} +enum EvalMultiConstEnum { + public const G = 7, H = 8; + case Ready; +} +echo EvalMultiConstClass::A; echo EvalMultiConstClass::B; echo ":"; +echo EvalMultiConstIface::C; echo EvalMultiConstIface::D; echo ":"; +echo EvalMultiConstTraitBox::E; echo EvalMultiConstTraitBox::F; echo ":"; +return EvalMultiConstEnum::G + EvalMultiConstEnum::H;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:34:56:"); + assert_eq!(values.get(result), FakeValue::Int(15)); +} + +/// Verifies protected class constant access from global eval scope throws Error. +#[test] +fn execute_program_protected_eval_class_constant_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalConstProtected { + protected const SECRET = 4; +} +try { + echo EvalConstProtected::SECRET; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access protected constant EvalConstProtected::SECRET" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies duplicate class constants in one eval class are rejected. +#[test] +fn execute_program_rejects_duplicate_eval_class_constant() { + let program = parse_fragment( + br#"class EvalConstDuplicate { + public const SEED = 1; + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("duplicate class constant should fail"); +} + +/// Verifies final eval class constants are readable and reject child redeclarations. +#[test] +fn execute_program_rejects_overriding_final_eval_class_constant() { + let program = parse_fragment( + br#"class EvalFinalConstBase { + final public const SEED = 1; +} +class EvalFinalConstChild extends EvalFinalConstBase { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final class constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts class constant redeclarations that keep compatible visibility. +#[test] +fn execute_program_accepts_compatible_eval_class_constant_redeclaration() { + let program = parse_fragment( + br#"class EvalConstVisibilityBase { + protected const SEED = 2; +} +class EvalConstVisibilityChild extends EvalConstVisibilityBase { + public const SEED = 7; +} +interface EvalConstVisibilityIface { + public const TOKEN = 3; +} +class EvalConstVisibilityImpl implements EvalConstVisibilityIface { + public const TOKEN = 5; +} +echo EvalConstVisibilityChild::SEED; echo ":"; +return EvalConstVisibilityImpl::TOKEN;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies eval rejects inherited class constant redeclarations with reduced visibility. +#[test] +fn execute_program_rejects_reduced_eval_class_constant_visibility() { + let reduced_parent_visibility = parse_fragment( + br#"class EvalConstPublicBase { + public const SEED = 1; +} +class EvalConstProtectedChild extends EvalConstPublicBase { + protected const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_parent_visibility, &mut scope, &mut values) + .expect_err("reduced class constant visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_protected_parent_visibility = parse_fragment( + br#"class EvalConstProtectedBase { + protected const SEED = 1; +} +class EvalConstPrivateChild extends EvalConstProtectedBase { + private const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_protected_parent_visibility, &mut scope, &mut values) + .expect_err("private class constant redeclaration should fail protected parent"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_interface_visibility = parse_fragment( + br#"interface EvalConstPublicContract { + public const SEED = 1; +} +class EvalConstProtectedImpl implements EvalConstPublicContract { + protected const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_interface_visibility, &mut scope, &mut values) + .expect_err("reduced interface constant visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects non-public interface constants like PHP. +#[test] +fn execute_program_rejects_non_public_eval_interface_constants() { + parse_fragment( + br#"interface EvalConstProtectedIface { + protected const SEED = 1; +}"#, + ) + .expect_err("protected interface constant should fail while parsing"); + + parse_fragment( + br#"interface EvalConstPrivateIface { + private const SEED = 1; +}"#, + ) + .expect_err("private interface constant should fail while parsing"); +} + +/// Verifies private eval constants cannot be declared final. +#[test] +fn execute_program_rejects_final_private_eval_class_constant() { + let program = parse_fragment( + br#"class EvalFinalPrivateConst { + final private const SEED = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("final private class constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies class-name literals resolve class-like receiver spelling. +#[test] +fn execute_program_reads_eval_class_name_literals() { + let program = parse_fragment( + br#"class EvalClassNameBase { + public static function selfName() { return self::class; } + public static function staticName() { return static::class; } +} +class EvalClassNameChild extends EvalClassNameBase {} +interface EvalClassNameIface {} +trait EvalClassNameTrait {} +echo EvalClassNameChild::class; echo ":"; +echo EvalClassNameIface::class; echo ":"; +echo EvalClassNameTrait::class; echo ":"; +echo EvalClassNameChild::selfName(); echo ":"; +return EvalClassNameChild::staticName();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalClassNameChild:EvalClassNameIface:EvalClassNameTrait:EvalClassNameBase:" + ); + assert_eq!( + values.get(result), + FakeValue::String("EvalClassNameChild".to_string()) + ); +} + +/// Verifies interface constants are readable directly, through inheritance, and through classes. +#[test] +fn execute_program_reads_eval_interface_constants() { + let program = parse_fragment( + br#"interface EvalConstParentIface { + public const BASE = 2; +} +interface EvalConstChildIface extends EvalConstParentIface { + public const LOCAL = 3; +} +class EvalConstIfaceImpl implements EvalConstChildIface {} +echo EvalConstParentIface::BASE; echo ":"; +echo EvalConstChildIface::BASE; echo ":"; +echo EvalConstChildIface::LOCAL; echo ":"; +return EvalConstIfaceImpl::BASE + EvalConstIfaceImpl::LOCAL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies final eval interface constants cannot be redeclared by children or implementors. +#[test] +fn execute_program_rejects_overriding_final_eval_interface_constant() { + let program = parse_fragment( + br#"interface EvalFinalConstIface { + final public const SEED = 1; +} +interface EvalFinalConstChildIface extends EvalFinalConstIface { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final interface constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let program = parse_fragment( + br#"interface EvalFinalImplConstIface { + final public const SEED = 1; +} +class EvalFinalImplConstBox implements EvalFinalImplConstIface { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("class overriding final interface constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies trait constants are readable directly and from classes using the trait. +#[test] +fn execute_program_reads_eval_trait_constants() { + let program = parse_fragment( + br#"trait EvalConstReusableTrait { + public const SEED = 6; + public static function readTraitSeed() { + return self::SEED; + } +} +class EvalConstTraitBox { + use EvalConstReusableTrait; +} +echo EvalConstReusableTrait::SEED; echo ":"; +echo EvalConstTraitBox::SEED; echo ":"; +return EvalConstTraitBox::readTraitSeed();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:6:"); + assert_eq!(values.get(result), FakeValue::Int(6)); +} + +/// Verifies compatible same-name trait constants are deduplicated during composition. +#[test] +fn execute_program_allows_compatible_eval_trait_constant_conflicts() { + let program = parse_fragment( + br#"trait EvalConstCompatibleA { + public const SEED = 6; +} +trait EvalConstCompatibleB { + public const SEED = 6; +} +class EvalConstCompatibleTraitBox { + use EvalConstCompatibleA, EvalConstCompatibleB; +} +class EvalConstCompatibleClassBox { + use EvalConstCompatibleA; + public const SEED = 6; +} +echo EvalConstCompatibleTraitBox::SEED; echo ":"; +return EvalConstCompatibleClassBox::SEED;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:"); + assert_eq!(values.get(result), FakeValue::Int(6)); +} + +/// Verifies incompatible same-name class and trait constants fail like PHP. +#[test] +fn execute_program_rejects_incompatible_eval_trait_constant_conflicts() { + let class_conflict = parse_fragment( + br#"trait EvalConstClassTraitConflict { + public const SEED = 6; +} +class EvalConstClassTraitConflictBox { + use EvalConstClassTraitConflict; + public const SEED = 7; +}"#, + ) + .expect("parse class/trait constant conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&class_conflict, &mut scope, &mut values) + .expect_err("incompatible class/trait constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let trait_conflict = parse_fragment( + br#"trait EvalConstTraitConflictA { + public const SEED = 6; +} +trait EvalConstTraitConflictB { + public const SEED = 7; +} +class EvalConstTraitConflictBox { + use EvalConstTraitConflictA, EvalConstTraitConflictB; +}"#, + ) + .expect("parse trait/trait constant conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&trait_conflict, &mut scope, &mut values) + .expect_err("incompatible trait/trait constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs b/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs new file mode 100644 index 0000000000..3092ac8f23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs @@ -0,0 +1,381 @@ +//! Purpose: +//! Interpreter tests for asymmetric property visibility and interface, +//! abstract-class, inheritance, and readonly compatibility rules. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Read and write visibility are exercised from owner, hierarchy, and external +//! scopes. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared asymmetric properties allow owner and subclass writes as PHP does. +#[test] +fn execute_program_allows_asymmetric_property_writes_from_allowed_scopes() { + let program = parse_fragment( + br#"class EvalAsymWriteBase { + public private(set) int $privateValue = 1; + public protected(set) string $protectedName = "base"; + public function ownerWrite($value, $name) { + $this->privateValue = $value; + $this->protectedName = $name; + } +} +class EvalAsymWriteChild extends EvalAsymWriteBase { + public function childWrite($name) { + $this->protectedName = $name; + } +} +$box = new EvalAsymWriteChild(); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->ownerWrite(7, "owner"); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->childWrite("child"); +echo $box->protectedName; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:base:7:owner:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `private(set)` throws Error without dispatching `__set`. +#[test] +fn execute_program_private_set_property_write_outside_declaring_class_throws_error() { + let program = parse_fragment( + br#"class EvalAsymPrivateSetBox { + public private(set) int $value = 1; + public function __set($name, $value) { + echo "bad"; + } +} +$box = new EvalAsymPrivateSetBox(); +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify private(set) property EvalAsymPrivateSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `protected(set)` throws Error for global writes. +#[test] +fn execute_program_protected_set_property_write_outside_hierarchy_throws_error() { + let program = parse_fragment( + br#"class EvalAsymProtectedSetBox { + public protected(set) int $value = 1; +} +$box = new EvalAsymProtectedSetBox(); +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify protected(set) property EvalAsymProtectedSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public interface set contract. +#[test] +fn execute_program_rejects_private_set_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalAsymSetContract { + public int $value { get; set; } +} +class EvalAsymSetContractBox implements EvalAsymSetContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public interface set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public abstract set contract. +#[test] +fn execute_program_rejects_private_set_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalAsymAbstractSetBase { + abstract public int $value { get; set; } +} +class EvalAsymAbstractSetBox extends EvalAsymAbstractSetBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public abstract set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval interface protected(set) property contracts accept compatible implementations. +#[test] +fn execute_program_allows_interface_protected_set_property_contract() { + let program = parse_fragment( + br#"interface EvalAsymProtectedSetContract { + public protected(set) string $name { get; set; } +} +class EvalAsymProtectedSetBase implements EvalAsymProtectedSetContract { + public protected(set) string $name = "base"; +} +class EvalAsymProtectedSetChild extends EvalAsymProtectedSetBase { + public function rename($name) { $this->name = $name; } +} +$box = new EvalAsymProtectedSetChild(); +echo $box->name; echo ":"; +$box->rename("child"); +echo $box->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "base:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private(set) interface contracts are final and cannot be implemented by a class. +#[test] +fn execute_program_rejects_private_set_interface_property_contract_implementation() { + let program = parse_fragment( + br#"interface EvalAsymPrivateSetInterfaceContract { + public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetInterfaceBox implements EvalAsymPrivateSetInterfaceContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) interface contract should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies private(set) abstract properties behave as final contracts. +#[test] +fn execute_program_rejects_private_set_abstract_property_redeclaration() { + let program = parse_fragment( + br#"abstract class EvalAsymPrivateSetAbstractBase { + abstract public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetAbstractBox extends EvalAsymPrivateSetAbstractBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) abstract property should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval property redeclarations may widen visibility while preserving invariant types. +#[test] +fn execute_program_accepts_compatible_property_redeclarations() { + let program = parse_fragment( + br#"class EvalPropertyRedeclareBase { + protected int|string $value; +} +class EvalPropertyRedeclareChild extends EvalPropertyRedeclareBase { + public string|int $value; +} +class EvalPropertyRelativeBase { + public self $selfValue; + public EvalPropertyRelativeBase $parentValue; +} +class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { + public self $selfValue; + public parent $parentValue; +} +class EvalPropertyReadonlyAddBase { + public int $count = 0; +} +class EvalPropertyReadonlyAddChild extends EvalPropertyReadonlyAddBase { + public readonly int $count; + public function __construct() { $this->count = 7; } +} +class EvalPropertyReadonlyWidenBase { + protected int $count = 0; + public function count() { return $this->count; } +} +class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { + public readonly int $count; + public function __construct() { $this->count = 9; } +} +$box = new EvalPropertyRedeclareChild(); +$box->value = "ok"; +$readonly = new EvalPropertyReadonlyAddChild(); +$widened = new EvalPropertyReadonlyWidenChild(); +return $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok:7:9:9".to_string())); +} + +/// Verifies eval rejects inherited property redeclarations that violate PHP invariance. +#[test] +fn execute_program_rejects_incompatible_property_redeclarations() { + let incompatible_type = parse_fragment( + br#"class EvalPropertyTypeBase { + public int $value; +} +class EvalPropertyStringChild extends EvalPropertyTypeBase { + public string $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible inherited property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_visibility = parse_fragment( + br#"class EvalPropertyPublicBase { + public int $value; +} +class EvalPropertyProtectedChild extends EvalPropertyPublicBase { + protected int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let typed_from_untyped = parse_fragment( + br#"class EvalPropertyUntypedBase { + public $value; +} +class EvalPropertyTypedChild extends EvalPropertyUntypedBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&typed_from_untyped, &mut scope, &mut values) + .expect_err("typed inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_mismatch = parse_fragment( + br#"class EvalPropertyStaticBase { + public static int $value; +} +class EvalPropertyInstanceChild extends EvalPropertyStaticBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_mismatch, &mut scope, &mut values) + .expect_err("static inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_mismatch = parse_fragment( + br#"class EvalPropertyReadonlyBase { + public readonly int $value; +} +class EvalPropertyMutableChild extends EvalPropertyReadonlyBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_mismatch, &mut scope, &mut values) + .expect_err("readonly inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_write_visibility = parse_fragment( + br#"class EvalPropertyProtectedSetBase { + public protected(set) int $value; +} +class EvalPropertyPrivateSetChild extends EvalPropertyProtectedSetBase { + public private(set) int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_write_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property write visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly class inheritance requires matching readonly status. +#[test] +fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { + let program = parse_fragment( + br#"class EvalReadonlyParentMismatchBase {} +readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatchBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class cannot extend non-readonly parent"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs b/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs new file mode 100644 index 0000000000..4b99223b84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs @@ -0,0 +1,205 @@ +//! Purpose: +//! Interpreter tests for class-related builtin attributes and readonly +//! inheritance constraints. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Attribute targets and `Override` declarations are validated at eval time. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval validates PHP's global `#[Override]` method marker. +#[test] +fn execute_program_validates_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalOverrideContract { + public function label(): string; +} +class EvalOverrideBase { + public function name(): string { return "base"; } +} +class EvalOverrideChild extends EvalOverrideBase implements EvalOverrideContract { + #[\Override] + public function name(): string { return "child"; } + #[Override] + public function label(): string { return "contract"; } +} +$box = new EvalOverrideChild(); +echo $box->name() . ":" . $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:contract"); + + let invalid = parse_fragment( + br#"class EvalOverrideMissing { + #[\Override] + public function missing(): string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("override marker without target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies interface `#[Override]` methods require an inherited interface method. +#[test] +fn execute_program_validates_interface_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalIfaceOverrideParent { + public function label(): string; +} +interface EvalIfaceOverrideChild extends EvalIfaceOverrideParent { + #[\Override] + public function label(): string; +} +class EvalIfaceOverrideImpl implements EvalIfaceOverrideChild { + public function label(): string { return "child"; } +} +$box = new EvalIfaceOverrideImpl(); +echo $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child"); + + let builtin_parent = parse_fragment( + br#"interface EvalIfaceOverrideStringable extends Stringable { + #[\Override] + public function __toString(): string; +} +class EvalIfaceOverrideStringableImpl implements EvalIfaceOverrideStringable { + public function __toString(): string { return "stringable"; } +} +$box = new EvalIfaceOverrideStringableImpl(); +echo $box;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&builtin_parent, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stringable"); + + let invalid = parse_fragment( + br#"interface EvalIfaceOverrideMissing { + #[\Override] + public function missing(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("interface override marker without parent method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects global builtin attributes on unsupported OOP targets. +#[test] +fn execute_program_rejects_invalid_builtin_attribute_targets() { + let cases: &[(&[u8], &str)] = &[ + ( + br#"#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}"#, + "AllowDynamicProperties interface", + ), + ( + br#"#[\AllowDynamicProperties] trait EvalInvalidAttrTrait {}"#, + "AllowDynamicProperties trait", + ), + ( + br#"#[\AllowDynamicProperties] enum EvalInvalidAttrEnum { case Ready; }"#, + "AllowDynamicProperties enum", + ), + ( + br#"#[\Override] class EvalInvalidAttrClass {}"#, + "Override class", + ), + ( + br#"class EvalInvalidAttrProperty { #[\Override] public int $value; }"#, + "Override property", + ), + ( + br#"class EvalInvalidAttrConstant { #[\AllowDynamicProperties] public const VALUE = 1; }"#, + "AllowDynamicProperties constant", + ), + ( + br#"class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }"#, + "AllowDynamicProperties method", + ), + ( + br#"enum EvalInvalidAttrCase { #[\AllowDynamicProperties] case Ready; }"#, + "AllowDynamicProperties enum case", + ), + ]; + + for &(source, label) in cases { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies readonly classes leave static properties mutable like ordinary classes. +#[test] +fn execute_program_allows_readonly_class_static_property() { + let program = parse_fragment( + br#"readonly class EvalReadonlyStaticBox { + public static int $count = 1; +} +EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; +echo EvalReadonlyStaticBox::$count;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies readonly classes may extend readonly parents and use inherited constructors. +#[test] +fn execute_program_allows_readonly_class_extending_readonly_parent() { + let program = parse_fragment( + br#"readonly class EvalReadonlyParentBase { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +readonly class EvalReadonlyParentChild extends EvalReadonlyParentBase {} +$box = new EvalReadonlyParentChild(13); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "13:"); + assert_eq!(values.get(result), FakeValue::Int(13)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/basics.rs b/crates/elephc-magician/src/interpreter/tests/classes/basics.rs new file mode 100644 index 0000000000..b11ae0ec9b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/basics.rs @@ -0,0 +1,430 @@ +//! Purpose: +//! Basic interpreter tests for eval-declared construction, inheritance, +//! properties, static dispatch, builtin contracts, and `ArrayAccess`. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover class property semantics that need eval runtime state. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies promoted constructor properties initialize before the constructor body runs. +#[test] +fn execute_program_initializes_constructor_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedUser { + public function __construct(public int $id, private string $name = "Ada") { + $this->id = $this->id + 1; + } + public function label() { return $this->id . ":" . $this->name; } +} +$user = new EvalPromotedUser(6); +echo $user->id; echo ":"; +return $user->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); +} + +/// Verifies `new self/static/parent` resolve inside eval-declared methods. +#[test] +fn execute_program_constructs_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"class EvalRelativeFactoryBase { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { + public function parentFactory() { return new parent("parent"); } +} +$child = new EvalRelativeFactoryChild("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof EvalRelativeFactoryBase + && !($self instanceof EvalRelativeFactoryChild) + && $static instanceof EvalRelativeFactoryChild + && $parent instanceof EvalRelativeFactoryBase + && !($parent instanceof EvalRelativeFactoryChild);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `new self/static/parent` stay relative inside eval namespaces. +#[test] +fn execute_program_constructs_namespaced_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"namespace EvalRelativeNs; +class Base { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class Child extends Base { + public function parentFactory() { return new parent("parent"); } +} +$child = new Child("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof Base + && !($self instanceof Child) + && $static instanceof Child + && $parent instanceof Base + && !($parent instanceof Child);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies PHP legacy `var` properties behave as public eval properties. +#[test] +fn execute_program_supports_legacy_var_properties() { + let program = parse_fragment( + br#"trait EvalLegacyVarTrait { + var ?string $label = "trait"; +} +class EvalLegacyVarProperty { + use EvalLegacyVarTrait; + var $plain = "p"; + var ?int $count = null; +} +$object = new EvalLegacyVarProperty(); +$plain = new ReflectionProperty("EvalLegacyVarProperty", "plain"); +$count = new ReflectionProperty("EvalLegacyVarProperty", "count"); +$label = new ReflectionProperty("EvalLegacyVarProperty", "label"); +$defaults = (new ReflectionClass("EvalLegacyVarProperty"))->getDefaultProperties(); +echo $object->plain; echo ":"; +echo $plain->isPublic() ? "P" : "p"; echo ":"; +echo $plain->hasType() ? "T" : "t"; echo ":"; +echo $count->isPublic() ? "C" : "c"; echo ":"; +echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; +echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; +echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; +echo $object->label; echo ":"; +echo $label->isPublic() ? "L" : "l"; echo ":"; +echo $label->getType()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p:P:t:C:int:N:null:trait:L:string"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies comma-separated eval properties initialize instance, static, and trait storage. +#[test] +fn execute_program_reads_comma_separated_eval_properties() { + let program = parse_fragment( + br#"class EvalMultiPropertyBox { + public int $a = 1, $b = 2; + public static int $s = 3, $t = 4; + public function sum() { return $this->a + $this->b + self::$s + self::$t; } +} +trait EvalMultiPropertyTrait { + public int $x = 5, $y = 6; +} +class EvalMultiPropertyTraitBox { + use EvalMultiPropertyTrait; + public function sum() { return $this->x + $this->y; } +} +$box = new EvalMultiPropertyBox(); +$traitBox = new EvalMultiPropertyTraitBox(); +echo $box->a; echo $box->b; echo ":"; +echo EvalMultiPropertyBox::$s; echo EvalMultiPropertyBox::$t; echo ":"; +echo $traitBox->x; echo $traitBox->y; echo ":"; +return $box->sum() + $traitBox->sum();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:34:56:"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} + +/// Verifies eval static method calls preserve PHP late-static forwarding rules. +#[test] +fn execute_program_forwards_eval_static_method_called_class() { + let program = parse_fragment( + br#"class EvalForwardA { + public static function who() { return static::tag(); } + public static function relayNamed() { return EvalForwardA::who(); } + public static function relaySelf() { return self::who(); } + public static function tag() { return "A"; } +} +class EvalForwardB extends EvalForwardA { + public static function relayParent() { return parent::who(); } + public static function relayStatic() { return static::who(); } + public static function tag() { return "B"; } +} +echo EvalForwardB::relayNamed(); echo ":"; +echo EvalForwardB::relaySelf(); echo ":"; +echo EvalForwardB::relayParent(); echo ":"; +return EvalForwardB::relayStatic();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:B:B:"); + assert_eq!(values.get(result), FakeValue::String("B".to_string())); +} + +/// Verifies `get_called_class()` follows eval late-static method scopes. +#[test] +fn execute_program_dispatches_get_called_class_builtin() { + let program = parse_fragment( + br#"class EvalCalledClassBase { + public function instanceWho() { return get_called_class(); } + public function instanceCall() { return call_user_func("get_called_class"); } + public static function staticWho() { return get_called_class(); } + public static function staticCallArray() { return call_user_func_array("get_called_class", []); } + public static function makeCallable() { return get_called_class(...); } +} +class EvalCalledClassChild extends EvalCalledClassBase {} +$child = new EvalCalledClassChild(); +echo $child->instanceWho(); echo ":"; +echo $child->instanceCall(); echo ":"; +echo EvalCalledClassChild::staticWho(); echo ":"; +echo EvalCalledClassChild::staticCallArray(); echo ":"; +echo EvalCalledClassBase::staticWho(); echo ":"; +try { + get_called_class(); +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; +} +$fn = EvalCalledClassChild::makeCallable(); +try { + $fn(); +} catch (Error $e) { + echo "callable:"; +} +echo function_exists("get_called_class"); echo is_callable("get_called_class"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassBase:Error:get_called_class() must be called from within a class:callable:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. +#[test] +fn execute_program_extends_runtime_class_from_eval_declaration() { + let program = parse_fragment( + br#"class EvalRuntimeParentChild extends KnownClass { + public function own() { return $this->read_x() + 1; } +} +$box = new EvalRuntimeParentChild(9); +echo get_class($box); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; +echo is_a($box, "KnownClass") ? "K" : "k"; echo ":"; +echo is_a($box, "KnownInterface") ? "I" : "i"; echo ":"; +echo is_subclass_of($box, "KnownClass") ? "S" : "s"; echo ":"; +echo is_subclass_of("EvalRuntimeParentChild", "KnownClass") ? "N" : "n"; echo ":"; +echo $box->read_x(); echo ":"; +return $box->own();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRuntimeParentChild:KnownClass:D:K:I:S:N:9:" + ); + assert_eq!(values.get(result), FakeValue::Int(10)); +} + +/// Verifies eval classes cannot directly implement PHP's special Throwable contract. +#[test] +fn execute_program_rejects_eval_class_implementing_throwable_contracts() { + for (source, label) in [ + ( + br#"class EvalInvalidThrowableClass implements Throwable {}"# as &[u8], + "direct Throwable implementation should fail", + ), + ( + br#"interface EvalThrowableMarker extends Throwable {} +class EvalInvalidThrowableMarkerClass implements EvalThrowableMarker {}"#, + "Throwable-derived interface implementation should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies eval classes must satisfy methods required by PHP builtin interfaces. +#[test] +fn execute_program_rejects_invalid_builtin_interface_implementations() { + for (source, label) in [ + ( + br#"class EvalMissingCountable implements Countable {}"# as &[u8], + "missing Countable::count should fail", + ), + ( + br#"class EvalBadCountableReturn implements Countable { + public function count(): string { return "1"; } +}"#, + "incompatible Countable::count return type should fail", + ), + ( + br#"class EvalMissingStringable implements Stringable {}"#, + "missing Stringable::__toString should fail", + ), + ( + br#"class EvalBadIterator implements Iterator { + public function current(): mixed { return null; } + public function key(): mixed { return null; } + public function next(): void {} + public function valid(): bool { return false; } +}"#, + "missing Iterator::rewind should fail", + ), + ( + br#"class EvalBadJsonSerializable implements JsonSerializable { + public static function jsonSerialize(): mixed { return []; } +}"#, + "static JsonSerializable::jsonSerialize should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies abstract eval classes can defer PHP builtin interface methods. +#[test] +fn execute_program_allows_abstract_builtin_interface_implementations() { + let program = parse_fragment( + br#"abstract class EvalAbstractCountable implements Countable {} +return class_exists("EvalAbstractCountable");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. +#[test] +fn execute_program_dispatches_eval_array_access_objects() { + let program = parse_fragment( + br#"class EvalArrayAccessBox implements ArrayAccess { + public function offsetExists(mixed $offset): bool { + echo "exists:" . $offset . ":"; + if ($offset === "missing") { + return false; + } + return true; + } + public function offsetGet(mixed $offset): mixed { + echo "get:" . $offset . ":"; + if ($offset === "empty") { + return ""; + } + return "v" . $offset; + } + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + echo "set:null:" . $value . ":"; + } else { + echo "set:" . $offset . ":" . $value . ":"; + } + } + public function offsetUnset(mixed $offset): void { + echo "unset:" . $offset . ":"; + } +} +$box = new EvalArrayAccessBox(); +$box["x"] = "1"; +$box[] = "tail"; +unset($box["drop"]); +if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } +if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } +if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } +if (empty($box["missing"])) { echo "N:"; } else { echo "n:"; } +return $box["y"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "set:x:1:set:null:tail:unset:drop:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" + ); + assert_eq!(values.get(result), FakeValue::String("vy".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs b/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs new file mode 100644 index 0000000000..eb99913fbe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs @@ -0,0 +1,396 @@ +//! Purpose: +//! Interpreter tests for inherited, interface, abstract, and trait property-hook +//! contracts. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests distinguish get-only, set-required, readonly, type, and final/abstract +//! compatibility failures. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies get-only property hooks throw Error on writes outside a set accessor. +#[test] +fn execute_program_write_to_get_only_eval_property_hook_throws_error() { + let program = parse_fragment( + br#"class EvalHookReadOnly { + public int $answer { + get => 42; + } +} +$box = new EvalHookReadOnly(); +try { + $box->answer = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Property EvalHookReadOnly::$answer is read-only" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval subclasses inherit parent property hooks. +#[test] +fn execute_program_inherits_eval_property_hooks() { + let program = parse_fragment( + br#"class EvalHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalHookChild extends EvalHookBase { + public function shout() { return $this->value . "?"; } +} +$box = new EvalHookChild(); +$box->value = "Ada"; +echo $box->value; echo ":"; +return $box->shout();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!:"); + assert_eq!(values.get(result), FakeValue::String("Ada!?".to_string())); +} + +/// Verifies eval interface property hook contracts are enforced through inheritance. +#[test] +fn execute_program_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface EvalHookContract { + public string $value { get; set; } +} +interface EvalNamedHookContract extends EvalHookContract { + public string $name { get; } +} +class EvalHookContractBox implements EvalNamedHookContract { + public string $name = "box"; + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalHookContractBox(); +$box->value = "Ada"; +echo $box->name; echo ":"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "box:Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies a normal public mutable property satisfies an eval interface get/set contract. +#[test] +fn execute_program_accepts_plain_property_for_interface_hook_contracts() { + let program = parse_fragment( + br#"interface EvalPlainHookContract { + public string $value { get; set; } +} +class EvalPlainHookContractBox implements EvalPlainHookContract { + public string $value = "Ada"; +} +$box = new EvalPlainHookContractBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies interface property hook types are checked on abstract and concrete classes. +#[test] +fn execute_program_validates_interface_property_hook_types() { + let valid_program = parse_fragment( + br#"interface EvalIfaceGetWide { + public int|string $value { get; } +} +interface EvalIfaceSetNarrow { + public int $slot { set; } +} +abstract class EvalIfacePropertyDeferred implements EvalIfaceGetWide {} +abstract class EvalIfacePropertyGood implements EvalIfaceGetWide, EvalIfaceSetNarrow { + abstract public int $value { get; } + abstract public int|string $slot { set; } +} +class EvalIfacePropertyConcrete implements EvalIfaceGetWide { + public int $value = 4; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + + let bad_abstract_get = parse_fragment( + br#"interface EvalIfaceGetInt { + public int $value { get; } +} +abstract class EvalIfaceGetWideBad implements EvalIfaceGetInt { + abstract public int|string $value { get; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_get, &mut scope, &mut values) + .expect_err("wider abstract get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_set = parse_fragment( + br#"interface EvalIfaceSetWide { + public int|string $value { set; } +} +abstract class EvalIfaceSetNarrowBad implements EvalIfaceSetWide { + abstract public int $value { set; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_set, &mut scope, &mut values) + .expect_err("narrower abstract set property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_concrete_get = parse_fragment( + br#"interface EvalIfaceConcreteGetInt { + public int $value { get; } +} +class EvalIfaceConcreteGetWideBad implements EvalIfaceConcreteGetInt { + public int|string $value = 4; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_concrete_get, &mut scope, &mut values) + .expect_err("wider concrete get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_property = parse_fragment( + br#"interface EvalIfaceInheritedGet { + public int $value { get; } +} +abstract class EvalIfaceInheritedPropertyBase { + public string $value = "bad"; +} +abstract class EvalIfaceInheritedPropertyChild extends EvalIfaceInheritedPropertyBase implements EvalIfaceInheritedGet {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_property, &mut scope, &mut values) + .expect_err("inherited incompatible interface property should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies a get-only hook cannot satisfy a writable eval interface contract. +#[test] +fn execute_program_rejects_get_only_hook_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalHookSetContract { + public int $answer { get; set; } +} +class EvalHookGetOnlyContractBox implements EvalHookSetContract { + public int $answer { + get => 42; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("get-only hook should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly properties cannot satisfy writable eval interface contracts. +#[test] +fn execute_program_rejects_readonly_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalReadonlyHookContract { + public int $id { get; set; } +} +class EvalReadonlyHookContractBox implements EvalReadonlyHookContract { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies concrete eval subclasses satisfy abstract property hook contracts. +#[test] +fn execute_program_accepts_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalAbstractHookBox extends EvalAbstractHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalAbstractHookBox(); +$box->value = "Ada"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies normal mutable properties satisfy abstract get/set hook contracts. +#[test] +fn execute_program_accepts_plain_property_for_abstract_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalPlainAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalPlainAbstractHookBox extends EvalPlainAbstractHookBase { + public string $value = "Ada"; +} +$box = new EvalPlainAbstractHookBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies concrete eval subclasses must declare inherited abstract properties. +#[test] +fn execute_program_rejects_missing_abstract_property_hook_contract() { + let program = parse_fragment( + br#"abstract class EvalMissingAbstractHookBase { + abstract public string $value { get; } +} +class EvalMissingAbstractHookBox extends EvalMissingAbstractHookBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing abstract property should fail concrete subclass"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract final eval properties are rejected while parsing. +#[test] +fn parse_fragment_rejects_final_abstract_property_hook_contract() { + let err = parse_fragment( + br#"abstract class EvalFinalAbstractHookBase { + abstract final public string $value { get; } +}"#, + ) + .expect_err("final abstract property should fail"); + + assert_eq!(err, EvalParseError::UnsupportedConstruct); +} + +/// Verifies readonly properties cannot satisfy abstract writable hook contracts. +#[test] +fn execute_program_rejects_readonly_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalReadonlyAbstractHookBase { + abstract public int $id { get; set; } +} +class EvalReadonlyAbstractHookBox extends EvalReadonlyAbstractHookBase { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail abstract writable contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract trait property hook contracts are enforced after trait expansion. +#[test] +fn execute_program_enforces_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait EvalTraitNeedsName { + abstract protected string $name { get; } + public function label() { return $this->name; } +} +class EvalTraitNameBox { + use EvalTraitNeedsName; + protected string $name = "Ada"; +} +$box = new EvalTraitNameBox(); +echo $box->label(); +return $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada"); + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs b/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs new file mode 100644 index 0000000000..8d0abbc4db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Interpreter tests for anonymous classes, cloning, clone visibility, and +//! destructor execution. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Lifecycle hooks are checked at their PHP-visible invocation boundaries. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies anonymous eval classes instantiate, reuse their synthetic class, and reflect as anonymous. +#[test] +fn execute_program_instantiates_anonymous_class_expressions() { + let program = parse_fragment( + br#"interface EvalAnonRuntimeLabel { + function label(); +} +class EvalAnonRuntimeBase { + protected string $prefix; + public function __construct($prefix) { $this->prefix = $prefix; } +} +function eval_anon_make($prefix) { + return new class($prefix) extends EvalAnonRuntimeBase implements EvalAnonRuntimeLabel { + public function label() { return $this->prefix . ":anon"; } + }; +} +$first = eval_anon_make("A"); +$second = eval_anon_make("B"); +echo $first->label(); echo ":"; +echo $second->label(); echo ":"; +echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; +$ref = new ReflectionClass(get_class($first)); +echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; +echo $ref->implementsInterface("EvalAnonRuntimeLabel") ? "iface" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:anon:B:anon:same:anonymous:iface"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly anonymous eval classes initialize and reject property writes. +#[test] +fn execute_program_instantiates_readonly_anonymous_class_expressions() { + let program = parse_fragment( + br#"$box = new readonly class("frozen") { + public function __construct(public string $label) {} +}; +echo $box->label; echo ":"; +try { + $box->label = "bad"; + echo "bad"; +} catch (Error $e) { + echo get_class($e); +} +return $box->label;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "frozen:Error"); + assert_eq!(values.get(result), FakeValue::String("frozen".to_string())); +} + +/// Verifies eval object cloning copies properties before running `__clone()`. +#[test] +fn execute_program_clones_eval_object_and_runs_clone_hook() { + let program = parse_fragment( + br#"class EvalCloneRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __clone() { $this->name = $this->name . ":clone"; } +} +$first = new EvalCloneRuntimeBox("A"); +$second = clone $first; +echo $first->name; echo ":"; +echo $second->name; +$second->name = "B"; +return $first->name . ":" . $second->name;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:clone"); + assert_eq!(values.get(result), FakeValue::String("A:B".to_string())); +} + +/// Verifies private `__clone()` can be invoked from inside the declaring eval class. +#[test] +fn execute_program_allows_private_clone_hook_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateBox { + public string $name = "A"; + private function __clone() { $this->name = $this->name . ":copy"; } + public function copy() { return clone $this; } +} +$first = new EvalCloneRuntimePrivateBox(); +$second = $first->copy(); +echo $first->name; echo ":"; +echo $second->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:copy"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `__destruct()` runs for explicit unset and discarded temporaries. +#[test] +fn execute_program_runs_eval_destructor_on_final_release() { + let program = parse_fragment( + br#"class EvalDestructRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalDestructRuntimeBox("A"); +unset($box); +new EvalDestructRuntimeBox("B"); +echo "after"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "drop:A:drop:B:after"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private `__clone()` throws Error through a global clone expression. +#[test] +fn execute_program_private_clone_hook_outside_declaring_class_throws_error() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateFail { + private function __clone() {} +} +$box = new EvalCloneRuntimePrivateFail(); +try { + clone $box; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to private EvalCloneRuntimePrivateFail::__clone() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs b/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs new file mode 100644 index 0000000000..4e28c289af --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs @@ -0,0 +1,434 @@ +//! Purpose: +//! Interpreter tests for eval magic property, string-conversion, debug, and +//! state-restoration methods. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Dispatch, visibility, existing dynamic properties, and PHP magic-method +//! contracts are covered separately. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. +#[test] +fn execute_program_dispatches_eval_magic_get_and_set() { + let program = parse_fragment( + br#"class EvalMagicPropertyBox { + public string $events = ""; + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPropertyBox(); +echo $box->missing; echo ":"; +$box->other = "B"; +$box->events = $box->events . "public;"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "value:missing:"); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;public;".to_string()) + ); +} + +/// Verifies eval invokes non-public magic methods that PHP accepts with warnings. +#[test] +fn execute_program_dispatches_non_public_eval_magic_methods() { + let program = parse_fragment( + br#"class EvalNonPublicMagicBox { + public string $events = ""; + protected function __get(string $name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + protected function __set(string $name, $value): void { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } + private function __isset(string $name): bool { + $this->events = $this->events . "isset:" . $name . ";"; + return true; + } + private function __unset(string $name): void { + $this->events = $this->events . "unset:" . $name . ";"; + } + private function __call(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private static function __callStatic(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private function __invoke(string $left = "I", string $right = "J") { + return "invoke:" . $left . $right; + } +} +$box = new EvalNonPublicMagicBox(); +echo is_callable($box) ? "callable:" : "bad:"; +echo $box->missing; echo ":"; +$box->other = "B"; +echo isset($box->probe) ? "isset:" : "bad:"; +unset($box->gone); +echo $box->run("A", name: "B"); echo ":"; +echo EvalNonPublicMagicBox::staticRun("C", name: "D"); echo ":"; +echo $box(right: "F", left: "E"); +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "callable:value:missing:isset:run:A:B:staticRun:C:D:invoke:EF" + ); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;isset:probe;unset:gone;".to_string()) + ); +} + +/// Verifies inaccessible eval properties dispatch through magic property methods. +#[test] +fn execute_program_dispatches_inaccessible_eval_properties_to_magic_methods() { + let program = parse_fragment( + br#"class EvalMagicPrivatePropertyBox { + private string $secret = "raw"; + public string $events = ""; + public function readOwn() { return $this->secret; } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "read:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPrivatePropertyBox(); +echo $box->readOwn(); echo ":"; +echo $box->secret; echo ":"; +$box->secret = "new"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "raw:read:secret:"); + assert_eq!( + values.get(result), + FakeValue::String("get:secret;set:secret=new;".to_string()) + ); +} + +/// Verifies dynamic properties created without `__set` are read directly even when `__get` exists. +#[test] +fn execute_program_reads_existing_dynamic_property_before_magic_get() { + let program = parse_fragment( + br#"class EvalMagicExistingDynamicBox { + public function __get($name) { + return "magic:" . $name; + } +} +$box = new EvalMagicExistingDynamicBox(); +$box->known = "plain"; +echo $box->known; echo ":"; +return $box->missing;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "plain:"); + assert_eq!( + values.get(result), + FakeValue::String("magic:missing".to_string()) + ); +} + +/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. +#[test] +fn execute_program_dispatches_eval_magic_isset_empty_and_unset() { + let program = parse_fragment( + br#"class EvalMagicPropertyProbeBox { + public string $events = ""; + public string $present = "ready"; + public $nullish = null; + private string $secret = "raw"; + public function __isset($name) { + $this->events = $this->events . "isset:" . $name . ";"; + return $name !== "no"; + } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return $name === "empty" ? "" : "value:" . $name; + } + public function __unset($name) { + $this->events = $this->events . "unset:" . $name . ";"; + } +} +$box = new EvalMagicPropertyProbeBox(); +echo isset($box->present) ? "P" : "p"; echo ":"; +echo isset($box->nullish) ? "N" : "n"; echo ":"; +echo isset($box->secret) ? "S" : "s"; echo ":"; +echo isset($box->no) ? "bad" : "no"; echo ":"; +echo empty($box->secret) ? "bad" : "filled"; echo ":"; +echo empty($box->empty) ? "empty" : "bad"; echo ":"; +unset($box->present); +unset($box->secret); +unset($box->missing); +echo isset($box->present) ? "bad" : "unset"; echo ":"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "P:n:S:no:filled:empty:unset:"); + assert_eq!( + values.get(result), + FakeValue::String( + "isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" + .to_string() + ) + ); +} + +/// Verifies eval objects stringify through public `__toString()` in PHP string contexts. +#[test] +fn execute_program_dispatches_eval_magic_tostring_for_string_contexts() { + let program = parse_fragment( + br#"class EvalStringableBox { + public string $name = "Ada"; + public function __toString() { + return "box:" . $this->name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalStringableBox(); +echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +return $box->accepts($box);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:" + ); + assert_eq!( + values.get(result), + FakeValue::String("typed:box:Ada".to_string()) + ); +} + +/// Verifies eval objects without `__toString()` fail in PHP string contexts. +#[test] +fn execute_program_rejects_eval_object_string_context_without_tostring() { + let program = parse_fragment( + br#"class EvalPlainStringContext {} +$box = new EvalPlainStringContext(); +try { + echo $box; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Object of class EvalPlainStringContext could not be converted to string" + ); +} + +/// Verifies eval rejects magic methods whose staticness, arity, or fatal contracts are invalid. +#[test] +fn execute_program_rejects_invalid_eval_magic_method_contracts() { + let cases: Vec<(&[u8], &str)> = vec![ + ( + br#"class EvalBadToString { private function __toString() { return "x"; } }"#.as_slice(), + "private __toString", + ), + ( + br#"class EvalBadToStringReturn { public function __toString(): int { return 1; } }"#.as_slice(), + "bad __toString return type", + ), + ( + br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), + "by-ref __get", + ), + ( + br#"class EvalBadGetParamType { public function __get(int $name) { return "x"; } }"#.as_slice(), + "bad __get parameter type", + ), + ( + br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), + "bad __isset return type", + ), + ( + br#"class EvalBadUnsetReturn { public function __unset($name): int { return 1; } }"#.as_slice(), + "bad __unset return type", + ), + ( + br#"class EvalBadSetReturn { public function __set($name, $value): int { return 1; } }"#.as_slice(), + "bad __set return type", + ), + ( + br#"class EvalBadSetParamType { public function __set(int $name, $value): void {} }"#.as_slice(), + "bad __set parameter type", + ), + ( + br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), + "variadic __call", + ), + ( + br#"class EvalBadCallArgsType { public function __call(string $name, string $args) {} }"#.as_slice(), + "bad __call args type", + ), + ( + br#"class EvalBadCallNameType { public function __call(int $name, array $args) {} }"#.as_slice(), + "bad __call name type", + ), + ( + br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), + "instance __callStatic", + ), + ( + br#"class EvalBadCallStaticArgsType { public static function __callStatic(string $name, string $args) {} }"#.as_slice(), + "bad __callStatic args type", + ), + ( + br#"class EvalBadSleepReturn { public function __sleep(): string { return "x"; } }"#.as_slice(), + "bad __sleep return type", + ), + ( + br#"class EvalBadSerializeStatic { public static function __serialize(): array { return []; } }"#.as_slice(), + "static __serialize", + ), + ( + br#"class EvalBadWakeupArity { public function __wakeup($value): void {} }"#.as_slice(), + "bad __wakeup arity", + ), + ( + br#"class EvalBadUnserializeArity { public function __unserialize(): void {} }"#.as_slice(), + "bad __unserialize arity", + ), + ( + br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), + "bad __unserialize return type", + ), + ( + br#"class EvalBadUnserializeParam { public function __unserialize(string $data): void {} }"#.as_slice(), + "bad __unserialize parameter type", + ), + ( + br#"class EvalBadDebugInfoReturn { public function __debugInfo(): string { return "x"; } }"#.as_slice(), + "bad __debugInfo return type", + ), + ( + br#"class EvalBadDebugInfoStatic { public static function __debugInfo(): array { return []; } }"#.as_slice(), + "static __debugInfo", + ), + ( + br#"class EvalBadSetStateInstance { public function __set_state($data) {} }"#.as_slice(), + "instance __set_state", + ), + ( + br#"class EvalBadSetStateArity { public static function __set_state($data, $extra) {} }"#.as_slice(), + "bad __set_state arity", + ), + ( + br#"class EvalBadSetStateParam { public static function __set_state(string $data) {} }"#.as_slice(), + "bad __set_state parameter type", + ), + ( + br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), + "static __clone", + ), + ( + br#"class EvalBadCloneReturn { public function __clone(): int {} }"#.as_slice(), + "bad __clone return type", + ), + ( + br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), + "static __destruct", + ), + ( + br#"class EvalBadConstructReturn { public function __construct(): void {} }"#.as_slice(), + "bad __construct return type", + ), + ( + br#"class EvalBadDestructReturn { public function __destruct(): void {} }"#.as_slice(), + "bad __destruct return type", + ), + ( + br#"trait EvalBadMagicTrait { public static function __isset($name) { return true; } }"#.as_slice(), + "trait static __isset", + ), + ]; + + for (source, label) in cases { + let program = parse_fragment(source).expect(label); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect_err(label); + } +} + +/// Verifies eval accepts PHP-compatible debug and set-state magic method contracts. +#[test] +fn execute_program_accepts_debug_and_set_state_magic_contracts() { + let program = parse_fragment( + br#"class EvalGoodDebugInfoMagic { + public function __debugInfo(): ?array { return null; } +} +class EvalGoodSetStateMagic { + public static function __set_state($data) {} +} +return class_exists("EvalGoodDebugInfoMagic") && class_exists("EvalGoodSetStateMagic");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/mod.rs b/crates/elephc-magician/src/interpreter/tests/classes/mod.rs new file mode 100644 index 0000000000..8b166a68d3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/mod.rs @@ -0,0 +1,19 @@ +//! Purpose: +//! Organizes interpreter tests for eval-declared class runtime behavior by +//! property, lifecycle, hook, and contract responsibility. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve the original test names for stable exact filtering. + +mod asymmetric_properties; +mod attributes; +mod basics; +mod hook_contracts; +mod lifecycle; +mod magic_methods; +mod promoted_references; +mod property_hooks; +mod readonly; diff --git a/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs b/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs new file mode 100644 index 0000000000..fef311f02f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Interpreter tests for by-reference constructor-promoted property aliases. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Cases cover variables, array elements, object/static storage, nested paths, +//! defaults, and the readonly incompatibility. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies by-reference promoted properties stay aliased to caller variables. +#[test] +fn execute_program_aliases_by_reference_promoted_variable_properties() { + let program = parse_fragment( + br#"class EvalPromotedRefBox { + public function __construct(public &$value) {} +} +$value = 1; +$box = new EvalPromotedRefBox($value); +$box->value = 5; +echo $value; echo ":"; +$value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller array elements. +#[test] +fn execute_program_aliases_by_reference_promoted_array_element_properties() { + let program = parse_fragment( + br#"class EvalPromotedArrayRefBox { + public function __construct(public &$value) {} +} +$items = [1]; +$box = new EvalPromotedArrayRefBox($items[0]); +$box->value = 5; +echo $items[0]; echo ":"; +$items[0] = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller object properties. +#[test] +fn execute_program_aliases_by_reference_promoted_object_property_properties() { + let program = parse_fragment( + br#"class EvalPromotedObjectRefHolder { + public $value = 1; +} +class EvalPromotedObjectRefBox { + public function __construct(public &$value) {} +} +$holder = new EvalPromotedObjectRefHolder(); +$box = new EvalPromotedObjectRefBox($holder->value); +$box->value = 5; +echo $holder->value; echo ":"; +$holder->value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias static and nested property targets. +#[test] +fn execute_program_aliases_by_reference_promoted_static_and_nested_properties() { + let program = parse_fragment( + br#"class EvalPromotedStaticRefHolder { + public static $value = 1; + public $items = [1]; + public static $staticItems = [1]; +} +class EvalPromotedStaticRefBox { + public function __construct(public &$value) {} +} +$box = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$value); +$box->value = 5; +echo EvalPromotedStaticRefHolder::$value; echo ":"; +EvalPromotedStaticRefHolder::$value = 7; +echo $box->value; echo ":"; +$holder = new EvalPromotedStaticRefHolder(); +$itemBox = new EvalPromotedStaticRefBox($holder->items[0]); +$itemBox->value = 11; +echo $holder->items[0]; echo ":"; +$holder->items[0] = 13; +echo $itemBox->value; echo ":"; +$staticItemBox = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$staticItems[0]); +$staticItemBox->value = 17; +echo EvalPromotedStaticRefHolder::$staticItems[0]; echo ":"; +EvalPromotedStaticRefHolder::$staticItems[0] = 19; +return $staticItemBox->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7:11:13:17:"); + assert_eq!(values.get(result), FakeValue::Int(19)); +} + +/// Verifies by-reference promoted defaults use internal property alias storage. +#[test] +fn execute_program_aliases_by_reference_promoted_default_properties() { + let program = parse_fragment( + br#"class EvalPromotedDefaultRefBox { + public function __construct(public &$value = null) {} +} +$box = new EvalPromotedDefaultRefBox(); +$box->value = 5; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies readonly by-reference promotion fails when the constructor creates the alias. +#[test] +fn execute_program_rejects_readonly_by_reference_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyRefBox { + public function __construct(public readonly int &$value) {} +} +$value = 1; +new EvalPromotedReadonlyRefBox($value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly by-reference promoted property should fail at construction"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs b/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs new file mode 100644 index 0000000000..12bd1e9b89 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs @@ -0,0 +1,209 @@ +//! Purpose: +//! Interpreter tests for eval property get/set hooks and their type rules. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests cover by-reference getters, short setters, mixed-case access, and +//! parameter validation. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies a get-only property hook computes a virtual eval property. +#[test] +fn execute_program_reads_eval_property_get_hook() { + let program = parse_fragment( + br#"class EvalHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +$person = new EvalHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + +/// Verifies by-reference get hook syntax routes through the concrete eval get accessor. +#[test] +fn execute_program_reads_eval_by_ref_get_property_hook() { + let program = parse_fragment( + br#"class EvalByRefGetHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + &get => $this->first . " " . $this->last; + } +} +$person = new EvalByRefGetHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + +/// Verifies get/set property hooks can use the raw backing slot from inside accessors. +#[test] +fn execute_program_routes_eval_property_get_and_set_hooks() { + let program = parse_fragment( + br#"class EvalHookName { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$name = new EvalHookName(); +$name->value = "Ada"; +echo $name->value; +return $name->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies short set hooks assign their expression result into the raw backing slot. +#[test] +fn execute_program_routes_eval_short_set_property_hooks() { + let program = parse_fragment( + br#"class EvalShortSetHookName { + public string $value { + get => $this->value; + set => trim($value); + } +} +class EvalShortSetHookLabel { + public string $text { + get => $this->text; + set(string $raw) => strtoupper($raw); + } +} +$name = new EvalShortSetHookName(); +$name->value = " Ada "; +echo "[" . $name->value . "]:"; +$label = new EvalShortSetHookLabel(); +$label->text = "hi"; +echo $label->text; +return $label->text;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[Ada]:HI"); + assert_eq!(values.get(result), FakeValue::String("HI".to_string())); +} + +/// Verifies explicit set-hook parameter types are contravariant with the property type. +#[test] +fn execute_program_validates_eval_property_set_hook_parameter_types() { + let valid_program = parse_fragment( + br#"class EvalWideSetHookParam { + public string $value { + get => $this->value; + set(mixed $raw) => $raw; + } +} +$box = new EvalWideSetHookParam(); +$box->value = "Ada"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); + + for source in [ + br#"class EvalNarrowSetHookParam { + public mixed $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + br#"class EvalNullableSetHookParam { + public ?string $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("incompatible set-hook parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies nullsafe reads and mixed-case names still route through eval property hooks. +#[test] +fn execute_program_routes_eval_nullsafe_and_mixed_case_property_hooks() { + let program = parse_fragment( + br#"class EvalNullsafeHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +class EvalMixedCaseHookBox { + private int $store = 0; + public int $Total { + get { return $this->store; } + } + public function set(int $value) { $this->store = $value; } +} +function eval_hook_describe($person) { + return $person?->full ?? "(none)"; +} +$person = new EvalNullsafeHookPerson(); +$box = new EvalMixedCaseHookBox(); +$box->set(5); +echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total; +return $box->Total;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace|(none)|5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs b/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs new file mode 100644 index 0000000000..471e8dd895 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs @@ -0,0 +1,325 @@ +//! Purpose: +//! Interpreter tests for readonly properties, readonly classes, and dynamic- +//! property restrictions. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constructor initialization and all post-initialization write paths are +//! checked independently. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies promoted readonly properties throw Error outside their constructor. +#[test] +fn execute_program_promoted_readonly_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyBox { + public function __construct(public readonly int $id) {} + public function replace($id) { $this->id = $id; } +} +$box = new EvalPromotedReadonlyBox(7); +echo $box->id; +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7:Error:Cannot modify readonly property EvalPromotedReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties can be initialized inside their constructor. +#[test] +fn execute_program_initializes_readonly_property_in_constructor() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyBox(7); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies direct reads of uninitialized typed eval properties throw catchable PHP errors. +#[test] +fn execute_program_rejects_uninitialized_typed_property_reads() { + let program = parse_fragment( + br#"class EvalTypedReadBox { + public int $typed; + public ?int $nullable; + public ?int $defaultNull = null; + public $plain; +} +$box = new EvalTypedReadBox(); +try { + echo $box->typed; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo $box->nullable; +} catch (Error $e) { + echo $e->getMessage(); +} +echo "|"; +echo is_null($box->defaultNull) ? "default-null" : "bad"; +echo "|"; +echo is_null($box->plain) ? "plain-null" : "bad"; +echo "|"; +$box->typed = 0; +echo $box->typed; +echo "|"; +unset($box->typed); +try { + echo $box->typed; +} catch (Error $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Typed property EvalTypedReadBox::$typed must not be accessed before initialization|\ +Typed property EvalTypedReadBox::$nullable must not be accessed before initialization|\ +default-null|plain-null|0|\ +Typed property EvalTypedReadBox::$typed must not be accessed before initialization" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties throw Error on writes outside the declaring constructor. +#[test] +fn execute_program_readonly_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyBox(7); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties must declare a type like PHP requires. +#[test] +fn execute_program_rejects_untyped_readonly_properties() { + let explicit = parse_fragment( + br#"class EvalReadonlyUntypedBox { + public readonly $value; +}"#, + ) + .expect("parse explicit readonly property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&explicit, &mut scope, &mut values) + .expect_err("explicit readonly property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_class = parse_fragment( + br#"readonly class EvalReadonlyClassUntypedBox { + public $value; +}"#, + ) + .expect("parse readonly class property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&readonly_class, &mut scope, &mut values) + .expect_err("readonly class property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly classes make instance properties readonly implicitly. +#[test] +fn execute_program_initializes_readonly_class_property_in_constructor() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyClassBox(11); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "11:"); + assert_eq!(values.get(result), FakeValue::Int(11)); +} + +/// Verifies readonly class instance properties throw Error on writes after construction. +#[test] +fn execute_program_readonly_class_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyClassFailBox(11); +try { + $box->replace(12); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes throw Error on dynamic property creation without a magic setter. +#[test] +fn execute_program_readonly_class_dynamic_property_creation_throws_error() { + let program = parse_fragment( + br#"readonly class EvalReadonlyDynamicFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } +} +$box = new EvalReadonlyDynamicFailBox(11); +try { + $box->dynamic = 12; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot create dynamic property EvalReadonlyDynamicFailBox::$dynamic" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes may still handle missing property writes through `__set()`. +#[test] +fn execute_program_allows_readonly_class_magic_set_for_missing_properties() { + let program = parse_fragment( + br#"readonly class EvalReadonlyMagicSetBox { + public function __set($name, $value) { + echo $name; echo ":"; echo $value; + } +} +$box = new EvalReadonlyMagicSetBox(); +$box->dynamic = 12; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "dynamic:12"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes reject PHP's global dynamic-property marker attribute. +#[test] +fn execute_program_rejects_allow_dynamic_properties_on_readonly_class() { + let program = + parse_fragment(br#"#[\AllowDynamicProperties] readonly class EvalReadonlyAllowDynamic {}"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("AllowDynamicProperties cannot apply to readonly classes"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies namespaced non-builtin attributes do not trigger the readonly-class marker rule. +#[test] +fn execute_program_allows_namespaced_allow_dynamic_properties_on_readonly_class() { + let program = parse_fragment( + br#"namespace EvalReadonlyAttrNs; +#[AllowDynamicProperties] readonly class Box {} +echo class_attribute_names("EvalReadonlyAttrNs\Box")[0]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReadonlyAttrNs\\AllowDynamicProperties"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/closures.rs b/crates/elephc-magician/src/interpreter/tests/closures.rs new file mode 100644 index 0000000000..70a923a1b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/closures.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Interpreter tests for eval closure literals, captures, and callable dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise eval-only closure execution against fake runtime cells. +//! - Closure values are PHP-visible `Closure` objects with eval-retained bodies. + +use super::super::*; +use super::support::*; + +/// Verifies eval closure literals dispatch through direct variable calls and call_user_func_array. +#[test] +fn execute_program_dispatches_eval_closure_literal() { + let program = parse_fragment( + br#"$fn = function($left, $right = 2) { return $left + $right; }; +echo $fn(3); echo ":"; +echo call_user_func_array($fn, ["right" => 6, "left" => 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:11"); +} + +/// Verifies by-value eval closure captures snapshot the defining value for each invocation. +#[test] +fn execute_program_closure_by_value_capture_uses_snapshot() { + let program = parse_fragment( + br#"$x = 1; +$fn = function() use ($x) { $x += 1; return $x; }; +$x = 5; +echo $fn(); echo ":"; +echo $fn(); echo ":"; +echo $x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "2:2:5"); + assert_eq!(values.get(x), FakeValue::Int(5)); +} + +/// Verifies by-reference eval closure captures write back before a failing body escapes. +#[test] +fn execute_program_closure_by_ref_capture_writes_back_before_fatal() { + let program = parse_fragment( + br#"$x = 1; +$fn = function() use (&$x) { $x = 9; missing_eval_closure_function(); }; +$fn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("closure should fail"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(x), FakeValue::Int(9)); +} + +/// Verifies eval closure by-reference parameters mutate the caller variable. +#[test] +fn execute_program_closure_by_ref_parameter_writes_back() { + let program = parse_fragment( + br#"$fn = function(&$value) { $value += 2; }; +$value = 3; +$fn($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies eval closure values are callable but do not leak into function_exists. +#[test] +fn execute_program_closure_is_callable_without_function_exists_leak() { + let program = parse_fragment( + br#"$fn = function() { return "ok"; }; +echo is_callable($fn) ? "C" : "c"; +echo call_user_func($fn);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let fn_cell = scope.visible_cell("fn").expect("scope should contain fn"); + let FakeValue::Object(_) = values.get(fn_cell) else { + panic!("closure representation should be a PHP object"); + }; + let identity = values + .object_identity(fn_cell) + .expect("closure object should have identity"); + let name = context + .closure_object_name(identity) + .expect("closure object should map to callable name"); + + assert_eq!(values.output, "Cok"); + assert!(context.has_closure(name)); + assert!(!context.has_function(name)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/control_flow.rs b/crates/elephc-magician/src/interpreter/tests/control_flow.rs new file mode 100644 index 0000000000..d51dfd38c7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/control_flow.rs @@ -0,0 +1,441 @@ +//! Purpose: +//! Interpreter tests for branching, loops, comparisons, match, logical operators, and foreach. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise control-flow outcomes without leaving the fake runtime. + +use super::super::*; +use super::support::*; + +/// Verifies if/else executes only the PHP-truthy branch. +#[test] +fn execute_program_if_else_uses_php_truthiness() { + let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(0).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("else".to_string())); +} +/// Verifies elseif chains execute the first truthy branch and skip later branches. +#[test] +fn execute_program_elseif_uses_first_truthy_branch() { + let program = + parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let a = values.bool_value(false).expect("create fake bool"); + let b = values.bool_value(true).expect("create fake bool"); + scope.set("a", a, ScopeCellOwnership::Owned); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("b".to_string())); +} +/// Verifies while repeats while the condition remains truthy and propagates writes. +#[test] +fn execute_program_while_uses_php_truthiness() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(2).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let flag = scope + .visible_cell("flag") + .expect("scope should contain flag"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(flag), FakeValue::Bool(false)); +} +/// Verifies do/while runs the body before testing the condition. +#[test] +fn execute_program_do_while_runs_body_before_condition() { + let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let i = values.int(0).expect("create fake int"); + scope.set("i", i, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "0"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} +/// Verifies switch uses loose matching and falls through after the matching case. +#[test] +fn execute_program_switch_matches_and_falls_through() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(2).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "twodefault"); +} +/// Verifies for loops run init, condition, update, and body in PHP order. +#[test] +fn execute_program_for_loop_updates_after_body() { + let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "321"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} +/// Verifies `continue` in a for loop still runs the update clause. +#[test] +fn execute_program_for_continue_runs_update_clause() { + let program = parse_fragment( + br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "done"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} +/// Verifies comparison operators return boolean cells usable by echo and branches. +#[test] +fn execute_program_comparisons_return_bool_cells() { + let program = parse_fragment( + br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1111ns"); +} +/// Verifies spaceship comparisons return PHP -1/0/1 integer cells. +#[test] +fn execute_program_spaceship_returns_int_cells() { + let program = + parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "-1:0:1"); +} +/// Verifies strict equality keeps PHP type identity distinct from loose equality. +#[test] +fn execute_program_strict_equality_uses_type_identity() { + let program = parse_fragment( + br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111"); +} +/// Verifies logical AND skips an unsupported right-hand expression after a false left side. +#[test] +fn execute_program_short_circuits_logical_and() { + let program = parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(false)); +} +/// Verifies logical OR skips an unsupported right-hand expression after a true left side. +#[test] +fn execute_program_short_circuits_logical_or() { + let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies match expressions use strict comparison across comma-separated patterns. +#[test] +fn execute_program_match_uses_strict_pattern_comparison() { + let program = + parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.string("1").expect("create fake string"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("string".to_string())); +} +/// Verifies match expressions evaluate only the selected arm result. +#[test] +fn execute_program_match_skips_unselected_results() { + let program = parse_fragment( + br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("two".to_string())); +} +/// Verifies match expressions without a matching arm or default fail at runtime. +#[test] +fn execute_program_match_without_default_fails_on_miss() { + let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. +#[test] +fn execute_program_evaluates_keyword_logical_operators() { + let program = + parse_fragment(br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "F"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. +#[test] +fn execute_program_evaluates_keyword_xor() { + let program = + parse_fragment(br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TF"); +} +/// Verifies ternary expressions evaluate only the selected branch. +#[test] +fn execute_program_ternary_short_circuits_unselected_branch() { + let program = + parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "yesno"); +} +/// Verifies the short ternary form returns the condition value when it is truthy. +#[test] +fn execute_program_short_ternary_reuses_truthy_condition() { + let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xfallback"); +} +/// Verifies null coalescing uses the default for missing or null values. +#[test] +fn execute_program_null_coalesce_uses_default_for_missing_or_null() { + let program = parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.null().expect("create fake null"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fallbacknull-fallback"); +} +/// Verifies null coalescing skips the default expression for non-null values. +#[test] +fn execute_program_null_coalesce_short_circuits_non_null_value() { + let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set"); +} +/// Verifies logical negation returns boolean cells using PHP truthiness. +#[test] +fn execute_program_evaluates_logical_not() { + let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1"); +} +/// Verifies unary numeric operators delegate to PHP numeric runtime operations. +#[test] +fn execute_program_evaluates_unary_numeric_ops() { + let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(5).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(-3)); +} +/// Verifies foreach assigns each indexed element to the value variable. +#[test] +fn execute_program_foreach_iterates_indexed_values() { + let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "ab"); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} +/// Verifies foreach key-value targets receive indexed integer keys and values. +#[test] +fn execute_program_foreach_assigns_indexed_keys() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let key = scope.visible_cell("key").expect("scope should contain key"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "0a1b"); + assert_eq!(values.get(key), FakeValue::Int(1)); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} +/// Verifies foreach over associative arrays preserves insertion-order keys and values. +#[test] +fn execute_program_foreach_iterates_assoc_keys_and_values() { + let program = parse_fragment( + br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a:1;b:2;"); +} +/// Verifies value-only foreach over associative arrays still yields values in insertion order. +#[test] +fn execute_program_foreach_iterates_assoc_values_only() { + let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12"); +} +/// Verifies break and continue control foreach execution inside eval. +#[test] +fn execute_program_foreach_honors_break_and_continue() { + let program = parse_fragment( + br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); +} + +/// Verifies foreach drives eval-declared Iterator and IteratorAggregate objects. +#[test] +fn execute_program_foreach_iterates_eval_traversable_objects() { + let program = parse_fragment( + br#"class EvalForeachIterator implements Iterator { + private int $i = 0; + public function rewind(): void { echo "rewind:"; $this->i = 0; } + public function valid(): bool { echo "valid" . $this->i . ":"; return $this->i < 2; } + public function current(): mixed { echo "current" . $this->i . ":"; return "v" . $this->i; } + public function key(): mixed { echo "key" . $this->i . ":"; return "k" . $this->i; } + public function next(): void { echo "next" . $this->i . ":"; $this->i = $this->i + 1; } +} +class EvalForeachAggregate implements IteratorAggregate { + public function getIterator(): Traversable { echo "agg:"; return new EvalForeachIterator(); } +} +foreach (new EvalForeachIterator() as $key => $item) { + echo $key . "=" . $item . ":"; + if ($item === "v0") { continue; } + break; +} +echo "|"; +foreach (new EvalForeachAggregate() as $item) { + echo $item . ":"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "rewind:valid0:current0:key0:k0=v0:next0:valid1:current1:key1:k1=v1:|agg:rewind:valid0:current0:v0:next0:valid1:current1:v1:next1:valid2:" + ); +} diff --git a/crates/elephc-magician/src/interpreter/tests/core.rs b/crates/elephc-magician/src/interpreter/tests/core.rs new file mode 100644 index 0000000000..a6d3ab7fc2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/core.rs @@ -0,0 +1,504 @@ +//! Purpose: +//! Interpreter tests for scope mutation, exceptions, includes, and early execution results. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover baseline eval execution before builtin-specific dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies assignment writes a named scope entry and return reads it back. +#[test] +fn execute_program_stores_and_returns_scope_value() { + let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::Int(3)); + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies reference assignment aliases variable names and writes through the alias. +#[test] +fn execute_program_reference_assignment_updates_source_variable() { + let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + let alias = scope + .visible_cell("alias") + .expect("scope should contain alias"); + + assert_eq!(x, alias); + assert_eq!(values.get(x), FakeValue::Int(5)); + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval `throw` exits the program with a retained Throwable cell. +#[test] +fn execute_program_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies eval `try/catch` catches a thrown object and binds the catch variable. +#[test] +fn execute_program_catches_throwable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. +#[test] +fn execute_program_catches_throwable_without_variable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 9; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("unbound catch should release the thrown object"); + + assert_eq!(scope.visible_cell("caught"), None); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies eval `catch (Exception)` matches thrown exception objects. +#[test] +fn execute_program_catches_specific_exception_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval `catch` clauses can match eval-declared interfaces. +#[test] +fn execute_program_catches_eval_declared_interface_inside_eval() { + let program = parse_fragment( + br#"interface EvalCatchable { + function value(); +} +class EvalThrownBox implements EvalCatchable { + public function value() { return 42; } +} +try { + throw new EvalThrownBox(); +} catch (EvalCatchable $caught) { + return $caught->value(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval catch clauses keep source order and skip non-matching types. +#[test] +fn execute_program_skips_non_matching_specific_catch_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException $wrong) { + return 1; +} catch (Exception $caught) { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(scope.visible_cell("wrong"), None); + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies union catch clauses test later types in the same catch clause. +#[test] +fn execute_program_catches_union_type_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException|Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval `finally` runs before a pending try-body return is observed. +#[test] +fn execute_program_runs_finally_before_returning_try_value() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "finally"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval `finally` return values replace pending try-body returns. +#[test] +fn execute_program_finally_return_overrides_try_return() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.releases.len(), 1); +} +/// Verifies eval `finally` return values replace pending uncaught throws. +#[test] +fn execute_program_finally_return_overrides_uncaught_throw() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("overridden throw should be released"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); +} +/// Verifies eval `finally` runs before an uncaught throw leaves the fragment. +#[test] +fn execute_program_runs_finally_before_uncaught_throw_outcome() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + assert_eq!(values.output, "finally"); +} +/// Verifies static locals declared inside eval catch blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_catch() { + let program = parse_fragment( + br#"function dyn($e) { + try { + throw $e; + } catch (Throwable $caught) { + static $n = 0; + $n++; + return $caught->answer() + $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let first_thrown = values + .new_object("Exception") + .expect("allocate first fake exception"); + let second_thrown = values + .new_object("Exception") + .expect("allocate second fake exception"); + + let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(43)); + assert_eq!(values.get(second), FakeValue::Int(44)); +} +/// Verifies static locals declared inside eval finally blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_finally() { + let program = parse_fragment( + br#"function dyn() { + try { + return 0; + } finally { + static $n = 0; + $n++; + return $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + + let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(2)); +} +/// Verifies throws from eval-declared functions escape through the shared context. +#[test] +fn execute_context_function_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + + let outcome = execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) + .expect("throw should be an eval function outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies nested eval preserves the thrown cell while returning an uncaught status. +#[test] +fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { + let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + scope.set("e", thrown, ScopeCellOwnership::Borrowed); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("nested throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. +#[test] +fn execute_program_include_uses_call_site_and_returns_file_result() { + let dir = std::env::temp_dir().join(format!( + "elephc-magician-include-{}-call-site", + std::process::id() + )); + let path = dir.join("piece.php"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create include fixture directory"); + std::fs::write( + &path, + format!( + r#" "F", "left" => "E"]); echo ":"; +$named = "EvalStaticCallableBox::join"; +return $named(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} + +/// Verifies fake runtime releases include both temporary callable-array index cells. +fn assert_released_array_callable_index_temps(values: &FakeOps) { + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(0)), + "array callable index 0 temporary should be released" + ); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(1)), + "array callable index 1 temporary should be released" + ); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs new file mode 100644 index 0000000000..5d0f249df8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs @@ -0,0 +1,227 @@ +//! Purpose: +//! Interpreter tests for `call_user_func_array` dispatch, named argument +//! normalization, temporary cleanup, and duplicate declarations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval, builtin, and registered native function targets share this surface. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. +#[test] +fn execute_program_call_user_func_array_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", [4, 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. +#[test] +fn execute_program_call_user_func_array_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. +#[test] +fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies `call_user_func_array` rejects positional values after named keys. +#[test] +fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", ["y" => 2, 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies `call_user_func_array` releases literal callback and argument-array temporaries. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_and_arg_array() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Array(_))), + "literal call argument array should be released after dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal temporaries after a fatal dispatch. +#[test] +fn execute_program_call_user_func_array_releases_literal_temporaries_after_fatal() { + let program = + parse_fragment(br#"return call_user_func_array("strlen", ["unknown" => "abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after fatal dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Assoc(_))), + "literal call argument hash should be released after fatal dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal callback when arg-array evaluation fails. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_after_arg_array_eval_fatal() { + let program = parse_fragment(br#"return call_user_func_array("strlen", MISSING_ARG_ARRAY);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released when argument-array evaluation fails" + ); +} +/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies `call_user_func_array` named keys can bind registered native parameters. +#[test] +fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies duplicate eval-declared function names fail in a shared context. +#[test] +fn execute_program_rejects_duplicate_declared_function() { + let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs new file mode 100644 index 0000000000..65fc26a687 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! Interpreter tests for first-class function/method callables, invokable eval +//! objects, magic fallback, and named object-method arguments. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Static called-class state and invalid object callables are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies first-class callable syntax dispatches through eval's callback paths. +#[test] +fn execute_program_first_class_callables_dispatch_functions_and_methods() { + let program = parse_fragment( + br#"function eval_fc_double($value) { + return $value * 2; +} +class EvalFirstClassCallableBase { + public function __construct($offset = 1) { + $this->offset = $offset; + } + public function add($value) { + return $value + $this->offset; + } + public function keep($value) { + return $value > 2; + } + public function sum($carry, $value) { + return $carry + $value + $this->offset; + } + public function show($value, $key) { + echo $key . $value; + } + public static function join($left, $right) { + return $left . $right; + } + public static function compareDesc($left, $right) { + return $right - $left; + } + public static function label($value) { + return "base:" . $value; + } + public static function relay($value) { + $fn = static::label(...); + return $fn($value); + } +} +class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { + public static function label($value) { + return "child:" . $value; + } +} +$function = eval_fc_double(...); +echo $function(4); echo ":"; +echo (strlen(...))("abcd"); echo ":"; +$box = new EvalFirstClassCallableBase(3); +$method = $box->add(...); +echo $method(4); echo ":"; +echo call_user_func($box->add(...), 5); echo ":"; +$static = EvalFirstClassCallableBase::join(...); +echo $static(right: "B", left: "A"); echo ":"; +$mapped = array_map($box->add(...), [1, 2]); +echo $mapped[0] . $mapped[1] . ":"; +$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); +echo count($filtered) . ":"; +echo array_reduce([1, 2], $box->sum(...), 0) . ":"; +$walked = ["a" => 1]; +array_walk($walked, $box->show(...)); +echo ":"; +$sorted = [3, 1, 2]; +usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); +echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; +return EvalFirstClassCallableChild::relay("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:4:7:8:AB:45:2:9:a1:321:"); + assert_eq!( + values.get(result), + FakeValue::String("child:ok".to_string()) + ); +} + +/// Verifies first-class static callables preserve late-static forwarding metadata. +#[test] +fn execute_program_first_class_static_callables_preserve_called_class() { + let program = parse_fragment( + br#"class EvalFirstClassStaticForwardBase { + public static function who() { + return static::tag(); + } + public static function tag() { + return "base"; + } + public static function relaySelf() { + $fn = self::who(...); + return $fn(); + } +} +class EvalFirstClassStaticForwardChild extends EvalFirstClassStaticForwardBase { + public static function relayParent() { + $fn = parent::who(...); + return $fn(); + } + public static function tag() { + return "child"; + } +} +echo EvalFirstClassStaticForwardChild::relayParent(); echo ":"; +return EvalFirstClassStaticForwardChild::relaySelf();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:"); + assert_eq!(values.get(result), FakeValue::String("child".to_string())); +} + +/// Verifies invokable eval objects dispatch through variable and callback call paths. +#[test] +fn execute_program_invokes_eval_object_callables() { + let program = parse_fragment( + br#"function eval_plain_call_side_effect() { + echo "bad"; + return "x"; +} +class EvalInvokableBox { + public function __construct($label = "box") { + $this->label = $label; + } + public function __invoke($left = "A", $right = "B") { + return $this->label . ":" . $left . $right; + } +} +class EvalPlainCallableProbe {} +$box = new EvalInvokableBox("box"); +$plain = new EvalPlainCallableProbe(); +echo is_callable($box) ? "Y:" : "N:"; +echo is_callable($plain) ? "bad:" : "plain:"; +echo $box(right: "D", left: "C"); echo ":"; +try { + $plain(eval_plain_call_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; +echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; +echo call_user_func($box, "G", "H"); echo ":"; +$first = $box(...); +echo $first("K", "L"); echo ":"; +return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:" + ); + assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); +} + +/// Verifies call_user_func rejects non-invokable eval objects with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_non_invokable_eval_object() { + let program = parse_fragment( + br#"class EvalPlainCallbackError {} +$plain = new EvalPlainCallbackError(); +try { + call_user_func($plain); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array($plain, []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies call_user_func rejects invalid object-method callable arrays with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_invalid_object_method_arrays() { + let program = parse_fragment( + br#"class EvalMissingCallbackArray {} +class EvalPrivateCallbackArray { + private function hidden() { + return "bad"; + } +} +class EvalPrivateInvokeCallbackArray { + private function __invoke() { + return "bad"; + } +} +class EvalInstanceCallbackArray { + public function inst() { + return "bad"; + } +} +$missing = new EvalMissingCallbackArray(); +try { + call_user_func([$missing, "MiSsInG"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array([$missing, "missing"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateInvokeCallbackArray(), "__invoke"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func(["EvalInstanceCallbackArray", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateInvokeCallbackArray::__invoke()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies call_user_func callable arrays still dispatch through magic method fallbacks. +#[test] +fn execute_program_call_user_func_arrays_dispatch_magic_method_fallbacks() { + let program = parse_fragment( + br#"class EvalMagicCallbackArray { + public function __call($method, $args) { + return $method . ":" . $args[0]; + } + public static function __callStatic($method, $args) { + return $method . ":" . $args[0]; + } +} +$box = new EvalMagicCallbackArray(); +echo is_callable([$box, "missing"]) ? "Y:" : "N:"; +echo call_user_func([$box, "missing"], "A") . ":"; +echo call_user_func_array([$box, "missing"], ["B"]) . ":"; +echo is_callable(["EvalMagicCallbackArray", "static_missing"]) ? "S:" : "s:"; +return call_user_func(["EvalMagicCallbackArray", "static_missing"], "C");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:missing:A:missing:B:S:"); + assert_eq!( + values.get(result), + FakeValue::String("static_missing:C".to_string()) + ); +} + +/// Verifies object-method callable arrays preserve eval named-argument binding. +#[test] +fn execute_program_object_method_callable_array_binds_eval_named_args() { + let program = parse_fragment( + br#"class EvalObjectCallableArrayBox { + public function join($left, $right) { + return $left . $right; + } +} +$box = new EvalObjectCallableArrayBox(); +$cb = [$box, "join"]; +echo is_callable($cb) ? "Y:" : "N:"; +return call_user_func_array($cb, ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:"); + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs new file mode 100644 index 0000000000..e45aee5113 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs @@ -0,0 +1,14 @@ +//! Purpose: +//! Organizes interpreter tests for dynamic callable dispatch by callable form +//! and runtime bridge surface. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test and helper function name. + +mod call_user_func; +mod call_user_func_array; +mod first_class_objects; +mod runtime_callables; diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs new file mode 100644 index 0000000000..4f267853ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs @@ -0,0 +1,229 @@ +//! Purpose: +//! Interpreter tests for runtime AOT static/method callable hooks, named/default +//! arguments, by-reference constraints, and writeback. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Runtime bridge failures and coercion writeback are asserted separately. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies static calls fall back to runtime AOT hooks when no eval class matches. +#[test] +fn execute_program_static_call_dispatches_runtime_method_hook() { + let program = parse_fragment( + br#"echo KnownClass::join("A", "B"); echo ":"; +$cb = ["KnownClass", "join"]; +echo call_user_func($cb, "C", "D"); echo ":"; +$named = "KnownClass::join"; +echo $named("E", "F"); echo ":"; +return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut join_signature = NativeCallableSignature::new(2); + assert!(join_signature.set_param_name(0, "left")); + assert!(join_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "join", + join_signature + )); + let mut sum_signature = NativeCallableSignature::new(2); + assert!(sum_signature.set_param_name(0, "left")); + assert!(sum_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "sum", + sum_signature + )); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies runtime AOT static method fallback binds registered named arguments. +#[test] +fn execute_program_static_runtime_method_hook_binds_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered named AOT call should bind"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies runtime AOT static method fallback fills registered scalar defaults. +#[test] +fn execute_program_static_runtime_method_hook_binds_default_args() { + let program = parse_fragment( + br#"echo KnownClass::join("A"); echo ":"; +return call_user_func_array(["KnownClass", "join"], ["left" => "C"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_default(1, NativeCallableDefault::String("B".to_string()))); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered AOT defaults should bind"); + + assert_eq!(values.output, "AB:"); + assert_eq!(values.get(result), FakeValue::String("CB".to_string())); +} + +/// Verifies runtime AOT static method fallback honors by-reference parameter metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_by_ref_temporary_arg() { + let program = parse_fragment(br#"return KnownClass::sum(1, 2);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a static by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies callable-array AOT method dispatch preserves by-reference writeback. +#[test] +fn execute_program_callable_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo $cb($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("callable-array runtime method should preserve by-ref target"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies `call_user_func_array()` preserves by-reference array elements for AOT methods. +#[test] +fn execute_program_call_user_func_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo call_user_func_array($cb, [&$value, 2]); +echo ":"; +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("call_user_func_array should preserve by-ref array element target"); + + assert_eq!(values.output, "15:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies string and first-class AOT static callables preserve by-reference writeback. +#[test] +fn execute_program_static_runtime_callables_write_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$string = "KnownClass::sum"; +$left = "3"; +echo $string($left, 2); echo ":"; +echo $left + 1; echo ":"; +$first = KnownClass::sum(...); +$next = "4"; +echo $first($next, 5); echo ":"; +return $next;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("runtime static callables should preserve by-ref target"); + + assert_eq!(values.output, "5:4:9:"); + assert_eq!(values.get(result), FakeValue::Int(4)); +} + +/// Verifies runtime AOT static method fallback rejects named arguments without metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let error = + execute_program(&program, &mut scope, &mut values).expect_err("named AOT call should fail"); + + assert_eq!(error, EvalStatus::UncaughtThrowable); +} diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs new file mode 100644 index 0000000000..ed6e19ae74 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -0,0 +1,446 @@ +//! Purpose: +//! Interpreter tests for eval-declared enum runtime behavior. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify enum singleton cases, class-like metadata, backed values, +//! and method/interface dispatch through the eval object path. + +use super::super::*; +use super::support::*; + +/// Executes an eval enum fragment and asserts it fails during runtime validation. +fn assert_invalid_enum_fragment(source: &[u8], message: &str) { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(message); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies pure eval enums expose singleton cases and class-like introspection. +#[test] +fn execute_program_declares_pure_eval_enum_cases() { + let program = parse_fragment( + br#"enum EvalSuit { + case Hearts; + case Clubs; +} +$cases = EvalSuit::cases(); +echo enum_exists("evalsuit") ? "enum" : "bad"; echo ":"; +echo class_exists("EvalSuit") ? "class" : "bad"; echo ":"; +echo count($cases); echo ":"; +echo $cases[0] === EvalSuit::Hearts ? "same" : "bad"; echo ":"; +echo EvalSuit::Hearts->name; echo ":"; +return get_class(EvalSuit::Clubs);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "enum:class:2:same:Hearts:"); + assert_eq!( + values.get(result), + FakeValue::String("EvalSuit".to_string()) + ); +} + +/// Verifies backed eval enums expose values and `from` / `tryFrom` lookups. +#[test] +fn execute_program_declares_backed_eval_enum_cases() { + let program = parse_fragment( + br#"enum EvalColor: int { + case Red = 1; + case Green = 2; +} +echo EvalColor::Green->value; echo ":"; +echo EvalColor::from(value: 1) === EvalColor::Red ? "from" : "bad"; echo ":"; +return EvalColor::tryFrom(99);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:from:"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies eval enum `from()` misses throw catchable `ValueError` objects. +#[test] +fn execute_program_enum_from_miss_throws_value_error() { + let program = parse_fragment( + br#"enum EvalColor: int { + case Red = 1; +} +try { + EvalColor::from(99); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e) . ":" . $e->getMessage(); + return true; +} +return false;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ValueError:99 is not a valid backing value for enum EvalColor" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval enum methods, constants, and interface implementation dispatch. +#[test] +fn execute_program_dispatches_eval_enum_methods_and_interfaces() { + let program = parse_fragment( + br#"interface EvalLabel { + function label(); +} +enum EvalSuit implements EvalLabel { + case Hearts; + public const PREFIX = "suit"; + public function label() { return self::PREFIX . ":" . $this->name; } + public static function fallback() { return self::Hearts; } +} +echo is_a(EvalSuit::Hearts, "EvalLabel") ? "iface" : "bad"; echo ":"; +echo EvalSuit::Hearts->label(); echo ":"; +return EvalSuit::fallback() === EvalSuit::Hearts;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "iface:suit:Hearts:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval enums can import trait methods and expose direct trait metadata. +#[test] +fn execute_program_dispatches_eval_enum_trait_use() { + let program = parse_fragment( + br#"trait EvalEnumTrait { + public function label() { return $this->name; } + public static function suffix() { return "S"; } +} +enum EvalTraitEnum { + use EvalEnumTrait { + label as private hiddenLabel; + } + case Ready; + public function read() { return $this->label() . ":" . $this->hiddenLabel(); } +} +echo EvalTraitEnum::Ready->read(); echo ":"; +echo EvalTraitEnum::suffix(); echo ":"; +$ref = new ReflectionClass("EvalTraitEnum"); +$traits = $ref->getTraitNames(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenLabel"]; echo ":"; +$uses = class_uses(EvalTraitEnum::Ready); +echo count($uses); echo ":"; echo $uses["EvalEnumTrait"]; echo ":"; +return EvalTraitEnum::Ready->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Ready:Ready:S:1:EvalEnumTrait:EvalEnumTrait::label:1:EvalEnumTrait:" + ); + assert_eq!(values.get(result), FakeValue::String("Ready".to_string())); +} + +/// Verifies enum synthetic methods hide conflicting trait imports like PHP. +#[test] +fn execute_program_uses_enum_synthetic_methods_over_trait_imports() { + let program = parse_fragment( + br#"trait EvalEnumSyntheticTrait { + public function cases() { return "trait-cases"; } + public static function from($value) { return "trait-from"; } + public static function tryFrom($value) { return "trait-try"; } +} +enum EvalPureSynthetic { + use EvalEnumSyntheticTrait { + cases as traitCases; + } + case Ready; +} +enum EvalBackedSynthetic: string { + use EvalEnumSyntheticTrait { + cases as traitCases; + from as traitFrom; + } + case Ready = "ready"; +} +echo is_array(EvalPureSynthetic::Ready->cases()) ? "cases" : "bad"; echo ":"; +echo EvalPureSynthetic::Ready->traitCases(); echo ":"; +echo EvalPureSynthetic::from("x"); echo ":"; +echo EvalPureSynthetic::Ready->from("x"); echo ":"; +echo EvalPureSynthetic::tryFrom("x"); echo ":"; +echo EvalBackedSynthetic::from("ready")->value; echo ":"; +echo EvalBackedSynthetic::Ready->from("ready")->value; echo ":"; +echo EvalBackedSynthetic::tryFrom("missing") === null ? "null" : "bad"; echo ":"; +echo EvalBackedSynthetic::traitFrom("x"); echo ":"; +echo EvalBackedSynthetic::Ready->traitCases(); echo ":"; +echo is_callable([EvalBackedSynthetic::Ready, "cases"]) ? "callable" : "bad";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "cases:trait-cases:trait-from:trait-from:trait-try:ready:ready:null:trait-from:trait-cases:callable" + ); +} + +/// Verifies pure eval enums may declare `from` and `tryFrom` methods directly. +#[test] +fn execute_program_allows_pure_eval_enum_direct_from_methods() { + let program = parse_fragment( + br#"enum EvalPureDirectFrom { + case Ready; + public static function from($value) { return "from:" . $value; } + public static function tryFrom($value) { return "try:" . $value; } +} +echo EvalPureDirectFrom::from("x"); echo ":"; +echo EvalPureDirectFrom::Ready->tryFrom("y"); echo ":"; +return is_callable([EvalPureDirectFrom::Ready, "from"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "from:x:try:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod metadata and invocation for enum synthetic methods. +#[test] +fn execute_program_reflects_eval_enum_synthetic_methods() { + let program = parse_fragment( + br#"enum EvalReflectSyntheticEnum: string { + case Ready = "ready"; +} +enum EvalReflectPureSyntheticEnum { + case Ready; +} +$ref = new ReflectionClass("EvalReflectSyntheticEnum"); +$methods = $ref->getMethods(ReflectionMethod::IS_STATIC); +echo count($methods); echo ":"; +echo $methods[0]->getName(); echo "/"; +echo $methods[1]->getName(); echo "/"; +echo $methods[2]->getName(); echo ":"; +$cases = $ref->getMethod("cases"); +echo $cases->getReturnType(); echo ":"; +echo count($cases->invoke(null)); echo ":"; +$from = new ReflectionMethod("EvalReflectSyntheticEnum", "from"); +$params = $from->getParameters(); +echo $from->getDeclaringClass()->getName(); echo ":"; +echo $from->getNumberOfParameters(); echo "/"; +echo $from->getNumberOfRequiredParameters(); echo ":"; +echo $params[0]->getName(); echo "/"; +echo $params[0]->getType(); echo ":"; +echo $from->getReturnType(); echo ":"; +echo $from->invoke(null, "ready")->name; echo ":"; +$try = ReflectionMethod::createFromMethodName("EvalReflectSyntheticEnum::tryFrom"); +echo $try->getReturnType(); echo ":"; +echo $try->invokeArgs(null, ["missing"]) === null ? "null" : "bad"; echo ":"; +$pure = new ReflectionClass("EvalReflectPureSyntheticEnum"); +echo count($pure->getMethods()); echo ":"; +echo $pure->hasMethod("from") ? "bad" : "nofrom";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "3:cases/from/tryFrom:array:1:EvalReflectSyntheticEnum:1/1:value/string|int:static:Ready:?static:null:1:nofrom" + ); +} + +/// Verifies eval enum interfaces can inherit PHP's native enum marker interfaces. +#[test] +fn execute_program_allows_eval_enum_marker_interface_inheritance() { + let program = parse_fragment( + br#"interface EvalUnitMarker extends UnitEnum {} +interface EvalBackedMarker extends BackedEnum {} +enum EvalMarkedUnit implements EvalUnitMarker { + case Ready; +} +enum EvalMarkedBacked: string implements EvalBackedMarker { + case Ready = "ready"; +} +echo interface_exists("UnitEnum") ? "U" : "u"; echo ":"; +echo interface_exists("BackedEnum") ? "B" : "b"; echo ":"; +echo is_a(EvalMarkedUnit::Ready, "EvalUnitMarker") ? "unit" : "bad"; echo ":"; +echo is_a(EvalMarkedBacked::Ready, "EvalBackedMarker") ? "backed" : "bad"; echo ":"; +$unitInterfaces = class_implements("EvalMarkedUnit"); +echo count($unitInterfaces); echo ":"; echo $unitInterfaces["EvalUnitMarker"]; echo ":"; +echo $unitInterfaces["UnitEnum"]; echo ":"; +$backedInterfaces = (new ReflectionClass("EvalMarkedBacked"))->getInterfaceNames(); +echo count($backedInterfaces); echo ":"; echo $backedInterfaces[0]; echo ":"; +echo $backedInterfaces[1]; echo ":"; echo $backedInterfaces[2]; echo ":"; +return EvalMarkedBacked::Ready->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "U:B:unit:backed:2:EvalUnitMarker:UnitEnum:3:EvalBackedMarker:UnitEnum:BackedEnum:" + ); + assert_eq!( + values.get(result), + FakeValue::String("ready".to_string()) + ); +} + +/// Verifies eval rejects enum members that conflict with PHP enum rules. +#[test] +fn execute_program_rejects_invalid_eval_enum_members() { + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnum { + case Ready; + public const Ready = 1; +}"#, + "case and constant name collision should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnumMethod { + case Ready; + public static function cases() { return []; } +}"#, + "reserved enum method should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidBackedEnumMethod: string { + case Ready = "ready"; + public static function from($value) { return self::Ready; } +}"#, + "backed enum from method should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnumMagicMethod { + case Ready; + public function __destruct() {} +}"#, + "forbidden enum magic method should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnumMagicMethodCase { + case Ready; + public function __GET($name) {} +}"#, + "forbidden enum magic method lookup should be case-insensitive", + ); + + assert_invalid_enum_fragment( + br#"trait EvalInvalidEnumPropertyTrait { + public int $x = 1; +} +enum EvalInvalidEnumTraitProperty { + use EvalInvalidEnumPropertyTrait; + case Ready; +}"#, + "enum cannot import trait properties", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidExplicitUnitEnum implements UnitEnum { + case Ready; +}"#, + "enum cannot explicitly implement UnitEnum", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidExplicitBackedEnum: string implements BackedEnum { + case Ready = "ready"; +}"#, + "enum cannot explicitly implement BackedEnum", + ); + + assert_invalid_enum_fragment( + br#"interface EvalBackedMarker extends BackedEnum {} +enum EvalInvalidPureBackedMarker implements EvalBackedMarker { + case Ready; +}"#, + "pure enum cannot implement BackedEnum through a marker", + ); + + assert_invalid_enum_fragment( + br#"interface EvalUnitMarker extends UnitEnum {} +class EvalInvalidUnitEnumClass implements EvalUnitMarker {}"#, + "non-enum class cannot implement UnitEnum through a marker", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidThrowableEnum implements Throwable { + case Ready; +}"#, + "enum cannot implement Throwable", + ); +} + +/// Verifies eval allows the enum magic methods PHP permits. +#[test] +fn execute_program_allows_supported_eval_enum_magic_methods() { + let program = parse_fragment( + br#"enum EvalAllowedEnumMagic { + case Ready; + public function __call($name, $arguments) { return $name; } + public static function __callStatic($name, $arguments) { return $name; } + public function __invoke() { return "invoke"; } +} +return enum_exists("EvalAllowedEnumMagic");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs b/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs new file mode 100644 index 0000000000..b27a973a99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs @@ -0,0 +1,389 @@ +//! Purpose: +//! Interpreter tests for eval class construction, inheritance, abstract/final +//! rules, dynamic interfaces, and traits. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Class declaration validation and construction behavior are exercised together. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared classes create objects with properties and methods. +#[test] +fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +$box = new DynBox(4); +echo get_class($box); +echo ":"; +echo $box->bump(3); +echo ":"; +echo is_a($box, "DynBox") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1); +echo ":"; +echo call_user_func_array($call, [2]); +echo ":"; +return $box->x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); +} +/// Verifies eval-declared classes inherit properties, methods, and constructors. +#[test] +fn execute_program_constructs_eval_declared_class_with_inheritance() { + let program = parse_fragment( + br#"class EvalBaseBox { + public int $base = 1; + public function __construct($base) { $this->base = $base; } + public function sum($n) { return $this->base + $this->tail + $n; } +} +class EvalChildBox extends EvalBaseBox implements KnownInterface { + public int $tail = 4; + public function read($n) { return $this->sum($n); } +} +$box = new EvalChildBox(3); +echo $box->read(5); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalBaseBox") ? "isa" : "bad"; echo ":"; +echo is_a($box, "KnownInterface") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalChildBox") ? "bad" : "self"; echo ":"; +echo is_subclass_of($box, "EvalBaseBox") ? "sub" : "bad"; echo ":"; +$parents = class_parents($box); +echo count($parents); echo ":"; +echo $parents["EvalBaseBox"]; +return $box->base;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "12:EvalBaseBox:isa:iface:self:sub:1:EvalBaseBox" + ); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval `instanceof` uses eval class, interface, and dynamic-target metadata. +#[test] +fn execute_program_evaluates_eval_instanceof_targets() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +class EvalInstanceOther {} +$box = new EvalInstanceChild(); +$class = "EvalInstanceChild"; +$target = ["EvalInstanceIface"]; +$prefix = "EvalInstance"; +$suffix = "Base"; +$targetObject = new EvalInstanceChild(); +echo $box instanceof EvalInstanceChild ? "C" : "c"; +echo $box instanceof EvalInstanceBase ? "B" : "b"; +echo $box instanceof EvalInstanceIface ? "I" : "i"; +echo $box instanceof $class ? "D" : "d"; +echo $box instanceof $target[0] ? "A" : "a"; +echo $box instanceof ($prefix . $suffix) ? "P" : "p"; +echo $box instanceof $targetObject ? "O" : "o"; +echo 7 instanceof MissingEvalClass ? "bad" : "S"; +return $box instanceof EvalInstanceOther;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBIDAPOS"); + assert_eq!(values.get(result), FakeValue::Bool(false)); +} + +/// Verifies dynamic `instanceof` rejects targets that are not strings or objects. +#[test] +fn execute_program_rejects_invalid_dynamic_instanceof_target() { + let program = parse_fragment( + br#"class EvalInvalidInstanceTarget {} +$box = new EvalInvalidInstanceTarget(); +$target = 42; +return $box instanceof $target;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid instanceof target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared classes can implement eval-declared interfaces. +#[test] +fn execute_program_constructs_eval_declared_class_with_dynamic_interface() { + let program = parse_fragment( + br#"interface EvalReader { + function read($n); +} +interface EvalNamedReader extends EvalReader { + function label(); +} +class EvalReaderBox implements EvalNamedReader { + public function read($n) { return $n + 1; } + public function label() { return "box"; } +} +$box = new EvalReaderBox(); +echo $box->read(4); echo ":"; +echo $box->label(); echo ":"; +echo is_a($box, "EvalNamedReader") ? "isa" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalReader") ? "sub" : "bad"; echo ":"; +echo is_subclass_of("EvalReaderBox", "EvalReader") ? "str" : "bad"; echo ":"; +$implements = class_implements($box); +echo count($implements); echo ":"; +echo $implements["EvalNamedReader"]; echo ":"; +echo $implements["EvalReader"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:box:isa:sub:str:2:EvalNamedReader:EvalReader" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies concrete eval classes can implement abstract class and interface contracts. +#[test] +fn execute_program_constructs_concrete_child_from_abstract_eval_class() { + let program = parse_fragment( + br#"interface EvalAbstractReadable { + function read($n); +} +abstract class EvalAbstractBase implements EvalAbstractReadable { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalConcreteBox extends EvalAbstractBase { + public function read($n) { return $n + 3; } +} +$box = new EvalConcreteBox(); +echo $box->wrap(4); echo ":"; +echo is_a($box, "EvalAbstractReadable") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad"; +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:iface:abstract"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval rejects instantiation of abstract eval-declared classes. +#[test] +fn execute_program_rejects_abstract_eval_class_instantiation() { + let program = parse_fragment( + br#"abstract class EvalAbstractOnly { + public function read() { return 1; } +} +new EvalAbstractOnly();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("abstract class instantiation should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies concrete eval classes must implement inherited abstract methods. +#[test] +fn execute_program_rejects_concrete_eval_class_with_abstract_methods() { + let program = parse_fragment( + br#"abstract class EvalNeedsRead { + abstract public function read(); +} +class EvalMissingReadChild extends EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("concrete class missing abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects extending a final eval-declared class. +#[test] +fn execute_program_rejects_extending_final_eval_class() { + let program = parse_fragment( + br#"final class EvalFinalBase {} +class EvalFinalChild extends EvalFinalBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("extending final class should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects overriding a final eval-declared method. +#[test] +fn execute_program_rejects_overriding_final_eval_method() { + let program = parse_fragment( + br#"class EvalFinalMethodBase { + final public function read() { return 1; } +} +class EvalFinalMethodChild extends EvalFinalMethodBase { + public function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects overriding a final eval-declared property. +#[test] +fn execute_program_rejects_overriding_final_eval_property() { + let program = parse_fragment( + br#"class EvalFinalPropertyBase { + final public $value = 1; +} +class EvalFinalPropertyChild extends EvalFinalPropertyBase { + public $value = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared traits contribute methods, properties, and metadata. +#[test] +fn execute_program_constructs_class_using_eval_declared_trait() { + let program = parse_fragment( + br#"trait EvalReusableTrait { + public int $seed = 2; + public function add($n) { return $this->seed + $n; } +} +class EvalTraitBox { + use EvalReusableTrait; + public function read($n) { return $this->add($n) + 1; } +} +$box = new EvalTraitBox(); +echo $box->read(4); echo ":"; +echo trait_exists("EvalReusableTrait") ? "trait" : "bad"; echo ":"; +$traits = get_declared_traits(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$uses = class_uses($box); +echo count($uses); echo ":"; echo $uses["EvalReusableTrait"]; +return $box->seed;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7:trait:1:EvalReusableTrait:1:EvalReusableTrait" + ); + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies eval trait abstract methods can be implemented by the using class. +#[test] +fn execute_program_constructs_class_satisfying_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitNeedsRead { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalTraitReader { + use EvalTraitNeedsRead; + public function read($n) { return $n + 4; } +} +$reader = new EvalTraitReader(); +return $reader->wrap(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(8)); +} +/// Verifies eval rejects a concrete class that leaves a trait abstract method open. +#[test] +fn execute_program_rejects_missing_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitAbstractMethod { + abstract public function read(); +} +class EvalTraitMissingRead { + use EvalTraitAbstractMethod; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("class missing trait abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects classes using traits that are not eval-declared. +#[test] +fn execute_program_rejects_missing_eval_trait_use() { + let program = parse_fragment( + br#"class EvalTraitMissingUse { + use MissingEvalTraitUse; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing eval trait use should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs b/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs new file mode 100644 index 0000000000..c846fa562e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs @@ -0,0 +1,375 @@ +//! Purpose: +//! Interpreter tests for eval interface method presence, variance, staticness, +//! by-reference parameters, and variadics. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Concrete and deferred abstract implementations are checked independently. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval rejects classes missing methods required by eval interfaces. +#[test] +fn execute_program_rejects_missing_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsRead { + function read($n); +} +class EvalMissingRead implements EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing interface method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts covariant return types for interface method contracts. +#[test] +fn execute_program_accepts_covariant_interface_method_return_type() { + let program = parse_fragment( + br#"interface EvalReturnReadable { + function read(): int|string; +} +class EvalReturnReader implements EvalReturnReadable { + public function read(): int { + return 7; + } +} +interface EvalReturnRootSelf { + function linked(): self; +} +interface EvalReturnChildSelf extends EvalReturnRootSelf {} +class EvalReturnSelfImpl implements EvalReturnChildSelf { + public function linked(): EvalReturnRootSelf { + return $this; + } +} +$reader = new EvalReturnReader(); +return $reader->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval rejects missing or wider return types for interface method contracts. +#[test] +fn execute_program_rejects_incompatible_interface_method_return_type() { + let missing_return = parse_fragment( + br#"interface EvalNeedsReturn { + function read(): string; +} +class EvalMissingReturnImpl implements EvalNeedsReturn { + public function read() { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let wider_return = parse_fragment( + br#"interface EvalNeedsStringReturn { + function read(): string; +} +class EvalWiderReturnImpl implements EvalNeedsStringReturn { + public function read(): int|string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_return, &mut scope, &mut values) + .expect_err("wider interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract eval classes must keep declared interface method signatures compatible. +#[test] +fn execute_program_rejects_incompatible_abstract_interface_method_declarations() { + let bad_abstract_param = parse_fragment( + br#"interface EvalAbstractIfaceParam { + function read(int $value); +} +abstract class EvalAbstractIfaceParamBase implements EvalAbstractIfaceParam { + abstract public function read(string $value); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_param, &mut scope, &mut values) + .expect_err("abstract interface method parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_return = parse_fragment( + br#"interface EvalAbstractIfaceReturn { + function read(): int; +} +abstract class EvalAbstractIfaceReturnBase implements EvalAbstractIfaceReturn { + abstract public function read(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_return, &mut scope, &mut values) + .expect_err("abstract interface method return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_method = parse_fragment( + br#"interface EvalInheritedIfaceMethod { + function read(int $value); +} +abstract class EvalInheritedIfaceMethodBase { + public function read(string $value) {} +} +abstract class EvalInheritedIfaceMethodChild extends EvalInheritedIfaceMethodBase implements EvalInheritedIfaceMethod {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_method, &mut scope, &mut values) + .expect_err("inherited incompatible interface method should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract eval classes may defer missing compatible interface methods. +#[test] +fn execute_program_accepts_deferred_abstract_interface_method_declarations() { + let program = parse_fragment( + br#"interface EvalAbstractIfaceDeferred { + function read(int $value): int; +} +abstract class EvalAbstractIfaceDeferredBase implements EvalAbstractIfaceDeferred {} +abstract class EvalAbstractIfaceDeferredTyped implements EvalAbstractIfaceDeferred { + abstract public function read(mixed $value): int; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval accepts PHP-contravariant parameter types for interface contracts. +#[test] +fn execute_program_accepts_contravariant_interface_method_parameter_types() { + let program = parse_fragment( + br#"interface EvalParamContract { + function read(int $value); +} +class EvalParamContractReader implements EvalParamContract { + public function read(mixed $value) { + return $value . ":ok"; + } +} +$reader = new EvalParamContractReader(); +return $reader->read(8);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("8:ok".to_string())); +} + +/// Verifies eval rejects interface implementations with incompatible parameter types. +#[test] +fn execute_program_rejects_incompatible_interface_method_parameter_types() { + let incompatible_type = parse_fragment( + br#"interface EvalParamStringContract { + function read(int $value); +} +class EvalParamStringReader implements EvalParamStringContract { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible interface parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"interface EvalParamUntypedContract { + function read($value); +} +class EvalParamTypedReader implements EvalParamUntypedContract { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter implementation of untyped contract should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval static interface method contracts are satisfied by public static methods. +#[test] +fn execute_program_accepts_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read($n); +} +class EvalStaticReader implements EvalNeedsStaticRead { + public static function read($n) { + return $n . "!"; + } +} +return EvalStaticReader::read("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok!".to_string())); +} + +/// Verifies eval rejects instance methods for static interface method contracts. +#[test] +fn execute_program_rejects_instance_method_for_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read(); +} +class EvalInstanceReader implements EvalNeedsStaticRead { + public function read() {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("instance method should not satisfy static interface method"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval interface method contracts require matching by-reference parameters. +#[test] +fn execute_program_validates_interface_method_by_ref_parameters() { + let program = parse_fragment( + br#"interface EvalRefReadable { + function read(&$value); +} +class EvalRefReader implements EvalRefReadable { + public function read(&$value) { + $value = "ok"; + } +} +$value = "bad"; +$reader = new EvalRefReader(); +$reader->read($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok".to_string())); + + let bad_value_impl = parse_fragment( + br#"interface EvalNeedsByRef { + function read(&$value); +} +class EvalByValueReader implements EvalNeedsByRef { + public function read($value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_value_impl, &mut scope, &mut values) + .expect_err("by-value implementation must not satisfy by-reference contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_ref_impl = parse_fragment( + br#"interface EvalNeedsByValue { + function read($value); +} +class EvalByRefReader implements EvalNeedsByValue { + public function read(&$value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_ref_impl, &mut scope, &mut values) + .expect_err("by-reference implementation must not satisfy by-value contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies variadic eval methods can satisfy fixed-arity interface contracts. +#[test] +fn execute_program_accepts_variadic_method_for_fixed_interface_contract() { + let program = parse_fragment( + br#"interface EvalFixedReadable { + function read($left, $right); +} +class EvalVariadicReadable implements EvalFixedReadable { + public function read($left, ...$tail) { + return $left . $tail[0]; + } +} +$box = new EvalVariadicReadable(); +return $box->read("A", "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies non-variadic eval methods cannot satisfy variadic interface contracts. +#[test] +fn execute_program_rejects_non_variadic_method_for_variadic_interface_contract() { + let program = parse_fragment( + br#"interface EvalVariadicReadable { + function read($left, ...$tail); +} +class EvalFixedReadable implements EvalVariadicReadable { + public function read($left, $tail = null) { + return $left; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-variadic implementation should not satisfy variadic contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs b/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs new file mode 100644 index 0000000000..69df12b452 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs @@ -0,0 +1,322 @@ +//! Purpose: +//! Interpreter tests for method override visibility, arity, variance, and return +//! type enforcement. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Both declaration compatibility and runtime return values are validated. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval rejects overriding a public method with lower visibility. +#[test] +fn execute_program_rejects_method_override_with_reduced_visibility() { + let program = parse_fragment( + br#"class EvalVisibleBase { + public function read() { return 1; } +} +class EvalVisibleChild extends EvalVisibleBase { + protected function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("reduced method visibility should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects parent method overrides that require more arguments. +#[test] +fn execute_program_rejects_method_override_with_narrower_arity() { + let program = parse_fragment( + br#"class EvalArityBase { + public function read($value = "base") { return $value; } +} +class EvalArityChild extends EvalArityBase { + public function read($value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("narrower method override arity should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts PHP-contravariant method parameter type overrides. +#[test] +fn execute_program_accepts_contravariant_method_parameter_type_overrides() { + let program = parse_fragment( + br#"class EvalParamBase { + public function anyInt(int $value) { return $value; } + public function maybeInt(int $value) { return $value; } + public function untypedInt(int $value) { return $value; } +} +class EvalParamChild extends EvalParamBase { + public function anyInt(mixed $value) { return $value . ":mixed"; } + public function maybeInt(?int $value) { return $value; } + public function untypedInt($value) { return $value; } +} +$child = new EvalParamChild(); +echo $child->anyInt(7); echo ":"; +echo $child->untypedInt("ok"); +return $child->maybeInt(null) === null;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:mixed:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval rejects method parameter overrides that narrow PHP's accepted type set. +#[test] +fn execute_program_rejects_incompatible_method_parameter_type_overrides() { + let incompatible_type = parse_fragment( + br#"class EvalParamTypeBase { + public function read(int $value) { return $value; } +} +class EvalParamStringChild extends EvalParamTypeBase { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let narrower_nullable = parse_fragment( + br#"class EvalParamNullableBase { + public function maybe(?int $value) { return $value; } +} +class EvalParamNonNullChild extends EvalParamNullableBase { + public function maybe(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&narrower_nullable, &mut scope, &mut values) + .expect_err("narrower nullable parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"class EvalParamUntypedBase { + public function read($value) { return $value; } +} +class EvalParamTypedChild extends EvalParamUntypedBase { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter override of untyped parent should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts covariant method return type overrides. +#[test] +fn execute_program_accepts_covariant_method_return_type_overrides() { + let program = parse_fragment( + br#"class EvalReturnBase { + public function id(): ?int { return 1; } + public function make(): EvalReturnBase { return $this; } + public function selfType(): self { return $this; } +} +class EvalReturnChild extends EvalReturnBase { + public function id(): int { return 2; } + public function make(): EvalReturnChild { return $this; } + public function selfType(): static { return $this; } +} +class EvalReturnParentRoot {} +class EvalReturnParentBase extends EvalReturnParentRoot { + public function parentKeyword(): EvalReturnParentRoot { return new EvalReturnParentRoot(); } +} +class EvalReturnParentChild extends EvalReturnParentBase { + public function parentKeyword(): parent { return new EvalReturnParentBase(); } +} +class EvalReturnMixedBase { + public function maybe(): mixed { return null; } +} +class EvalReturnMixedChild extends EvalReturnMixedBase { + public function maybe(): ?int { return null; } +} +$child = new EvalReturnChild(); +return $child->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); +} + +/// Verifies eval rejects method overrides that widen declared return types. +#[test] +fn execute_program_rejects_incompatible_method_return_type_overrides() { + let wider_nullable = parse_fragment( + br#"class EvalReturnNarrowBase { + public function id(): int { return 1; } +} +class EvalReturnWiderNullable extends EvalReturnNarrowBase { + public function id(): ?int { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_nullable, &mut scope, &mut values) + .expect_err("wider nullable return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_return = parse_fragment( + br#"class EvalReturnRequiredBase { + public function label(): string { return "base"; } +} +class EvalReturnMissingChild extends EvalReturnRequiredBase { + public function label() { return "child"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_to_self = parse_fragment( + br#"class EvalReturnStaticBase { + public function make(): static { return $this; } +} +class EvalReturnSelfChild extends EvalReturnStaticBase { + public function make(): self { return $this; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_to_self, &mut scope, &mut values) + .expect_err("static return type should not widen to self"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let nullable_to_mixed = parse_fragment( + br#"class EvalReturnNullableBase { + public function maybe(): ?int { return null; } +} +class EvalReturnMixedChildBad extends EvalReturnNullableBase { + public function maybe(): mixed { return null; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&nullable_to_mixed, &mut scope, &mut values) + .expect_err("mixed return type should widen nullable int"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval enforces declared method return values at runtime. +#[test] +fn execute_program_enforces_eval_method_return_type_values() { + let program = parse_fragment( + br#"class EvalReturnRuntimeBase { + public function id(): int { return "12"; } + public function makeSelf(): self { return new EvalReturnRuntimeBase(); } + public function done(): void { return; } +} +class EvalReturnRuntimeChild extends EvalReturnRuntimeBase {} +$child = new EvalReturnRuntimeChild(); +echo $child->id(); echo ":"; +echo get_class($child->makeSelf()); echo ":"; +$child->done(); +return 3;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:EvalReturnRuntimeBase:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval rejects method return values that do not satisfy declarations. +#[test] +fn execute_program_rejects_invalid_eval_method_return_type_values() { + let bad_scalar = parse_fragment( + br#"class EvalReturnBadScalar { + public function id(): int { return "nope"; } +} +$box = new EvalReturnBadScalar(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_scalar, &mut scope, &mut values) + .expect_err("non-numeric string should fail int return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_void = parse_fragment( + br#"class EvalReturnBadVoid { + public function done(): void { return null; } +} +$box = new EvalReturnBadVoid(); +return $box->done();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_void, &mut scope, &mut values) + .expect_err("explicit value should fail void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_static = parse_fragment( + br#"class EvalReturnStaticRuntimeBase { + public function make(): static { return new EvalReturnStaticRuntimeBase(); } +} +class EvalReturnStaticRuntimeChild extends EvalReturnStaticRuntimeBase {} +$child = new EvalReturnStaticRuntimeChild(); +return $child->make();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_static, &mut scope, &mut values) + .expect_err("base instance should fail inherited static return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let implicit_return = parse_fragment( + br#"class EvalReturnImplicitBad { + public function id(): ?int {} +} +$box = new EvalReturnImplicitBad(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&implicit_return, &mut scope, &mut values) + .expect_err("implicit return should fail non-void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs b/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs new file mode 100644 index 0000000000..6217ca6c2f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Organizes interpreter expression tests by scalar/object execution, class +//! behavior, visibility, and callable contracts. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod classes_traits; +mod interface_contracts; +mod method_contracts; +mod scalars_objects; +mod visibility; diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs b/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs new file mode 100644 index 0000000000..6326553828 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs @@ -0,0 +1,341 @@ +//! Purpose: +//! Interpreter tests for scalar expressions, echo/print, object calls, and +//! runtime constructor argument handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases assert EvalIR expression execution against fake runtime values. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies simple variable compound assignments read, compute, and write the scope value. +#[test] +fn execute_program_evaluates_compound_assignments() { + let program = + parse_fragment(br#"$x = 2; $x += 3; $x *= 4; $x -= 5; $s = "v"; $s .= $x; echo $s;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "v15"); + assert_eq!(values.get(x), FakeValue::Int(15)); +} +/// Verifies division and modulo evaluate through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_division_and_modulo() { + let program = parse_fragment(br#"$x = 20; $x /= 2; $x %= 6; echo $x; return 9 / 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "4"); + assert_eq!(values.get(x), FakeValue::Int(4)); + assert_eq!(values.get(result), FakeValue::Float(4.5)); +} +/// Verifies exponentiation evaluates through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_exponentiation() { + let program = parse_fragment( + br#"$x = 2; $x **= 3; echo $x; echo ":"; echo -2 ** 2; return 2 ** 3 ** 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "8:-4"); + assert_eq!(values.get(x), FakeValue::Float(8.0)); + assert_eq!(values.get(result), FakeValue::Float(512.0)); +} +/// Verifies bitwise and shift operators evaluate through fake runtime hooks. +#[test] +fn execute_program_evaluates_bitwise_and_shift_ops() { + let program = parse_fragment( + br#"$x = 6; $x &= 3; echo $x; echo ":"; +$x = 4; $x |= 1; echo $x; echo ":"; +$x = 7; $x ^= 3; echo $x; echo ":"; +$x = 1; $x <<= 5; echo $x; echo ":"; +$x = 64; $x >>= 3; echo $x; echo ":"; +echo ~0; echo ":"; echo -16 >> 2; +return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} +/// Verifies simple variable increment and decrement statements update the scope value. +#[test] +fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} +/// Verifies echo and unset operate through runtime hooks and scope metadata. +#[test] +fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); +} +/// Verifies comma-separated echo expressions are executed in source order. +#[test] +fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "abc"); +} +/// Verifies print writes output and returns integer 1. +#[test] +fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval property reads and writes dispatch through runtime hooks. +#[test] +fn execute_program_reads_and_writes_object_property() { + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); +} +/// Verifies eval method calls dispatch through the runtime method hook. +#[test] +fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval method calls forward evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_two_arguments() { + let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval method calls forward numerically unpacked arguments. +#[test] +fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval object construction dispatches through runtime hooks. +#[test] +fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); +} +/// Verifies eval object construction passes constructor arguments through runtime hooks. +#[test] +fn execute_program_constructs_named_object_with_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); + + assert_eq!(values.get(x), FakeValue::Int(1)); +} + +/// Verifies eval object construction binds registered AOT constructor named arguments. +#[test] +fn execute_program_constructs_named_object_with_registered_named_args() { + let program = parse_fragment(br#"$box = new KnownClass(value: 9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(9)); +} + +/// Verifies runtime/AOT constructor fallback honors by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_constructor_by_ref_temporary_arg() { + let program = parse_fragment(br#"$box = new KnownClass(9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a constructor by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies runtime/AOT constructor fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_type_coercion() { + let program = parse_fragment( + br#"$value = "9"; +$box = new KnownClass($value); +echo $box->read_x(); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor by-ref coercion should bind"); + + assert_eq!(values.output, "9"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} + +/// Verifies AOT constructor by-reference writeback still runs when construction fatals. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "9"; +new KnownFailingConstructor($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownFailingConstructor", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("failing constructor should abort after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(value), FakeValue::Int(9)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs b/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs new file mode 100644 index 0000000000..f7722a00f9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs @@ -0,0 +1,207 @@ +//! Purpose: +//! Interpreter tests for private/protected member access, shadowing, and missing +//! method failures. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Declaring-class scope and global-scope failures use separate cases. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval methods can access private properties and methods declared in their class. +#[test] +fn execute_program_allows_private_eval_members_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalPrivateBox { + private int $secret = 4; + private function bump($n) { return $this->secret + $n; } + public function read($n) { return $this->bump($n); } +} +$box = new EvalPrivateBox(); +return $box->read(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies protected eval members are accessible across a class hierarchy. +#[test] +fn execute_program_allows_protected_eval_members_from_related_classes() { + let program = parse_fragment( + br#"class EvalProtectedBase { + protected int $base = 5; + protected function add($n) { return $this->base + $n; } +} +class EvalProtectedChild extends EvalProtectedBase { + public function read($n) { return $this->add($n); } +} +$box = new EvalProtectedChild(); +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval child properties shadow private parent properties with a separate storage slot. +#[test] +fn execute_program_shadows_private_eval_parent_property_with_separate_slot() { + let program = parse_fragment( + br#"class EvalPrivateShadowBase { + private $value = 1; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateShadowChild extends EvalPrivateShadowBase { + public $value = "child"; + + public function childValue() { + return $this->value; + } +} +$box = new EvalPrivateShadowChild(); +echo $box->parentValue(); echo ":"; +echo $box->childValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:child:child"); +} + +/// Verifies eval later redeclarations update the visible slot while preserving a private grandparent slot. +#[test] +fn execute_program_keeps_eval_private_grandparent_slot_after_later_redeclaration() { + let program = parse_fragment( + br#"class EvalPrivateGrandBase { + private $value = 1; + + public function grandValue() { + return $this->value; + } +} +class EvalPrivateGrandParent extends EvalPrivateGrandBase { + public $value = 2; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateGrandChild extends EvalPrivateGrandParent { + public $value = 3; +} +$box = new EvalPrivateGrandChild(); +echo $box->grandValue(); echo ":"; +echo $box->parentValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:3"); +} + +/// Verifies eval throws Error for private property access from global scope. +#[test] +fn execute_program_private_eval_member_access_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalPrivateGlobalBox { + private int $secret = 4; + private function read() { return $this->secret; } +} +$box = new EvalPrivateGlobalBox(); +try { + echo $box->secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access private property EvalPrivateGlobalBox::$secret" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval throws Error for calls to private methods from global scope. +#[test] +fn execute_program_private_eval_method_call_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalPrivateMethodBox { + private function read() { return 4; } +} +$box = new EvalPrivateMethodBox(); +try { + echo $box->read(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to private method EvalPrivateMethodBox::read() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies missing eval-declared instance methods throw PHP-compatible Error values. +#[test] +fn execute_program_missing_eval_method_call_throws_error() { + let program = parse_fragment( + br#"class EvalMissingMethodBox {} +$box = new EvalMissingMethodBox(); +try { + echo $box->missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to undefined method EvalMissingMethodBox::missing()" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs b/crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs new file mode 100644 index 0000000000..44c5231ad0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs @@ -0,0 +1,397 @@ +//! Purpose: +//! Interpreter tests for nested eval, magic constants, namespaces, functions, globals, and argument rules. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases share a persistent eval context when declarations must survive across calls. + +use super::super::*; +use super::support::*; + +/// Verifies nested eval calls parse and execute against the same dynamic scope. +#[test] +fn execute_program_nested_eval_uses_same_scope() { + let program = + parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies `__LINE__` inside eval uses the source line within the fragment. +#[test] +fn execute_program_magic_line_uses_fragment_line() { + let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies file-dependent eval magic constants use call-site metadata from the context. +#[test] +fn execute_program_magic_file_and_dir_use_context_call_site() { + let program = + parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/main.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) + ); +} +/// Verifies eval class, namespace, and trait magic constants are empty in eval scope. +#[test] +fn execute_program_scope_magic_constants_are_empty_strings() { + let program = + parse_fragment(br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); +} +/// Verifies eval-declared functions can be called by the same fragment. +#[test] +fn execute_program_calls_declared_function() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval namespace declarations qualify functions and namespace magic values. +#[test] +fn execute_program_namespace_qualifies_declared_function() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } +return dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) + ); +} +/// Verifies unqualified namespaced calls fall back to global builtins when needed. +#[test] +fn execute_program_namespace_call_falls_back_to_builtin() { + let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies namespaced dynamic functions take precedence over global builtin fallback. +#[test] +fn execute_program_namespace_function_overrides_builtin_fallback() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function strlen($value) { return 99; } +return strlen("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(99)); +} +/// Verifies unqualified namespaced constants fall back to global predefined constants. +#[test] +fn execute_program_namespace_const_fetch_falls_back_to_global() { + let program = + parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("\n".to_string())); +} +/// Verifies namespaced dynamic constants take precedence over global fallback. +#[test] +fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { + let program = + parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(7).expect("create fake int"); + assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. +#[test] +fn execute_program_namespace_use_function_import_dispatches() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 1; } +namespace Eval\App; +use function Eval\Lib\target as AliasTarget; +return aliastarget(6);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval namespace `use const` imports fetch qualified dynamic constants. +#[test] +fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { + let program = parse_fragment( + br#"namespace Eval\App; +use const Eval\Lib\VALUE as LocalValue; +return LocalValue;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(11).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(11)); +} +/// Verifies eval grouped namespace imports dispatch dynamic functions and constants. +#[test] +fn execute_program_grouped_namespace_use_imports_dispatch() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 2; } +namespace Eval\App; +use function Eval\Lib\{target as AliasTarget}; +use const Eval\Lib\{VALUE as LocalValue}; +return AliasTarget(LocalValue);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(5).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval-declared functions bind named arguments by parameter name. +#[test] +fn execute_program_calls_declared_function_with_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval-declared functions unpack indexed arrays as positional arguments. +#[test] +fn execute_program_calls_declared_function_with_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies string keys unpack as named arguments for eval-declared functions. +#[test] +fn execute_program_calls_declared_function_with_named_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval-declared function static locals persist between calls. +#[test] +fn execute_program_static_var_persists_in_declared_function() { + let program = parse_fragment( + br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } +return (dyn() * 10) + dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(24)); +} +/// Verifies top-level eval static declarations reinitialize on each eval execution. +#[test] +fn execute_program_top_level_static_var_reinitializes_per_eval() { + let program = + parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute first eval ir"); + let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute second eval ir"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(1)); +} +/// Verifies `global` declarations read and write the context global scope. +#[test] +fn execute_program_global_alias_writes_context_global_scope() { + let program = + parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.get(global), FakeValue::Int(2)); +} +/// Verifies references to global aliases write the source global variable. +#[test] +fn execute_program_reference_alias_to_global_updates_source_global() { + let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(4)); + assert_eq!(values.get(global), FakeValue::Int(4)); + assert!(global_scope.visible_cell("alias").is_none()); +} +/// Verifies named calls reject positional arguments that follow named arguments. +#[test] +fn execute_program_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert_eq!(values.output, ""); +} +/// Verifies named calls reject argument unpacking after named arguments. +#[test] +fn execute_program_rejects_spread_after_named_arg() { + let program = + parse_fragment(br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies function-scope magic constants keep the eval declaration spelling. +#[test] +fn execute_program_magic_function_and_method_use_eval_declared_name() { + let program = parse_fragment( + br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("DynMagicCase:DynMagicCase".to_string()) + ); +} +/// Verifies eval-declared functions persist in a shared eval context. +#[test] +fn execute_program_context_keeps_declared_function() { + let define = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); + let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs new file mode 100644 index 0000000000..f8ad7b0374 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs @@ -0,0 +1,152 @@ +//! Purpose: +//! Interpreter tests for named, default, constant-default, and variadic argument +//! binding on eval methods and constructors. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover named arguments and named unpacking on instance methods, +//! static methods, and constructors declared inside eval fragments. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared instance, static, and constructor methods bind named args. +#[test] +fn execute_program_binds_eval_method_named_args() { + let program = parse_fragment( + br#"class EvalNamedMethodBox { + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function read($left, $right) { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left, $right) { + return $left . "-" . $right; + } +} +$box = new EvalNamedMethodBox(right: "B", left: "A"); +echo $box->read(right: "D", left: "C"); echo ":"; +$args = ["right" => "F", "left" => "E"]; +echo $box->read(...$args); echo ":"; +return EvalNamedMethodBox::join(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + +/// Verifies eval-declared methods use default values for omitted arguments. +#[test] +fn execute_program_binds_eval_method_default_args() { + let program = parse_fragment( + br#"class EvalDefaultMethodBox { + public function __construct($left = "A", $right = "B") { + $this->label = $left . $right; + } + public function read($left, $right = "D") { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left = "G", $right = "H") { + return $left . "-" . $right; + } +} +$box = new EvalDefaultMethodBox(); +echo $box->read("C"); echo ":"; +echo $box->read(right: "F", left: "E"); echo ":"; +return EvalDefaultMethodBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + +/// Verifies eval-declared methods materialize constant-expression parameter defaults. +#[test] +fn execute_program_binds_eval_method_constant_default_args() { + let program = parse_fragment( + br#"define("EVAL_METHOD_DEFAULT_GLOBAL", "G"); +class EvalDefaultConstBase { + const LABEL = "base"; +} +interface EvalDefaultConstIface { + const WORD = "iface"; +} +class EvalDefaultConstDep { + public function __construct($label = "dep") { + $this->label = $label; + } + public function read() { + return $this->label; + } +} +class EvalDefaultConstBox extends EvalDefaultConstBase { + const LABEL = "box"; + public function __construct($label = self::LABEL) { + $this->label = $label; + } + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; + } + public static function join($label = self::LABEL, $parent = parent::LABEL) { + return $label . "-" . $parent; + } +} +$box = new EvalDefaultConstBox(); +echo $box->read(); echo ":"; +return EvalDefaultConstBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:" + ); + assert_eq!(values.get(result), FakeValue::String("box-base".to_string())); +} + +/// Verifies eval-declared methods bind positional and named values into variadic arrays. +#[test] +fn execute_program_binds_eval_method_variadic_args() { + let program = parse_fragment( + br#"class EvalVariadicMethodBox { + public function __construct(...$parts) { + $this->label = $parts[0] . $parts["right"]; + } + public function read($head, ...$tail) { + echo count($tail); echo ":"; + return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; + } + public static function join(...$items) { + return $items[0] . $items[1]; + } +} +$box = new EvalVariadicMethodBox("A", right: "B"); +echo $box->read("C", "D", named: "E", tail: "F"); echo ":"; +return EvalVariadicMethodBox::join("G", "H");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:AB:C:D:E:F:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs new file mode 100644 index 0000000000..bc80635cad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs @@ -0,0 +1,337 @@ +//! Purpose: +//! Interpreter tests for eval method by-reference binding and writeback across +//! variables, arrays, object/static properties, and nested elements. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Invalid temporary and readonly targets retain their fatal behavior. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared instance, static, and constructor methods write back by-reference args. +#[test] +fn execute_program_writes_back_eval_method_by_ref_args() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function __construct(&$value) { + $value = $value . "-ctor"; + } + public function change(&$value) { + $value = $value . "-method"; + } + public static function changeStatic(&$value) { + $value = $value . "-static"; + } +} +$ctor = "A"; +$box = new EvalByRefMethodBox($ctor); +$box->change($ctor); +EvalByRefMethodBox::changeStatic($ctor); +$named = "B"; +$box->change(value: $named); +echo $ctor; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-ctor-method-static:"); + assert_eq!(values.get(result), FakeValue::String("B-method".to_string())); +} + +/// Verifies eval-declared by-reference method parameters reject temporary values. +#[test] +fn execute_program_rejects_eval_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function change(&$value) { + $value = "changed"; + } +} +$box = new EvalByRefMethodBox(); +$box->change("literal");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies typed eval-declared by-reference method params write back entry coercions. +#[test] +fn execute_program_writes_back_eval_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"class EvalByRefTypedMethodBox { + public function coerce(int &$value) {} +} +$value = "3"; +$box = new EvalByRefTypedMethodBox(); +$box->coerce($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval-declared by-reference variadics write back mutated captured elements. +#[test] +fn execute_program_writes_back_eval_method_by_ref_variadic_elements() { + let program = parse_fragment( + br#"class EvalByRefVariadicMethodBox { + public function change(&...$items) { + $items[0] = $items[0] . "-first"; + $items["named"] = $items["named"] . "-named"; + } + public function rebind(&...$items) { + $items = []; + } +} +$box = new EvalByRefVariadicMethodBox(); +$first = "A"; +$named = "B"; +$box->change($first, named: $named); +$box->rebind($first); +echo $first; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-first:"); + assert_eq!(values.get(result), FakeValue::String("B-named".to_string())); +} + +/// Verifies eval-declared by-reference method params write back array-element lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_array_elements() { + let program = parse_fragment( + br#"class EvalByRefArrayElementMethodBox { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +$box = new EvalByRefArrayElementMethodBox(); +$items = ["k" => "old"]; +$box->set($items["k"], "changed"); +$box->set($missing["new"], "created"); +$box->variadic($items["k"]); +echo $items["k"]; echo ":"; +return $missing["new"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "variadic:"); + assert_eq!(values.get(result), FakeValue::String("created".to_string())); +} + +/// Verifies eval-declared by-reference method params write back object-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_object_properties() { + let program = parse_fragment( + br#"class EvalByRefPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +class EvalByRefPublicPropertyBox { + public string $value = "old"; +} +class EvalByRefPrivatePropertyBox { + private string $value = "private"; + public function update($changer) { + $name = "value"; + $changer->set($this->{$name}, "secret"); + return $this->value; + } +} +$changer = new EvalByRefPropertyChanger(); +$public = new EvalByRefPublicPropertyBox(); +$changer->set($public->value, "changed"); +$changer->variadic($public->value); +$name = "value"; +$changer->set($public->{$name}, "dynamic"); +echo $public->value; echo ":"; +$private = new EvalByRefPrivatePropertyBox(); +return $private->update($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "dynamic:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + +/// Verifies eval-declared by-reference method params write back static-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_static_properties() { + let program = parse_fragment( + br#"class EvalByRefStaticPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefStaticPropertyBox { + public static string $value = "old"; + public static string $other = "second"; + public static string $third = "third"; + private static string $secret = "private"; + public static function updatePrivate($changer) { + $changer->set(self::$secret, "secret"); + return self::$secret; + } +} +$changer = new EvalByRefStaticPropertyChanger(); +$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); +echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value); +echo ":"; +echo EvalByRefStaticPropertyBox::$value; echo ":"; +$class = "EvalByRefStaticPropertyBox"; +$changer->set($class::$other, "dynamic"); +$name = "third"; +$changer->set($class::${$name}, "name"); +echo EvalByRefStaticPropertyBox::$other; echo ":"; +echo EvalByRefStaticPropertyBox::$third; echo ":"; +return EvalByRefStaticPropertyBox::updatePrivate($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "right:right:dynamic:name:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + +/// Verifies by-reference method params write back array elements stored in properties. +#[test] +fn execute_program_writes_back_eval_method_by_ref_property_array_elements() { + let program = parse_fragment( + br#"class EvalByRefPropertyArrayElementChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefPropertyArrayElementBox { + public $items = ["first" => "old", "same" => "same"]; + public $other = null; + public static $staticItems = ["first" => "static-old", "same" => "static-same"]; +} +$changer = new EvalByRefPropertyArrayElementChanger(); +$box = new EvalByRefPropertyArrayElementBox(); +$changer->set($box->items["first"], "changed"); +$name = "items"; +$changer->set($box->{$name}["dynamic"], "dynamic"); +$changer->set($box->other["created"], "created"); +echo $box->items["first"]; echo ":"; +echo $box->items["dynamic"]; echo ":"; +echo $box->other["created"]; echo ":"; +echo $changer->pair($box->items["same"], $box->items["same"]); echo ":"; +echo $box->items["same"]; echo ":"; +$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); +$class = "EvalByRefPropertyArrayElementBox"; +$staticName = "staticItems"; +$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); +echo EvalByRefPropertyArrayElementBox::$staticItems["first"]; echo ":"; +echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"]; echo ":"; +return $changer->pair( + EvalByRefPropertyArrayElementBox::$staticItems["same"], + EvalByRefPropertyArrayElementBox::$staticItems["same"] +) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "changed:dynamic:created:right:right:static:static-dynamic:"); + assert_eq!(values.get(result), FakeValue::String("right:right".to_string())); +} + +/// Verifies eval-declared by-reference method params keep property access restrictions. +#[test] +fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { + let private_program = parse_fragment( + br#"class EvalByRefPrivatePropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefPrivatePropertyFailBox { + private string $value = "private"; +} +$changer = new EvalByRefPrivatePropertyFailChanger(); +$box = new EvalByRefPrivatePropertyFailBox(); +$changer->set($box->value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&private_program, &mut scope, &mut values) + .expect_err("private property by-ref target should fail from global scope"); + assert_eq!(err, EvalStatus::UncaughtThrowable); + + let readonly_program = parse_fragment( + br#"class EvalByRefReadonlyPropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefReadonlyPropertyFailBox { + public readonly string $value; + public function __construct($changer) { + $this->value = "old"; + $changer->set($this->value); + } +} +new EvalByRefReadonlyPropertyFailBox(new EvalByRefReadonlyPropertyFailChanger());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_program, &mut scope, &mut values) + .expect_err("readonly property by-ref target should fail as an indirect modification"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs new file mode 100644 index 0000000000..5c763f818e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs @@ -0,0 +1,14 @@ +//! Purpose: +//! Organizes interpreter tests for eval and runtime method argument binding by +//! defaults, references, types, and fallback surface. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod binding_defaults; +mod by_reference; +mod runtime_fallback; +mod types_errors; diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs new file mode 100644 index 0000000000..fa4f36b7dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs @@ -0,0 +1,166 @@ +//! Purpose: +//! Interpreter tests for runtime/AOT method argument fallback, named binding, +//! by-reference validation, coercion writeback, and fatal paths. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Instance and static runtime hooks preserve writeback before errors. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies runtime/AOT method fallback binds registered native method named arguments. +#[test] +fn execute_program_binds_registered_runtime_method_named_args() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(right: 2, left: 3);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(15)); +} + +/// Verifies runtime/AOT method fallback honors registered by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(1, 2);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a runtime by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies runtime/AOT method fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +echo $box->add2_x($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method by-ref coercion should bind"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies AOT instance method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +$box->add2_x($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + +/// Verifies AOT static method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_static_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "3"; +KnownClass::sum($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime static method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + +/// Verifies runtime/AOT method fallback rejects named arguments without metadata. +#[test] +fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { + let program = + parse_fragment(br#"return $this->answer(value: 1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unregistered runtime method fallback named args should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs new file mode 100644 index 0000000000..b41c406eb3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Interpreter tests for duplicate/unknown arguments, omission rules, and scalar, +//! object, or variadic method type hints. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Binding diagnostics and runtime coercion checks use separate cases. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared variadic methods reject duplicate named variadic keys. +#[test] +fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { + let program = parse_fragment( + br#"class EvalDuplicateVariadicBox { + public function read(...$tail) { + return count($tail); + } +} +$box = new EvalDuplicateVariadicBox(); +return $box->read(name: "A", name: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("duplicate named variadic argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies defaults before required eval method parameters do not make earlier slots optional. +#[test] +fn execute_program_rejects_eval_method_default_before_required_omission() { + let program = parse_fragment( + br#"class EvalRequiredAfterDefaultBox { + public function read($left = "A", $right) { + return $left . $right; + } +} +$box = new EvalRequiredAfterDefaultBox(); +return $box->read(right: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("default before required parameter should remain required"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared method scalar type hints coerce weak scalar arguments. +#[test] +fn execute_program_enforces_eval_method_scalar_type_hints() { + let program = parse_fragment( + br#"class EvalTypedScalarBox { + public function read(int $id, string $label, bool $flag) { + echo $id + 1; echo ":"; + echo $label; echo ":"; + return $flag ? "T" : "F"; + } +} +$box = new EvalTypedScalarBox(); +return $box->read("7", 8, 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:8:"); + assert_eq!(values.get(result), FakeValue::String("T".to_string())); +} + +/// Verifies eval-declared method scalar type hints reject non-coercible values. +#[test] +fn execute_program_rejects_eval_method_scalar_type_mismatch() { + let program = parse_fragment( + br#"class EvalTypedScalarFailBox { + public function read(int $id) { + return $id; + } +} +$box = new EvalTypedScalarFailBox(); +return $box->read("not numeric");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-numeric string should fail int parameter type"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared method class/interface type hints accept matching eval objects. +#[test] +fn execute_program_enforces_eval_method_object_type_hints() { + let program = parse_fragment( + br#"interface EvalTypedReadable {} +class EvalTypedDep implements EvalTypedReadable {} +class EvalTypedObjectBox { + public function read(EvalTypedReadable $dep, ?EvalTypedDep $nullable) { + echo get_class($dep); echo ":"; + return $nullable === null ? "N" : "bad"; + } +} +$dep = new EvalTypedDep(); +$box = new EvalTypedObjectBox(); +return $box->read($dep, null);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalTypedDep:"); + assert_eq!(values.get(result), FakeValue::String("N".to_string())); +} + +/// Verifies eval-declared variadic method type hints apply to each captured argument. +#[test] +fn execute_program_enforces_eval_method_variadic_type_hints() { + let program = parse_fragment( + br#"class EvalTypedVariadicBox { + public function sum(int ...$items) { + return $items[0] + $items[1]; + } +} +$box = new EvalTypedVariadicBox(); +return $box->sum("3", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval-declared methods reject unknown named arguments. +#[test] +fn execute_program_rejects_unknown_eval_method_named_arg() { + let program = parse_fragment( + br#"class EvalUnknownNamedMethodBox { + public function read($left) { + return $left; + } +} +$box = new EvalUnknownNamedMethodBox(); +return $box->read(missing: "bad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unknown named method argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs new file mode 100644 index 0000000000..482988cf96 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Interpreter test module wiring and shared fake runtime support. +//! The concrete tests live in focused child modules so each file owns one +//! execution surface instead of one large mixed test bucket. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `support` exposes the fake runtime cells used by all interpreter tests. +//! - Child modules import the interpreter entry points from their parent module. + +mod array_literals; +mod builtins_arrays_core; +mod builtins_arrays_iterators; +mod builtins_arrays_sets; +mod builtins_class_metadata; +mod builtins_debug_output; +mod builtins_directory_streams; +mod builtins_file_streams; +mod builtins_filesystem_metadata; +mod builtins_filesystem_ops; +mod builtins_json; +mod builtins_language_constructs; +mod builtins_math_formatting; +mod builtins_process_pipes; +mod builtins_raw_memory; +mod builtins_readline; +mod builtins_reflection_functions; +mod builtins_scalars; +mod builtins_spl_autoload; +mod builtins_stream_contexts; +mod builtins_stream_extensions; +mod builtins_stream_settings; +mod builtins_stream_sockets; +mod builtins_stream_wrapper_cast; +mod builtins_stream_wrapper_directories; +mod builtins_stream_wrapper_file_io; +mod builtins_stream_wrapper_metadata; +mod builtins_stream_wrapper_options; +mod builtins_stream_wrapper_path_ops; +mod builtins_stream_wrappers; +mod builtins_strings_binary; +mod builtins_strings_encoding; +mod builtins_strings_text; +mod builtins_symbols; +mod builtins_system_network; +mod class_constants; +mod classes; +mod closures; +mod control_flow; +mod core; +mod dynamic_calls; +mod enums; +mod expressions; +mod functions_namespaces; +mod method_arguments; +mod native_scope; +mod static_members; +mod support; +mod trait_adaptations; diff --git a/crates/elephc-magician/src/interpreter/tests/native_scope.rs b/crates/elephc-magician/src/interpreter/tests/native_scope.rs new file mode 100644 index 0000000000..cab78a9071 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/native_scope.rs @@ -0,0 +1,348 @@ +//! Purpose: +//! Interpreter tests for native function dispatch, scope array mutation, ownership, break, and continue. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover integration edges between scope cells and fake runtime hooks. + +use super::super::*; +use super::support::*; + +/// Verifies eval fragments can dispatch registered native AOT functions. +#[test] +fn execute_program_calls_registered_native_function() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} + +/// Verifies registered native AOT function return types are enforced after dispatch. +#[test] +fn execute_program_checks_registered_native_function_return_type() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.string("not-an-int").expect("allocate fake result"); + let mut native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + native.set_return_type(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("native return type mismatch should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies raw native by-reference staging is released when invoker argument setup fails. +#[test] +fn execute_program_cleans_native_raw_ref_slots_when_arg_array_build_fails() { + let program = parse_fragment(br#"$value = "keep"; native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 1); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + values.fail_array_set_call(0); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("argument-array build failure should abort native dispatch"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + +/// Verifies raw native by-reference staging is released after a successful invoker call. +#[test] +fn execute_program_cleans_native_raw_ref_slots_after_normal_return() { + let program = parse_fragment(br#"$value = "keep"; return native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 1); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("native dispatch should return normally"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(result, expected); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + +/// Verifies raw native by-reference staging is released when the invoker signals fatal. +#[test] +fn execute_program_cleans_native_raw_ref_slots_after_null_invoker_return() { + let program = parse_fragment(br#"$value = "keep"; native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let mut native = NativeFunction::new( + std::ptr::null_mut(), + fake_native_null_descriptor, + 1, + ); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("null invoker result should become a runtime fatal"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + +/// Test native invoker that returns NULL to model a bridge-level fatal result. +unsafe extern "C" fn fake_native_null_descriptor( + _descriptor: *mut std::ffi::c_void, + _args: *mut crate::value::RuntimeCell, +) -> *mut crate::value::RuntimeCell { + std::ptr::null_mut() +} + +/// Verifies direct eval calls can bind registered native parameters by name. +#[test] +fn execute_program_calls_registered_native_function_with_named_args() { + let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies direct eval calls can unpack arrays into registered native parameters. +#[test] +fn execute_program_calls_registered_native_function_with_spread_args() { + let program = + parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies indexed array writes mutate an existing scope array. +#[test] +fn execute_program_writes_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies indexed array append writes use the next visible index. +#[test] +fn execute_program_appends_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies associative append starts at key zero when only string keys exist. +#[test] +fn execute_program_appends_assoc_scope_array_with_string_keys() { + let program = + parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} +/// Verifies associative append uses one plus the largest existing integer key. +#[test] +fn execute_program_appends_assoc_scope_array_after_positive_int_key() { + let program = parse_fragment( + br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies associative append preserves PHP's largest-negative-key behavior. +#[test] +fn execute_program_appends_assoc_scope_array_after_negative_int_key() { + let program = + parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies mutating a borrowed scope array does not make the eval scope own it. +#[test] +fn execute_program_preserves_borrowed_array_ownership() { + let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let array = values.array_new(1).expect("create fake array"); + scope.set("items", array, ScopeCellOwnership::Borrowed); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let entry = scope.entry("items").expect("scope should contain items"); + + assert_eq!(entry.cell(), array); + assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); + assert!(values.releases.is_empty()); +} +/// Verifies replacing an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_replaced_scope_value() { + let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} +/// Verifies unsetting an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_unset_scope_value() { + let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} +/// Verifies break exits a runtime eval loop before later statements run. +#[test] +fn execute_program_break_exits_loop() { + let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a"); +} +/// Verifies continue restarts a runtime eval loop and observes later scope updates. +#[test] +fn execute_program_continue_restarts_loop() { + let program = parse_fragment( + br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "done"); +} diff --git a/crates/elephc-magician/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs new file mode 100644 index 0000000000..839b34b0a6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -0,0 +1,333 @@ +//! Purpose: +//! Interpreter tests for eval-declared static properties and static methods. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover storage persistence, visibility checks, and late static binding. + +use super::super::*; +use super::support::*; + +/// Verifies static properties persist and can be read and written through static methods. +#[test] +fn execute_program_reads_writes_eval_static_members() { + let program = parse_fragment( + br#"class EvalStaticCounter { + public static int $count = 1; + public static function bump($step) { + self::$count += $step; + return self::$count; + } +} +echo EvalStaticCounter::$count; echo ":"; +echo EvalStaticCounter::bump(2); echo ":"; +return EvalStaticCounter::$count;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies `static::` uses the called class while `self::` keeps the declaring class. +#[test] +fn execute_program_late_binds_eval_static_property_access() { + let program = parse_fragment( + br#"class EvalStaticBase { + protected static int $n = 2; + public static function add($x) { + static::$n += $x; + return static::$n; + } + public static function baseRead() { + return self::$n; + } +} +class EvalStaticChild extends EvalStaticBase { + protected static int $n = 10; +} +echo EvalStaticChild::add(4); echo ":"; +echo EvalStaticBase::add(3); echo ":"; +return EvalStaticBase::baseRead();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "14:5:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies `isset()` and `empty()` support dynamic static property-name expressions. +#[test] +fn execute_program_probes_dynamic_static_property_names() { + let program = parse_fragment( + br#"class EvalStaticNameProbe { + public static $nullish = null; + public static $empty = ""; + public static $value = "x"; +} +$class = "EvalStaticNameProbe"; +$valueName = "value"; +$nullName = "nullish"; +$emptyName = "empty"; +$missingName = "missing"; +echo isset($class::${$valueName}) ? "set" : "bad"; echo ":"; +echo isset($class::${$nullName}) ? "bad" : "null"; echo ":"; +echo empty($class::${$emptyName}) ? "empty" : "bad"; echo ":"; +echo empty($class::${$valueName}) ? "bad" : "value"; echo ":"; +echo isset($class::${$missingName}) ? "bad" : "missing"; echo ":"; +echo empty($class::${$missingName}) ? "missing-empty" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set:null:empty:value:missing:missing-empty"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private static property access from global eval scope throws Error. +#[test] +fn execute_program_private_eval_static_property_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalStaticPrivate { + private static int $secret = 4; +} +try { + echo EvalStaticPrivate::$secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access private property EvalStaticPrivate::$secret" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies invalid eval-declared static property access throws PHP-compatible Error values. +#[test] +fn execute_program_invalid_eval_static_property_access_throws_error() { + let program = parse_fragment( + br#"class EvalStaticPropertyErrors { + public int $instance = 1; + public static int $typed; +} +try { + echo EvalStaticPropertyErrors::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo EvalStaticPropertyErrors::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo EvalStaticPropertyErrors::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticPropertyErrors::$missing = 9; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Access to undeclared static property EvalStaticPropertyErrors::$missing|\ +Error:Access to undeclared static property EvalStaticPropertyErrors::$instance|\ +Error:Typed static property EvalStaticPropertyErrors::$typed must not be accessed before initialization|\ +Error:Access to undeclared static property EvalStaticPropertyErrors::$missing" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies generated/AOT static property misses distinguish missing classes from missing properties. +#[test] +fn execute_program_invalid_runtime_static_property_access_throws_error() { + let program = parse_fragment( + br#"try { + echo KnownClass::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo MissingRuntimeStaticClass::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Access to undeclared static property KnownClass::$missing|\ +Error:Class \"MissingRuntimeStaticClass\" not found" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies invalid eval-declared static method calls throw PHP-compatible Error values. +#[test] +fn execute_program_invalid_static_method_calls_throw_error() { + let program = parse_fragment( + br#"class EvalStaticCallRules { + public function read() { return 1; } +} +class EvalStaticMissingRules {} +abstract class EvalStaticAbstractRules { + abstract public static function abs(); +} +try { + EvalStaticCallRules::read(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticMissingRules::missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticAbstractRules::abs(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Non-static method EvalStaticCallRules::read() cannot be called statically|\ +Error:Call to undefined method EvalStaticMissingRules::missing()|\ +Error:Cannot call abstract method EvalStaticAbstractRules::abs()" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval allows object-style calls to accessible static methods. +#[test] +fn execute_program_allows_instance_call_to_eval_static_method() { + let program = parse_fragment( + br#"class EvalStaticInstanceRules { + public static function read() { return 1; } +} +$box = new EvalStaticInstanceRules(); +return $box->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(1)); +} + +/// Verifies missing and inaccessible instance methods dispatch through `__call`. +#[test] +fn execute_program_dispatches_eval_magic_call() { + let program = parse_fragment( + br#"class EvalMagicCallBox { + private function hidden($value) { return "bad"; } + public function __call($method, $args) { + return $method . ":" . $args[0] . ":" . $args["name"]; + } +} +$box = new EvalMagicCallBox(); +echo $box->DoThing("A", name: "B"); echo ":"; +return $box->hidden("C", name: "D");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DoThing:A:B:"); + assert_eq!( + values.get(result), + FakeValue::String("hidden:C:D".to_string()) + ); +} + +/// Verifies missing and inaccessible static methods dispatch through `__callStatic`. +#[test] +fn execute_program_dispatches_eval_magic_call_static() { + let program = parse_fragment( + br#"class EvalMagicStaticBox { + private static function hidden($value) { return "bad"; } + public static function __callStatic($method, $args) { + return $method . ":" . $args[0] . ":" . $args["name"]; + } +} +echo EvalMagicStaticBox::DoStatic("A", name: "B"); echo ":"; +return EvalMagicStaticBox::Hidden("C", name: "D");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DoStatic:A:B:"); + assert_eq!( + values.get(result), + FakeValue::String("Hidden:C:D".to_string()) + ); +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs new file mode 100644 index 0000000000..f479b57b86 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs @@ -0,0 +1,188 @@ +//! Purpose: +//! Array-related fake runtime operations for interpreter tests. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers back RuntimeValueOps array creation, reads, writes, and array tag checks. + +use super::*; + +impl FakeOps { + /// Creates a fake indexed array cell. + pub(super) fn runtime_array_new( + &mut self, + capacity: usize, + ) -> Result { + Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) + } + /// Creates a fake direct-string indexed array cell. + pub(super) fn runtime_string_array_new( + &mut self, + capacity: usize, + ) -> Result { + self.runtime_array_new(capacity) + } + /// Appends one string to a fake direct-string indexed array. + pub(super) fn runtime_string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + let value = self.runtime_string(value)?; + let id = array.as_ptr() as usize; + let Some(FakeValue::Array(elements)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + elements.push(value); + Ok(array) + } + /// Creates a fake associative array cell. + pub(super) fn runtime_assoc_new( + &mut self, + _capacity: usize, + ) -> Result { + Ok(self.alloc(FakeValue::Assoc(Vec::new()))) + } + /// Reads one fake indexed array element. + pub(super) fn runtime_array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + match self.get(array) { + FakeValue::Array(elements) => { + let FakeKey::Int(index) = key else { + return self.null(); + }; + if index < 0 { + return self.null(); + } + elements + .get(index as usize) + .copied() + .map_or_else(|| self.null(), Ok) + } + FakeValue::Assoc(entries) => entries + .iter() + .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => self.null(), + } + } + /// Checks whether a fake array has the requested key without reading its value. + pub(super) fn runtime_array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + let key = self.key(key)?; + let exists = match self.get(array) { + FakeValue::Array(elements) => { + matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) + } + FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.bool_value(exists) + } + /// Returns one fake foreach key by insertion-order position. + pub(super) fn runtime_array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) if position < elements.len() => self.int(position as i64), + FakeValue::Assoc(entries) => { + let Some((key, _)) = entries.get(position) else { + return self.null(); + }; + self.alloc_key(key) + } + FakeValue::Array(_) => self.null(), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Writes one fake indexed or associative array element. + /// + /// String keys promote indexed arrays to associative arrays so fake PHP arrays can + /// model mixed integer/string keys produced by runtime metadata helpers. + pub(super) fn runtime_array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + let call_index = self.array_set_calls; + self.array_set_calls += 1; + if self.fail_array_set_call == Some(call_index) { + return Err(EvalStatus::UnsupportedConstruct); + } + let key = self.key(index)?; + let id = array.as_ptr() as usize; + let Some(slot) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + match slot { + FakeValue::Array(elements) => match key { + FakeKey::Int(index) => { + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + } + elements[index] = value; + } + key => { + let mut entries = std::mem::take(elements) + .into_iter() + .enumerate() + .filter_map(|(index, value)| { + (!value.as_ptr().is_null()) + .then_some((FakeKey::Int(index as i64), value)) + }) + .collect::>(); + entries.push((key, value)); + *slot = FakeValue::Assoc(entries); + } + }, + FakeValue::Assoc(entries) => { + if let Some((_, existing_value)) = + entries.iter_mut().find(|(entry_key, _)| entry_key == &key) + { + *existing_value = value; + } else { + entries.push((key, value)); + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(array) + } + /// Returns the visible element count for fake array values. + pub(super) fn runtime_array_len( + &mut self, + array: RuntimeCellHandle, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) => Ok(elements.len()), + FakeValue::Assoc(entries) => Ok(entries.len()), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns whether a fake runtime cell is an indexed or associative array. + pub(super) fn runtime_is_array_like( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + Ok(matches!( + self.get(value), + FakeValue::Array(_) | FakeValue::Assoc(_) + )) + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs new file mode 100644 index 0000000000..77e03dab4e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Scalar cell construction, type-tag, byte, and truthiness fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers allocate primitive fake values and expose PHP-like truthiness. + +use super::*; + +impl FakeOps { + /// Returns whether a fake runtime cell is null. + pub(super) fn runtime_is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!(self.get(value), FakeValue::Null)) + } + /// Returns the fake runtime tag corresponding to a test value. + pub(super) fn runtime_type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Int(_) => EVAL_TAG_INT, + FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, + FakeValue::Float(_) => EVAL_TAG_FLOAT, + FakeValue::Bool(_) => EVAL_TAG_BOOL, + FakeValue::Array(_) => EVAL_TAG_ARRAY, + FakeValue::Assoc(_) => EVAL_TAG_ASSOC, + FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, + FakeValue::Resource(_) => EVAL_TAG_RESOURCE, + FakeValue::InvokerRefCell(_) => 11, + FakeValue::Null => EVAL_TAG_NULL, + }) + } + /// Creates a fake null cell. + pub(super) fn runtime_null(&mut self) -> Result { + Ok(self.alloc(FakeValue::Null)) + } + /// Creates a fake bool cell. + pub(super) fn runtime_bool_value( + &mut self, + value: bool, + ) -> Result { + Ok(self.alloc(FakeValue::Bool(value))) + } + /// Creates a fake int cell. + pub(super) fn runtime_int(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Int(value))) + } + /// Creates a fake resource cell. + pub(super) fn runtime_resource(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Resource(value))) + } + /// Creates a fake float cell. + pub(super) fn runtime_float(&mut self, value: f64) -> Result { + Ok(self.alloc(FakeValue::Float(value))) + } + /// Creates a fake string cell. + pub(super) fn runtime_string(&mut self, value: &str) -> Result { + Ok(self.alloc(FakeValue::String(value.to_string()))) + } + /// Creates a fake string cell from raw PHP bytes. + pub(super) fn runtime_string_bytes_value( + &mut self, + value: &[u8], + ) -> Result { + match std::str::from_utf8(value) { + Ok(value) => self.string(value), + Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), + } + } + /// Casts one fake runtime cell to bytes for nested eval parsing. + pub(super) fn runtime_string_bytes( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + Ok(self.string_bytes_for_value(&self.get(value))) + } + /// Returns PHP-like truthiness for fake runtime cells. + pub(super) fn runtime_truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Null => false, + FakeValue::Bool(value) => value, + FakeValue::Int(value) => value != 0, + FakeValue::Float(value) => value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + FakeValue::InvokerRefCell(_) => true, + }) + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/conversions.rs b/crates/elephc-magician/src/interpreter/tests/support/conversions.rs new file mode 100644 index 0000000000..426ed91ebe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/conversions.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Conversion, comparison, and stringification helpers for fake interpreter values. +//! RuntimeValueOps methods delegate here to keep scalar PHP-like coercion rules +//! out of the trait implementation file. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - Helpers intentionally cover only semantics asserted by eval interpreter tests. + +use super::*; + +impl FakeOps { + /// Compares fake scalar values with the same loose rules covered by eval tests. + pub(super) fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), + (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Null, FakeValue::String(value)) + | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::Null, FakeValue::Bytes(value)) + | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), + (FakeValue::String(left), FakeValue::String(right)) => { + match (left.parse::(), right.parse::()) { + (Ok(left), Ok(right)) => left == right, + _ => left == right, + } + } + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::String(left), right) => left + .parse::() + .is_ok_and(|left| left == self.fake_numeric(&right)), + (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) + .ok() + .and_then(|left| left.parse::().ok()) + .is_some_and(|left| left == self.fake_numeric(&right)), + (left, FakeValue::String(right)) => right + .parse::() + .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) + .ok() + .and_then(|right| right.parse::().ok()) + .is_some_and(|right| self.fake_numeric(&left) == right), + (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), + } + } + + /// Compares fake scalar values by PHP strict tag and payload equality. + pub(super) fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + if left == right + && matches!( + self.get(left), + FakeValue::Object(_) | FakeValue::Iterator { .. } + ) + { + return true; + } + match (self.get(left), self.get(right)) { + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, + (FakeValue::Int(left), FakeValue::Int(right)) => left == right, + (FakeValue::Float(left), FakeValue::Float(right)) => left == right, + (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, + _ => false, + } + } + + /// Converts one fake scalar cell to a numeric value for comparison tests. + pub(super) fn numeric(&self, handle: RuntimeCellHandle) -> Result { + Ok(self.fake_numeric(&self.get(handle))) + } + + /// Converts a fake value to the numeric scalar used by comparison tests. + pub(super) fn fake_numeric(&self, value: &FakeValue) -> f64 { + match value { + FakeValue::Null => 0.0, + FakeValue::Bool(false) => 0.0, + FakeValue::Bool(true) => 1.0, + FakeValue::Int(value) => *value as f64, + FakeValue::Float(value) => *value, + FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Bytes(value) => std::str::from_utf8(value) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0), + FakeValue::Array(value) => value.len() as f64, + FakeValue::Assoc(value) => value.len() as f64, + FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, + FakeValue::Resource(value) => (*value + 1) as f64, + FakeValue::InvokerRefCell(_) => 0.0, + } + } + + /// Converts a fake value to the integer scalar used by modulo tests. + pub(super) fn fake_int(&self, value: &FakeValue) -> i64 { + self.fake_numeric(value) as i64 + } + + /// Returns fake PHP truthiness for already-loaded test values. + pub(super) fn fake_truthy(&self, value: &FakeValue) -> bool { + match value { + FakeValue::Null => false, + FakeValue::Bool(value) => *value, + FakeValue::Int(value) => *value != 0, + FakeValue::Float(value) => *value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + FakeValue::InvokerRefCell(_) => true, + } + } + + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. + pub(super) fn stringify(&self, handle: RuntimeCellHandle) -> String { + match self.get(handle) { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value, + FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), + FakeValue::Array(_) => "Array".to_string(), + FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + FakeValue::InvokerRefCell(_) => "[invoker-ref]".to_string(), + } + } + + /// Converts a fake PHP value to string bytes while preserving binary strings. + pub(super) fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { + match value { + FakeValue::String(value) => value.as_bytes().to_vec(), + FakeValue::Bytes(value) => value.clone(), + value => self.stringify_value(value).into_bytes(), + } + } + + /// Converts one loaded fake PHP value to display text for byte coercions. + pub(super) fn stringify_value(&self, value: &FakeValue) -> String { + match value { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value.clone(), + FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), + FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + FakeValue::InvokerRefCell(_) => "[invoker-ref]".to_string(), + } + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/lifecycle_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/lifecycle_ops.rs new file mode 100644 index 0000000000..db3b6fd135 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/lifecycle_ops.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Release, retain, warning, and echo fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers record observable side effects for assertions without touching real runtime memory. + +use super::*; + +impl FakeOps { + /// Records fake releases without freeing handles needed for assertions. + pub(super) fn runtime_release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.releases.push(value); + Ok(()) + } + /// Returns the same fake handle because fake cells do not refcount. + pub(super) fn runtime_retain( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + Ok(value) + } + /// Records fake PHP warnings without writing to stderr. + pub(super) fn runtime_warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.warnings.push(message.to_string()); + Ok(()) + } + /// Appends fake echo output for interpreter tests. + pub(super) fn runtime_echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + let value = self.stringify(value); + self.output.push_str(&value); + Ok(()) + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/mod.rs b/crates/elephc-magician/src/interpreter/tests/support/mod.rs new file mode 100644 index 0000000000..799f2a0eb1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/mod.rs @@ -0,0 +1,134 @@ +//! Purpose: +//! Shared fake runtime support for interpreter unit tests. +//! The fixtures allocate opaque runtime cells and implement `RuntimeValueOps` +//! without linking generated runtime hooks. +//! +//! Called from: +//! - `crate::interpreter::tests::*` focused test modules. +//! +//! Key details: +//! - Fake handles are stable integer-backed pointers used only inside tests. +//! - Output, warnings, and releases are recorded for assertions. + +use std::collections::HashMap; +use std::ffi::c_void; + +use crate::value::RuntimeCell; + +use super::super::*; + +mod array_ops; +mod cell_ops; +mod conversions; +mod lifecycle_ops; +mod numeric_ops; +mod object_ops; +mod runtime_ops; + +/// Test-only array key representation for fake indexed and associative arrays. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(super) enum FakeKey { + Int(i64), + String(String), +} + +/// Test-only runtime value representation used behind opaque cell handles. +#[derive(Clone, Debug, PartialEq)] +pub(super) enum FakeValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + Bytes(Vec), + Array(Vec), + Assoc(Vec<(FakeKey, RuntimeCellHandle)>), + Object(Vec<(String, RuntimeCellHandle)>), + Iterator { len: i64, position: i64 }, + Resource(i64), + InvokerRefCell(usize), +} + +/// Test runtime hooks that allocate stable fake handles and record echo output. +#[derive(Default)] +pub(super) struct FakeOps { + pub(super) next_id: usize, + pub(super) values: HashMap, + pub(super) object_classes: HashMap, + pub(super) output: String, + pub(super) releases: Vec, + pub(super) warnings: Vec, + pub(super) fail_array_set_call: Option, + pub(super) array_set_calls: usize, +} + +impl FakeOps { + /// Allocates one fake runtime cell and returns its opaque handle. + pub(super) fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { + self.next_id += 1; + let id = self.next_id; + self.values.insert(id, value); + RuntimeCellHandle::from_raw(id as *mut RuntimeCell) + } + + /// Reads a fake runtime cell by opaque handle. + pub(super) fn get(&self, handle: RuntimeCellHandle) -> FakeValue { + let id = handle.as_ptr() as usize; + self.values.get(&id).cloned().expect("fake cell missing") + } + + /// Converts a fake runtime cell into a normalized fake PHP array key. + pub(super) fn key(&self, handle: RuntimeCellHandle) -> Result { + let value = self.get(handle); + match value { + FakeValue::Int(value) => Ok(FakeKey::Int(value)), + FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) + .map(FakeKey::Int) + .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) + .map(FakeKey::Int) + .map_or_else( + || { + Ok(FakeKey::String( + String::from_utf8_lossy(&value).into_owned(), + )) + }, + Ok, + ), + FakeValue::Null => Ok(FakeKey::String(String::new())), + value => Ok(FakeKey::Int(self.fake_int(&value))), + } + } + + /// Allocates a fake runtime cell for an existing PHP array key. + pub(super) fn alloc_key(&mut self, key: &FakeKey) -> Result { + match key { + FakeKey::Int(value) => self.int(*value), + FakeKey::String(value) => self.string(value), + } + } + + /// Finds a fake object property by insertion-order name. + pub(super) fn object_property( + properties: &[(String, RuntimeCellHandle)], + name: &str, + ) -> Option { + properties + .iter() + .find_map(|(property, value)| (property == name).then_some(*value)) + } + + /// Configures one fake array-set call to fail for cleanup-path tests. + pub(super) fn fail_array_set_call(&mut self, call_index: usize) { + self.fail_array_set_call = Some(call_index); + self.array_set_calls = 0; + } +} + +/// Test native invoker that returns the descriptor pointer as a runtime cell. +pub(super) unsafe extern "C" fn fake_native_return_descriptor( + descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + descriptor.cast() +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/numeric_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/numeric_ops.rs new file mode 100644 index 0000000000..18649dad7b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/numeric_ops.rs @@ -0,0 +1,296 @@ +//! Purpose: +//! Numeric, bitwise, comparison, concatenation, cast, and string-reversal fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers implement PHP-like scalar coercions used by expression and builtin tests. + +use super::*; + +impl FakeOps { + /// Casts a fake runtime cell to a fake integer cell. + pub(super) fn runtime_cast_int( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_int(&value); + self.int(value) + } + /// Casts a fake runtime cell to a fake float cell. + pub(super) fn runtime_cast_float( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_numeric(&value); + self.float(value) + } + /// Casts a fake runtime cell to a fake string cell. + pub(super) fn runtime_cast_string( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.stringify(value); + self.string(&value) + } + /// Casts a fake runtime cell to a fake boolean cell. + pub(super) fn runtime_cast_bool( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_truthy(&value); + self.bool_value(value) + } + /// Computes fake PHP absolute value while preserving float payloads. + pub(super) fn runtime_abs( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + match self.get(value) { + FakeValue::Float(value) => self.float(value.abs()), + value => self.int(self.fake_int(&value).wrapping_abs()), + } + } + /// Computes fake PHP ceiling through numeric conversion as a float result. + pub(super) fn runtime_ceil( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).ceil()) + } + /// Computes fake PHP floor through numeric conversion as a float result. + pub(super) fn runtime_floor( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).floor()) + } + /// Computes fake PHP square root through numeric conversion as a float result. + pub(super) fn runtime_sqrt( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).sqrt()) + } + /// Reverses a fake string byte-wise for interpreter tests. + pub(super) fn runtime_strrev( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let mut bytes = self.stringify(value).into_bytes(); + bytes.reverse(); + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(&value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + pub(super) fn runtime_fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left / right) + } + /// Computes fake floating-point modulo for interpreter tests. + pub(super) fn runtime_fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left % right) + } + /// Adds fake numeric cells for interpreter tests. + pub(super) fn runtime_add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), + (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), + } + } + /// Subtracts fake numeric cells for interpreter tests. + pub(super) fn runtime_sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), + (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), + } + } + /// Multiplies fake numeric cells for interpreter tests. + pub(super) fn runtime_mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), + (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), + } + } + /// Divides fake numeric cells for interpreter tests. + pub(super) fn runtime_div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_numeric(&self.get(right)); + if right == 0.0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_numeric(&self.get(left)); + self.float(left / right) + } + /// Computes fake integer modulo for interpreter tests. + pub(super) fn runtime_modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_int(&self.get(right)); + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_int(&self.get(left)); + self.int(left % right) + } + /// Raises fake numeric cells for interpreter tests. + pub(super) fn runtime_pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left.powf(right)) + } + /// Rounds fake numeric cells with PHP's optional decimal precision. + pub(super) fn runtime_round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let value = self.fake_numeric(&self.get(value)); + let precision = precision + .map(|precision| self.fake_int(&self.get(precision))) + .unwrap_or(0); + let multiplier = 10_f64.powf(precision as f64); + self.float((value * multiplier).round() / multiplier) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. + pub(super) fn runtime_bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_int(&self.get(left)); + let right = self.fake_int(&self.get(right)); + let value = match op { + EvalBinOp::BitAnd => left & right, + EvalBinOp::BitOr => left | right, + EvalBinOp::BitXor => left ^ right, + EvalBinOp::ShiftLeft => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shl(right as u32) + } + EvalBinOp::ShiftRight => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shr(right as u32) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.int(value) + } + /// Applies fake integer bitwise NOT for interpreter tests. + pub(super) fn runtime_bit_not( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.fake_int(&self.get(value)); + self.int(!value) + } + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + pub(super) fn runtime_concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let mut left = self.string_bytes_for_value(&self.get(left)); + let right = self.string_bytes_for_value(&self.get(right)); + left.extend_from_slice(&right); + self.string_bytes_value(&left) + } + /// Compares fake scalar cells and returns a fake PHP boolean. + pub(super) fn runtime_compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let result = match op { + EvalBinOp::LooseEq => self.loose_eq(left, right), + EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::StrictEq => self.strict_eq(left, right), + EvalBinOp::StrictNotEq => !self.strict_eq(left, right), + EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, + EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, + EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, + EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + self.bool_value(result) + } + /// Compares fake numeric cells and returns a PHP spaceship integer. + pub(super) fn runtime_spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.numeric(left)?; + let right = self.numeric(right)?; + let value = if left < right { + -1 + } else if left > right { + 1 + } else { + 0 + }; + self.int(value) + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs new file mode 100644 index 0000000000..71e3ab5ec7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Object, method, class-metadata, and identity fake runtime operations for interpreter tests. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers model only the object and class behavior needed by eval tests. + +use super::*; + +mod class_runtime; +mod method_dispatch; +mod properties; +mod reflection_construction; +mod reflection_helpers; +mod relations; +mod static_dispatch; + +use relations::*; + +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; +const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; +const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; +const EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED: u64 = 16384; +const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; +const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; +const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; +const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; +const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; +const EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE: u64 = 256; +const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs new file mode 100644 index 0000000000..7f9ee6f645 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs @@ -0,0 +1,324 @@ +//! Purpose: +//! Fake runtime operations for object construction, class-like existence, +//! reflection metadata, inheritance checks, class names, and object identity. +//! +//! Called from: +//! - `FakeOps`'s object/class `RuntimeValueOps` methods. +//! +//! Key details: +//! - The intentionally small fake class graph is shared with relation helpers. + +use super::*; + +impl FakeOps { + + /// Creates one fake object for eval `new` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_new_object( + &mut self, + class_name: &str, + ) -> Result { + let object = self.alloc(FakeValue::Object(Vec::new())); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) + } + /// Applies fake constructor side effects for eval `new` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let class_name = self.object_classes.get(&id).cloned(); + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if class_name + .as_deref() + .is_some_and(fake_runtime_exception_like_class) + { + if let Some(message) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "message") + { + *value = message; + } else { + properties.push(("message".to_string(), message)); + } + } + if let Some(code) = args.get(1).copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "code") { + *value = code; + } else { + properties.push(("code".to_string(), code)); + } + } + return Ok(()); + } + if class_name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("KnownFailingConstructor")) + { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(first) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); + } + } + Ok(()) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass") + || name.eq_ignore_ascii_case("KnownFailingConstructor")) + } + /// Reports fake generated AOT ReflectionClass flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_flags( + &mut self, + class_name: &str, + ) -> Result, EvalStatus> { + match class_name.to_ascii_lowercase().as_str() { + "knownabstract" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_ABSTRACT)), + "knownfinal" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_FINAL)), + "knownreadonly" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_READONLY)), + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "answer" | "add_x" | "add2_x" | "read_x" | "run" => { + Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)) + } + "helper" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_STATIC | EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + )), + "locked" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_FINAL, + )), + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "answer" | "add_x" | "add2_x" | "read_x" | "run" | "helper" | "locked" => { + Ok(Some("KnownClass".to_string())) + } + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod names for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(3)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "run")?; + array = self.runtime_string_array_push(array, "helper")?; + array = self.runtime_string_array_push(array, "locked")?; + } + Ok(array) + } + /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + return Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_PROMOTED, + )); + } + Ok(None) + } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + Ok(Some("KnownClass".to_string())) + } else { + Ok(None) + } + } + /// Reports fake generated AOT ReflectionProperty names for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "promoted")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass interface names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownInterface")?; + } else if class_name.eq_ignore_ascii_case("KnownInterface") { + array = self.runtime_string_array_push(array, "Traversable")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait")?; + } else if class_name.eq_ignore_ascii_case("KnownTrait") { + array = self.runtime_string_array_push(array, "KnownInnerTrait")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "knownAlias")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait::source")?; + } + Ok(array) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_interface_exists(&mut self, name: &str) -> Result { + Ok([ + "KnownInterface", + "ArrayAccess", + "Countable", + "Iterator", + "IteratorAggregate", + "JsonSerializable", + "OuterIterator", + "RecursiveIterator", + "SeekableIterator", + "SplObserver", + "SplSubject", + "Stringable", + "Throwable", + "Traversable", + ] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_trait_exists(&mut self, name: &str) -> Result { + Ok(["KnownTrait", "KnownInnerTrait"] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + let object_id = object_or_class.as_ptr() as usize; + match self.get(object_or_class) { + FakeValue::Object(_) if self.object_classes.contains_key(&object_id) => Ok(self + .object_classes + .get(&object_id) + .is_some_and(|class_name| { + fake_runtime_object_is_a(class_name, target_class, exclude_self) + })), + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + FakeValue::String(name) => { + Ok(fake_runtime_object_is_a(&name, target_class, exclude_self)) + } + _ => Ok(false), + } + } + /// Returns a fake PHP class name for object-tagged test values. + pub(in crate::interpreter::tests::support) fn runtime_object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let object_id = object.as_ptr() as usize; + if let Some(class_name) = self.object_classes.get(&object_id).cloned() { + return self.string(&class_name); + } + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Returns fake parent-class names for eval introspection unit tests. + pub(in crate::interpreter::tests::support) fn runtime_parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") + } + _ => self.string(""), + } + } + /// Returns the fake object handle as a stable object identity. + pub(in crate::interpreter::tests::support) fn runtime_object_identity( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), + } + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs new file mode 100644 index 0000000000..b6b6066de2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs @@ -0,0 +1,448 @@ +//! Purpose: +//! Implements fake runtime instance-method dispatch used by interpreter tests. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps::method_call()` implementation. +//! +//! Key details: +//! - The match models only registered test doubles and their observable effects. + +use super::*; + +impl FakeOps { + + /// Calls one fake object method by name. + pub(in crate::interpreter::tests::support) fn runtime_method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + let method = method.to_ascii_lowercase(); + match (self.get(object), method.as_str()) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "__tostring") if args.is_empty() => { + let class_name = self.object_classes.get(&(object.as_ptr() as usize)).cloned(); + self.reflection_type_to_string(class_name.as_deref(), &properties) + } + (FakeValue::Object(properties), "getname") if args.is_empty() => { + Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(_), "getdoccomment" | "getextensionname") if args.is_empty() => { + self.bool_value(false) + } + (FakeValue::Object(_), "getextension") if args.is_empty() => self.null(), + (FakeValue::Object(properties), "getshortname") if args.is_empty() => { + Self::object_property(&properties, "__short_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getnamespacename") if args.is_empty() => { + Self::object_property(&properties, "__namespace_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "innamespace") if args.is_empty() => { + Self::object_property(&properties, "__in_namespace") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isfinal") if args.is_empty() => { + Self::object_property(&properties, "__is_final") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isabstract") if args.is_empty() => { + Self::object_property(&properties, "__is_abstract") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinterface") if args.is_empty() => { + Self::object_property(&properties, "__is_interface") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "istrait") if args.is_empty() => { + Self::object_property(&properties, "__is_trait") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isenum") if args.is_empty() => { + Self::object_property(&properties, "__is_enum") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isreadonly") if args.is_empty() => { + Self::object_property(&properties, "__is_readonly") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispromoted") if args.is_empty() => { + Self::object_property(&properties, "__is_promoted") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isvirtual") if args.is_empty() => { + Self::object_property(&properties, "__is_virtual") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { + Self::object_property(&properties, "__is_anonymous") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinstantiable") if args.is_empty() => { + Self::object_property(&properties, "__is_instantiable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "iscloneable") if args.is_empty() => { + Self::object_property(&properties, "__is_cloneable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isiterable" | "isiterateable") if args.is_empty() => { + Self::object_property(&properties, "__is_iterable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinternal") if args.is_empty() => { + Self::object_property(&properties, "__is_internal") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isuserdefined") if args.is_empty() => { + Self::object_property(&properties, "__is_user_defined") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdeprecated") if args.is_empty() => { + Self::object_property(&properties, "__is_deprecated") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { + Self::object_property(&properties, "__parent_class") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getconstructor") if args.is_empty() => { + Self::object_property(&properties, "__constructor").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { + Self::object_property(&properties, "__declaring_class") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getdeclaringfunction") if args.is_empty() => { + Self::object_property(&properties, "__declaring_function") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { + Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "isprotectedset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 2048) + } + (FakeValue::Object(properties), "isprivateset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 4096) + } + (FakeValue::Object(properties), "getvalue") if args.is_empty() => { + Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getbackingvalue") if args.is_empty() => { + Self::object_property(&properties, "__backing_value") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isenumcase") if args.is_empty() => { + Self::object_property(&properties, "__is_enum_case") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isstatic") if args.is_empty() => { + Self::object_property(&properties, "__is_static") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispublic") if args.is_empty() => { + Self::object_property(&properties, "__is_public") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprotected") if args.is_empty() => { + Self::object_property(&properties, "__is_protected") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprivate") if args.is_empty() => { + Self::object_property(&properties, "__is_private") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "hasmethod") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__method_names", args[0], true) + } + (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__property_names", args[0], false) + } + (FakeValue::Object(properties), "hasconstant") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__constant_names", args[0], false) + } + (FakeValue::Object(properties), "getconstant") if args.len() == 1 => { + let Some(constants) = Self::object_property(&properties, "__constants") else { + return self.bool_value(false); + }; + let exists = self.runtime_array_key_exists(args[0], constants)?; + if matches!(self.get(exists), FakeValue::Bool(true)) { + self.runtime_array_get(constants, args[0]) + } else { + self.bool_value(false) + } + } + (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { + let direct = self.object_string_array_contains( + &properties, + "__interface_names", + args[0], + true, + )?; + if matches!(self.get(direct), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(is_interface) = Self::object_property(&properties, "__is_interface") + else { + return Ok(direct); + }; + if !matches!(self.get(is_interface), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(reflected_name) = Self::object_property(&properties, "__name") else { + return Ok(direct); + }; + let FakeValue::String(reflected_name) = self.get(reflected_name) else { + return Ok(direct); + }; + let FakeValue::String(interface_name) = self.get(args[0]) else { + return Ok(direct); + }; + self.bool_value(reflected_name.eq_ignore_ascii_case(&interface_name)) + } + (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { + Self::object_property(&properties, "__interface_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getinterfaces") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__interface_names") + } + (FakeValue::Object(properties), "gettraitnames") if args.is_empty() => { + Self::object_property(&properties, "__trait_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "gettraits") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__trait_names") + } + (FakeValue::Object(properties), "getmethods") if args.is_empty() => { + Self::object_property(&properties, "__methods") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getmethod") if args.len() == 1 => { + self.object_named_member(&properties, "__methods", args[0], true) + } + (FakeValue::Object(properties), "getproperties") if args.is_empty() => { + Self::object_property(&properties, "__properties") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getproperty") if args.len() == 1 => { + self.object_named_member(&properties, "__properties", args[0], false) + } + (FakeValue::Object(properties), "getconstants") if args.is_empty() => { + Self::object_property(&properties, "__constants") + .map_or_else(|| self.runtime_assoc_new(0), Ok) + } + (FakeValue::Object(properties), "getreflectionconstants") if args.is_empty() => { + Self::object_property(&properties, "__reflection_constants") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getreflectionconstant") if args.len() == 1 => self + .object_named_member_or_false( + &properties, + "__reflection_constants", + args[0], + false, + ), + (FakeValue::Object(properties), "getarguments") if args.is_empty() => { + Self::object_property(&properties, "__args") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getparameters") if args.is_empty() => { + Self::object_property(&properties, "__parameters") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getnumberofparameters") if args.is_empty() => { + match Self::object_property(&properties, "__parameters") { + Some(parameters) => { + let len = self.array_len(parameters)?; + self.int(len as i64) + } + None => self.int(0), + } + } + (FakeValue::Object(properties), "getnumberofrequiredparameters") if args.is_empty() => { + Self::object_property(&properties, "__required_parameter_count") + .map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "getposition") if args.is_empty() => { + Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "gettype") if args.is_empty() => { + Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getsettabletype") if args.is_empty() => { + Self::object_property(&properties, "__settable_type") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getclass") if args.is_empty() => { + Self::object_property(&properties, "__class").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isdynamic") if args.is_empty() => { + Self::object_property(&properties, "__is_dynamic") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "gettypes") if args.is_empty() => { + Self::object_property(&properties, "__types") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getdefaultvalue") if args.is_empty() => { + Self::object_property(&properties, "__default_value") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getdefaultvalueconstantname") if args.is_empty() => { + Self::object_property(&properties, "__default_value_constant_name") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isconstructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__construct")) + } + (FakeValue::Object(properties), "isdestructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__destruct")) + } + (FakeValue::Object(properties), "isoptional") if args.is_empty() => { + Self::object_property(&properties, "__is_optional") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isvariadic") if args.is_empty() => { + Self::object_property(&properties, "__is_variadic") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispassedbyreference") if args.is_empty() => { + Self::object_property(&properties, "__is_passed_by_reference") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "canbepassedbyvalue") if args.is_empty() => { + let Some(by_ref) = Self::object_property(&properties, "__is_passed_by_reference") + else { + return self.bool_value(true); + }; + self.bool_value(!matches!(self.get(by_ref), FakeValue::Bool(true))) + } + (FakeValue::Object(properties), "hastype") if args.is_empty() => { + if let Some(has_type) = Self::object_property(&properties, "__has_type") { + return Ok(has_type); + } + match Self::object_property(&properties, "__type") { + Some(value) => self.bool_value(!matches!(self.get(value), FakeValue::Null)), + None => self.bool_value(false), + } + } + (FakeValue::Object(properties), "isdefaultvalueavailable" | "hasdefaultvalue") + if args.is_empty() => + { + Self::object_property(&properties, "__has_default_value") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdefaultvalueconstant") if args.is_empty() => { + Self::object_property(&properties, "__is_default_value_constant") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdefault") if args.is_empty() => { + match Self::object_property(&properties, "__is_dynamic") { + Some(is_dynamic) => { + self.bool_value(!matches!(self.get(is_dynamic), FakeValue::Bool(true))) + } + None => self.bool_value(true), + } + } + (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { + Self::object_property(&properties, "__allows_null") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isarray") if args.is_empty() => { + Self::object_property(&properties, "__is_array_type") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "iscallable") if args.is_empty() => { + Self::object_property(&properties, "__is_callable_type") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isbuiltin") if args.is_empty() => { + Self::object_property(&properties, "__is_builtin") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), + (FakeValue::Object(properties), "getattributes") if args.is_empty() => { + Self::object_property(&properties, "__attrs") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getmessage") if args.is_empty() => { + Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getcode") if args.is_empty() => { + Self::object_property(&properties, "code").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs new file mode 100644 index 0000000000..af12031ef2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs @@ -0,0 +1,105 @@ +//! Purpose: +//! Fake runtime operations for object property reads, writes, initialization, +//! shallow cloning, and property iteration. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps` implementation in interpreter tests. +//! +//! Key details: +//! - Operations mutate only the in-memory fake value store. + +use super::*; + +impl FakeOps { + /// Reads one fake object property by name. + pub(in crate::interpreter::tests::support) fn runtime_property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => properties + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns whether one fake object property exists by name. + pub(in crate::interpreter::tests::support) fn runtime_property_is_initialized( + &self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + Ok(properties.iter().any(|(name, _)| name == property)) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Writes one fake object property by name. + pub(in crate::interpreter::tests::support) fn runtime_property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); + } + Ok(()) + } + /// Creates one shallow fake object clone, preserving stored property handles. + pub(in crate::interpreter::tests::support) fn runtime_object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let id = object.as_ptr() as usize; + let properties = match self.get(object) { + FakeValue::Object(properties) => properties.clone(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let clone = self.alloc(FakeValue::Object(properties)); + if let Some(class_name) = self.object_classes.get(&id).cloned() { + self.object_classes + .insert(clone.as_ptr() as usize, class_name); + } + Ok(clone) + } + /// Returns the number of fake object properties in insertion order. + pub(in crate::interpreter::tests::support) fn runtime_object_property_len( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns one fake object property key by insertion-order position. + pub(in crate::interpreter::tests::support) fn runtime_object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs new file mode 100644 index 0000000000..6ad4de08b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs @@ -0,0 +1,300 @@ +//! Purpose: +//! Builds fake ReflectionAttribute and reflection owner/member objects for +//! interpreter tests. +//! +//! Called from: +//! - Reflection-related `RuntimeValueOps` hooks on `FakeOps`. +//! +//! Key details: +//! - Object layouts mirror the metadata consumed by eval Reflection builtins. + +use super::*; + +impl FakeOps { + + /// Materializes one fake populated `ReflectionAttribute` object. + pub(in crate::interpreter::tests::support) fn runtime_reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + let name = self.string(name)?; + let factory = self.int(0)?; + let target = self.int(target as i64)?; + let repeated = self.bool_value(repeated)?; + let object = self.alloc(FakeValue::Object(vec![ + ("__name".to_string(), name), + ("__args".to_string(), args), + ("__factory".to_string(), factory), + ("__target".to_string(), target), + ("__is_repeated".to_string(), repeated), + ])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); + Ok(object) + } + /// Materializes one fake populated Reflection owner object. + pub(in crate::interpreter::tests::support) fn runtime_reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + let class_name = match owner_kind { + EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_OBJECT => "ReflectionObject", + EVAL_REFLECTION_OWNER_ENUM => "ReflectionEnum", + EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", + EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", + EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", + EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", + EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", + EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", + EVAL_REFLECTION_OWNER_UNION_TYPE => "ReflectionUnionType", + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE => "ReflectionIntersectionType", + _ => return Err(EvalStatus::RuntimeFatal), + }; + let name = self.string(reflected_name)?; + let is_final = self.bool_value((flags & 1) != 0)?; + let is_abstract = self.bool_value((flags & 2) != 0)?; + let is_interface = self.bool_value((flags & 4) != 0)?; + let is_trait = self.bool_value((flags & 8) != 0)?; + let is_enum = self.bool_value((flags & 16) != 0)?; + let is_readonly = self.bool_value((flags & 32) != 0)?; + let is_instantiable = self.bool_value((flags & 64) != 0)?; + let is_cloneable = self.bool_value((flags & 128) != 0)?; + let is_internal = self.bool_value((flags & 256) != 0)?; + let is_user_defined = self.bool_value((flags & 512) != 0)?; + let is_iterable = self.bool_value((flags & 1024) != 0)?; + let is_anonymous = self.bool_value((flags & 2048) != 0)?; + let modifiers_cell = self.int(modifiers as i64)?; + let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS + | EVAL_REFLECTION_OWNER_OBJECT + | EVAL_REFLECTION_OWNER_ENUM + ) { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + let has_namespace = !namespace_name.is_empty(); + let namespace_name = self.string(namespace_name)?; + let short_name = self.string(short_name)?; + let in_namespace = self.bool_value(has_namespace)?; + let constant_names = self.runtime_array_new(0)?; + let constants = self.runtime_assoc_new(0)?; + let reflection_constants = self.runtime_array_new(0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__is_interface".to_string(), is_interface)); + properties.push(("__is_trait".to_string(), is_trait)); + properties.push(("__is_enum".to_string(), is_enum)); + properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__is_anonymous".to_string(), is_anonymous)); + properties.push(("__is_instantiable".to_string(), is_instantiable)); + properties.push(("__is_cloneable".to_string(), is_cloneable)); + properties.push(("__is_iterable".to_string(), is_iterable)); + properties.push(("__is_internal".to_string(), is_internal)); + properties.push(("__is_user_defined".to_string(), is_user_defined)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__short_name".to_string(), short_name)); + properties.push(("__namespace_name".to_string(), namespace_name)); + properties.push(("__in_namespace".to_string(), in_namespace)); + properties.push(("__interface_names".to_string(), interface_names)); + properties.push(("__trait_names".to_string(), trait_names)); + properties.push(("__method_names".to_string(), method_names)); + properties.push(("__property_names".to_string(), property_names)); + properties.push(("__constant_names".to_string(), constant_names)); + properties.push(("__constants".to_string(), constants)); + properties.push(("__reflection_constants".to_string(), reflection_constants)); + properties.push(("__methods".to_string(), method_objects)); + properties.push(("__constructor".to_string(), constructor)); + properties.push(("__parent_class".to_string(), parent_class)); + properties.push(("__properties".to_string(), property_objects)); + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + || owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + { + let is_static = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0)?; + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + properties.push(("__is_static".to_string(), is_static)); + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + let is_readonly = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; + let is_virtual = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL) != 0)?; + let is_dynamic = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC) != 0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__type".to_string(), method_objects)); + properties.push(("__settable_type".to_string(), constant_value)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(("__is_promoted".to_string(), is_promoted)); + properties.push(("__is_virtual".to_string(), is_virtual)); + properties.push(("__is_dynamic".to_string(), is_dynamic)); + properties.push(("__default_value".to_string(), property_objects)); + } + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + | EVAL_REFLECTION_OWNER_PARAMETER + ) { + properties.push(("__declaring_class".to_string(), parent_class)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { + let is_deprecated = + self.bool_value((flags & EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED) != 0)?; + properties.push(("__parameters".to_string(), method_objects)); + properties.push(("__required_parameter_count".to_string(), modifiers_cell)); + properties.push(("__is_deprecated".to_string(), is_deprecated)); + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + let method_modifiers = self.int(method_modifiers as i64)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__modifiers".to_string(), method_modifiers)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_enum_case = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE) != 0)?; + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_enum_case".to_string(), is_enum_case)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + } + if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { + properties.push(("__value".to_string(), constant_value)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + properties.push(("__value".to_string(), constant_value)); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + properties.push(("__backing_value".to_string(), backing_value)); + } + if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { + let position = self.int(modifiers as i64)?; + let is_optional = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL) != 0)?; + let is_variadic = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC) != 0)?; + let is_passed_by_reference = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; + let has_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; + let allows_null = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; + let is_default_value_constant = self + .bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT) != 0)?; + let is_array_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE) != 0)?; + let is_callable_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE) != 0)?; + let class_value = self.reflection_parameter_class_value(method_objects)?; + properties.push(("__position".to_string(), position)); + properties.push(("__is_optional".to_string(), is_optional)); + properties.push(("__is_variadic".to_string(), is_variadic)); + properties.push(( + "__is_passed_by_reference".to_string(), + is_passed_by_reference, + )); + properties.push(("__is_promoted".to_string(), is_promoted)); + properties.push(("__has_type".to_string(), has_type)); + properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_array_type".to_string(), is_array_type)); + properties.push(("__is_callable_type".to_string(), is_callable_type)); + properties.push(("__type".to_string(), method_objects)); + properties.push(("__class".to_string(), class_value)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(( + "__is_default_value_constant".to_string(), + is_default_value_constant, + )); + properties.push(("__default_value_constant_name".to_string(), constant_value)); + properties.push(("__default_value".to_string(), property_objects)); + properties.push(("__declaring_function".to_string(), interface_names)); + } + if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + let is_builtin = self.bool_value((flags & 2) != 0)?; + properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_builtin".to_string(), is_builtin)); + } + if owner_kind == EVAL_REFLECTION_OWNER_UNION_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } + if owner_kind == EVAL_REFLECTION_OWNER_INTERSECTION_TYPE { + let allows_null = self.bool_value(false)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } + let object = self.alloc(FakeValue::Object(properties)); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs new file mode 100644 index 0000000000..2f91fe1b57 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs @@ -0,0 +1,252 @@ +//! Purpose: +//! Shared fake reflection value, modifier, type-formatting, and named-member +//! helpers. +//! +//! Called from: +//! - Fake reflection owner construction and metadata lookup modules. +//! +//! Key details: +//! - Missing members retain the false-versus-fatal behavior expected by PHP APIs. + +use super::*; + +impl FakeOps { + + /// Builds the fake `ReflectionParameter::getClass()` value from a named non-builtin type. + pub(super) fn reflection_parameter_class_value( + &mut self, + type_value: RuntimeCellHandle, + ) -> Result { + if !self + .object_classes + .get(&(type_value.as_ptr() as usize)) + .is_some_and(|class_name| class_name == "ReflectionNamedType") + { + return self.null(); + } + let FakeValue::Object(type_properties) = self.get(type_value) else { + return self.null(); + }; + let is_builtin = Self::object_property(&type_properties, "__is_builtin") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if is_builtin { + return self.null(); + } + let Some(name) = Self::object_property(&type_properties, "__name") else { + return self.null(); + }; + let FakeValue::String(name) = self.get(name) else { + return self.null(); + }; + self.fake_reflection_class_object(&name) + } + + /// Checks whether a private fake object array property contains one string. + pub(super) fn object_string_array_contains( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return self.bool_value(false); + }; + let contains = match self.get(array) { + FakeValue::Array(elements) => elements.iter().any(|element| match self.get(*element) { + FakeValue::String(value) if case_insensitive => { + value.to_ascii_lowercase() == needle + } + FakeValue::String(value) => value == needle, + _ => false, + }), + _ => false, + }; + self.bool_value(contains) + } + + /// Returns whether a fake Reflection owner stores one modifier bit. + pub(super) fn reflection_modifier_mask( + &mut self, + properties: &[(String, RuntimeCellHandle)], + mask: i64, + ) -> Result { + let modifiers = Self::object_property(properties, "__modifiers") + .map(|handle| self.fake_int(&self.get(handle))) + .unwrap_or(0); + self.bool_value((modifiers & mask) != 0) + } + + /// Builds a name-keyed fake ReflectionClass map from a private string-array property. + pub(super) fn object_relation_reflection_classes( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + ) -> Result { + let result = self.runtime_assoc_new(0)?; + let Some(array) = Self::object_property(properties, property) else { + return Ok(result); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::String(name) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let key = self.runtime_string(&name)?; + let object = self.fake_reflection_class_object(&name)?; + self.runtime_array_set(result, key, object)?; + } + Ok(result) + } + + /// Builds a minimal fake ReflectionClass object with a working `getName()` slot. + pub(super) fn fake_reflection_class_object( + &mut self, + class_name: &str, + ) -> Result { + let name = self.string(class_name)?; + let object = self.alloc(FakeValue::Object(vec![("__name".to_string(), name)])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionClass".to_string()); + Ok(object) + } + + /// Formats fake ReflectionType objects through their synthetic `__toString()` method. + pub(super) fn reflection_type_to_string( + &mut self, + class_name: Option<&str>, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + match class_name { + Some("ReflectionNamedType") => self.reflection_named_type_to_string(properties), + Some("ReflectionUnionType") => self.reflection_composite_type_to_string( + properties, + "|", + true, + ), + Some("ReflectionIntersectionType") => self.reflection_composite_type_to_string( + properties, + "&", + false, + ), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Formats one fake ReflectionNamedType object using retained name/nullability slots. + pub(super) fn reflection_named_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + let Some(name) = Self::object_property(properties, "__name") else { + return self.string(""); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if allows_null && name != "mixed" { + self.string(&format!("?{name}")) + } else { + self.string(&name) + } + } + + /// Formats one fake ReflectionUnionType or ReflectionIntersectionType object. + pub(super) fn reflection_composite_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + separator: &str, + append_null: bool, + ) -> Result { + let mut names = Vec::new(); + if let Some(types) = Self::object_property(properties, "__types") { + let FakeValue::Array(elements) = self.get(types) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(type_properties) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let Some(name) = Self::object_property(&type_properties, "__name") else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + names.push(name); + } + } + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if append_null && allows_null { + names.push("null".to_string()); + } + self.string(&names.join(separator)) + } + + /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. + pub(super) fn object_named_member( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return Err(EvalStatus::RuntimeFatal); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(member_properties) = self.get(element) else { + continue; + }; + let Some(name) = Self::object_property(&member_properties, "__name") else { + continue; + }; + let FakeValue::String(mut name) = self.get(name) else { + continue; + }; + if case_insensitive { + name = name.to_ascii_lowercase(); + } + if name == needle { + return Ok(element); + } + } + Err(EvalStatus::RuntimeFatal) + } + + /// Finds one fake reflection member by name, returning PHP `false` when absent. + pub(super) fn object_named_member_or_false( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + match self.object_named_member(properties, property, needle, case_insensitive) { + Ok(member) => Ok(member), + Err(EvalStatus::RuntimeFatal) => self.bool_value(false), + Err(status) => Err(status), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs new file mode 100644 index 0000000000..68f69d476c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Models the small fake class-name, namespace, Throwable, and inheritance graph +//! used by interpreter tests. +//! +//! Called from: +//! - Fake object construction, reflection, and `is_a` runtime operations. +//! +//! Key details: +//! - Matching is case-insensitive to mirror PHP class-like names. + +/// Returns whether a fake runtime class stores PHP Throwable constructor state. +pub(super) fn fake_runtime_exception_like_class(class_name: &str) -> bool { + [ + "Exception", + "JsonException", + "ReflectionException", + "Error", + "ValueError", + "TypeError", + ] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)) +} + +/// Splits one PHP class-like name into namespace and short-name parts. +pub(super) fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { + match reflected_name.rfind('\\') { + Some(separator) => ( + &reflected_name[..separator], + &reflected_name[separator + 1..], + ), + None => ("", reflected_name), + } +} + +/// Checks the small fake Throwable inheritance graph used by eval interpreter tests. +pub(super) fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: bool) -> bool { + if class_name.eq_ignore_ascii_case(target_class) { + return !exclude_self; + } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("ParentClass") + { + return true; + } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("KnownInterface") + { + return true; + } + if class_name.eq_ignore_ascii_case("ReflectionObject") + && target_class.eq_ignore_ascii_case("ReflectionClass") + { + return true; + } + if target_class.eq_ignore_ascii_case("Throwable") { + return fake_runtime_exception_like_class(class_name); + } + if target_class.eq_ignore_ascii_case("Exception") { + return ["Exception", "JsonException", "ReflectionException"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + if target_class.eq_ignore_ascii_case("Error") { + return ["Error", "ValueError", "TypeError"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + false +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs new file mode 100644 index 0000000000..af0383dc2a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Implements fake runtime static-method dispatch for AOT bridge tests. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps::static_method_call()` implementation. +//! +//! Key details: +//! - Unsupported class/method pairs fail with the existing eval status. + +use super::*; + +impl FakeOps { + + /// Calls one fake public static AOT method by class and method name. + pub(in crate::interpreter::tests::support) fn runtime_static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let method = method.to_ascii_lowercase(); + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Err(EvalStatus::UnsupportedConstruct); + } + match method.as_str() { + "join" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.string(&format!("{}{}", left, right)) + } + "sum" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs new file mode 100644 index 0000000000..90a27e4aca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! RuntimeValueOps implementation for interpreter test fake values. +//! This keeps the large trait surface separate from test fixture type +//! declarations and assertion-only conversion helpers. +//! +//! Called from: +//! - `crate::interpreter::tests::support::FakeOps` through trait dispatch. +//! +//! Key details: +//! - Methods intentionally model only the runtime behavior covered by eval tests. +//! - Handles are fake stable cells and must not be freed by this implementation. + +use super::*; + +mod collection_calls; +mod construction_raw; +mod lifecycle_scalars; +mod numeric_string; +mod reflection; + +use collection_calls::impl_fake_collection_call_ops; +use construction_raw::impl_fake_construction_raw_ops; +use lifecycle_scalars::impl_fake_lifecycle_scalar_ops; +use numeric_string::impl_fake_numeric_string_ops; +use reflection::impl_fake_reflection_ops; + +impl RuntimeValueOps for FakeOps { + impl_fake_collection_call_ops!(); + impl_fake_reflection_ops!(); + impl_fake_construction_raw_ops!(); + impl_fake_lifecycle_scalar_ops!(); + impl_fake_numeric_string_ops!(); +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs new file mode 100644 index 0000000000..c77d751a0c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Defines fake array, property, object-call, and static-call trait methods for +//! interpreter tests. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Methods delegate to focused fake runtime helpers without altering handles. + +macro_rules! impl_fake_collection_call_ops { + () => { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + self.runtime_array_new(capacity) + } + /// Creates a fake direct-string indexed array cell. + fn string_array_new(&mut self, capacity: usize) -> Result { + self.runtime_string_array_new(capacity) + } + /// Appends one string to a fake direct-string indexed array cell. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + self.runtime_string_array_push(array, value) + } + /// Creates a fake associative array cell. + fn assoc_new(&mut self, _capacity: usize) -> Result { + self.runtime_assoc_new(_capacity) + } + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + self.runtime_array_get(array, index) + } + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + self.runtime_array_key_exists(key, array) + } + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_array_iter_key(array, position) + } + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + self.runtime_array_set(array, index, value) + } + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_get(object, property) + } + /// Checks whether one fake object property exists by name. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_is_initialized(object, property) + } + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + self.runtime_property_set(object, property, value) + } + /// Reports no fake AOT static property match. + fn static_property_get( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake AOT static property initialization match. + fn static_property_is_initialized( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result { + Ok(false) + } + /// Reports a failed fake AOT static property write. + fn static_property_set( + &mut self, + _class_name: &str, + _property: &str, + _value: RuntimeCellHandle, + ) -> Result { + Ok(false) + } + /// Reports no fake AOT class constant match. + fn class_constant_get( + &mut self, + _class_name: &str, + _constant: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Creates one shallow fake object clone. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_clone_shallow(object) + } + /// Returns the number of fake object properties in insertion order. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_property_len(object) + } + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_object_property_iter_key(object, position) + } + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + self.runtime_method_call(object, method, args) + } + /// Calls one fake static runtime method by class and method name. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + self.runtime_static_method_call(class_name, method, args) + } + + }; +} + +pub(super) use impl_fake_collection_call_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs new file mode 100644 index 0000000000..c0ed994d6f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Defines fake construction, class queries, raw words, and raw string/heap +//! ownership trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Opaque fake handles preserve the staging and release behavior under test. + +macro_rules! impl_fake_construction_raw_ops { + () => { + + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + self.runtime_new_object(_class_name) + } + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + self.runtime_construct_object(object, args) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + self.runtime_class_exists(name) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + self.runtime_interface_exists(name) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + self.runtime_trait_exists(name) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + self.runtime_enum_exists(name) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + self.runtime_object_is_a(object_or_class, target_class, exclude_self) + } + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_class_name(object) + } + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + self.runtime_parent_class_name(object_or_class) + } + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + self.runtime_array_len(array) + } + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_array_like(value) + } + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_null(value) + } + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_type_tag(value) + } + /// Creates a fake invoker-only by-reference marker. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } + /// Creates a fake invoker-only raw by-reference marker. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + _source_tag: u64, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } + /// Extracts one fake low payload word for raw by-reference staging. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Bool(value) => u64::from(value), + FakeValue::Float(value) => value.to_bits(), + FakeValue::Int(value) => value as u64, + FakeValue::String(_) | FakeValue::Bytes(_) => value.as_ptr() as u64, + FakeValue::Array(_) + | FakeValue::Assoc(_) + | FakeValue::Object(_) + | FakeValue::Iterator { .. } => value.as_ptr() as u64, + _ => 0, + }) + } + /// Extracts one fake high payload word for raw by-reference staging. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::String(value) => value.len() as u64, + FakeValue::Bytes(value) => value.len() as u64, + _ => 0, + }) + } + /// Retains a fake raw string payload for native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + self.runtime_retain(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell))?; + Ok((ptr, len)) + } + /// Converts a fake raw string payload back to its stable fake handle. + fn raw_string_value(&mut self, ptr: u64, _len: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } + /// Records release of a fake raw string payload owned by a staged slot. + fn release_raw_string_words(&mut self, ptr: u64, _len: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } + /// Retains a fake raw heap word for native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; + Ok(word) + } + /// Boxes one fake one-word raw payload with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + match source_tag { + EVAL_TAG_INT => self.runtime_int(word as i64), + EVAL_TAG_FLOAT => self.runtime_float(f64::from_bits(word)), + EVAL_TAG_BOOL => self.runtime_bool_value(word != 0), + EVAL_TAG_RESOURCE => self.runtime_resource(word as i64), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Converts a fake raw heap word back to its stable fake handle. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + /// Records release of a fake raw heap word owned by a staged slot. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + + }; +} + +pub(super) use impl_fake_construction_raw_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs new file mode 100644 index 0000000000..b1068336fc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Defines fake identity, retain/release, warning, scalar construction, and cast +//! trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Operations retain the fake runtime's stable-cell ownership model. + +macro_rules! impl_fake_lifecycle_scalar_ops { + () => { + + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_identity(object) + } + /// Returns fake object identity for releases that target object cells. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + if self.runtime_type_tag(value)? == EVAL_TAG_OBJECT { + self.runtime_object_identity(value).map(Some) + } else { + Ok(None) + } + } + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_release(value) + } + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_retain(value) + } + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.runtime_warning(message) + } + /// Creates a fake null cell. + fn null(&mut self) -> Result { + self.runtime_null() + } + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + self.runtime_bool_value(value) + } + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + self.runtime_int(value) + } + /// Creates a fake resource cell. + fn resource(&mut self, value: i64) -> Result { + self.runtime_resource(value) + } + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + self.runtime_float(value) + } + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + self.runtime_string(value) + } + /// Creates a fake string cell from raw PHP bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + self.runtime_string_bytes_value(value) + } + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_int(value) + } + /// Casts a fake runtime cell to a fake float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_float(value) + } + /// Casts a fake runtime cell to a fake string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_string(value) + } + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_bool(value) + } + + }; +} + +pub(super) use impl_fake_lifecycle_scalar_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs new file mode 100644 index 0000000000..8e3f80382c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs @@ -0,0 +1,160 @@ +//! Purpose: +//! Defines fake numeric, bitwise, comparison, string, echo, byte, and truthiness +//! trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - PHP-like fake behavior remains in the existing `runtime_*` helpers. + +macro_rules! impl_fake_numeric_string_ops { + () => { + + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_abs(value) + } + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_ceil(value) + } + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_floor(value) + } + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_sqrt(value) + } + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_strrev(value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fdiv(left, right) + } + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fmod(left, right) + } + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_add(left, right) + } + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_sub(left, right) + } + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_mul(left, right) + } + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_div(left, right) + } + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_modulo(left, right) + } + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_pow(left, right) + } + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + self.runtime_round(value, precision) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_bitwise(op, left, right) + } + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_bit_not(value) + } + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_concat(left, right) + } + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_compare(op, left, right) + } + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_spaceship(left, right) + } + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_echo(value) + } + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + self.runtime_string_bytes(value) + } + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_truthy(value) + } + + }; +} + +pub(super) use impl_fake_numeric_string_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs new file mode 100644 index 0000000000..a1fb93b55c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs @@ -0,0 +1,176 @@ +//! Purpose: +//! Defines fake Reflection object and metadata-query trait methods for +//! interpreter tests. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - All calls delegate to the fake reflection object model. + +macro_rules! impl_fake_reflection_ops { + () => { + + /// Materializes one fake `ReflectionAttribute` object for eval metadata tests. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + self.runtime_reflection_attribute_new(name, args, target, repeated) + } + /// Materializes one fake Reflection owner object for eval metadata tests. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + self.runtime_reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + parent_class, + flags, + modifiers, + method_modifiers, + constant_value, + backing_value, + constructor, + ) + } + /// Reports fake generated AOT ReflectionMethod flags for metadata bridge tests. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_flags(class_name, method_name) + } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata bridge tests. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_declaring_class(class_name, method_name) + } + /// Reports fake generated AOT ReflectionMethod names for metadata bridge tests. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_method_names(class_name) + } + /// Reports fake generated AOT ReflectionClass flags for metadata bridge tests. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + self.runtime_reflection_class_flags(class_name) + } + /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_flags(class_name, property_name) + } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata bridge tests. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_declaring_class(class_name, property_name) + } + /// Reports fake generated AOT ReflectionProperty names for metadata bridge tests. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_property_names(class_name) + } + /// Reports no fake generated AOT ReflectionClassConstant value. + fn reflection_constant_value( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant flags. + fn reflection_constant_flags( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant declaring class. + fn reflection_constant_declaring_class( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports an empty fake generated AOT ReflectionClassConstant name list. + fn reflection_constant_names( + &mut self, + _class_name: &str, + ) -> Result { + self.runtime_string_array_new(0) + } + /// Reports fake generated/AOT ReflectionClass interface names for metadata bridge tests. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_interface_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait names for metadata bridge tests. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata bridge tests. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata bridge tests. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_sources(class_name) + } + + }; +} + +pub(super) use impl_fake_reflection_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs new file mode 100644 index 0000000000..6a5058e31e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -0,0 +1,476 @@ +//! Purpose: +//! Interpreter tests for eval-declared trait adaptations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover conflict resolution, aliases, and visibility changes +//! applied while expanding traits into eval class metadata. + +use super::super::*; +use super::support::*; + +/// Verifies `insteadof` keeps the selected method while `as` can alias the other method. +#[test] +fn execute_program_applies_eval_trait_insteadof_and_alias() { + let program = parse_fragment( + br#"trait EvalAdaptA { + public function talk() { return "A"; } + public function hidden() { return "secret"; } +} +trait EvalAdaptB { + public function talk() { return "B"; } +} +class EvalAdaptBox { + use EvalAdaptA, EvalAdaptB { + EvalAdaptA::talk insteadof EvalAdaptB; + EvalAdaptB::talk as talkB; + EvalAdaptA::hidden as private; + } + public function read() { + return $this->talk() . ":" . $this->talkB() . ":" . $this->hidden(); + } +} +$box = new EvalAdaptBox(); +echo $box->read(); echo ":"; +return $box->talk();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:B:secret:"); + assert_eq!(values.get(result), FakeValue::String("A".to_string())); +} + +/// Verifies visibility-only `as private` throws when called from global scope. +#[test] +fn execute_program_applies_eval_trait_visibility_adaptation() { + let program = parse_fragment( + br#"trait EvalAdaptHidden { + public function hidden() { return "secret"; } +} +class EvalAdaptHiddenBox { + use EvalAdaptHidden { + EvalAdaptHidden::hidden as private; + } +} +$box = new EvalAdaptHiddenBox(); +return $box->hidden();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private adapted trait method should fail from global scope"); + + assert_eq!(err, EvalStatus::UncaughtThrowable); +} + +/// Verifies trait aliases that collide with class methods or no-op names follow PHP rules. +#[test] +fn execute_program_applies_eval_trait_alias_collision_rules() { + let program = parse_fragment( + br#"trait EvalAliasSource { + public function source() { return "T"; } +} +class EvalAliasClassCollisionBox { + use EvalAliasSource { + source as target; + } + public function target() { return "C"; } + public function read() { return $this->source() . $this->target(); } +} +class EvalAliasNoopBox { + use EvalAliasSource { + source as source; + } +} +$box = new EvalAliasClassCollisionBox(); +echo $box->read(); echo ":"; +return (new EvalAliasNoopBox())->source();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TC:"); + assert_eq!(values.get(result), FakeValue::String("T".to_string())); +} + +/// Verifies traits can compose other eval traits before classes import them. +#[test] +fn execute_program_expands_eval_trait_used_by_trait() { + let program = parse_fragment( + br#"trait EvalNestedInner { + public const WORD = "in"; + public function word() { return self::WORD; } +} +trait EvalNestedOuter { + use EvalNestedInner { + word as private hiddenWord; + } + public function read() { return $this->word() . $this->hiddenWord(); } +} +class EvalNestedBox { + use EvalNestedOuter; +} +$box = new EvalNestedBox(); +echo $box->read(); echo ":"; +$ref = new ReflectionClass("EvalNestedOuter"); +$traits = $ref->getTraitNames(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenWord"]; echo ":"; +$uses = class_uses($box); +echo count($uses); echo ":"; echo $uses["EvalNestedOuter"]; echo ":"; +return $box->word();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "inin:1:EvalNestedInner:EvalNestedInner::word:1:EvalNestedOuter:" + ); + assert_eq!(values.get(result), FakeValue::String("in".to_string())); +} + +/// Verifies nested trait aliases preserve PHP magic constants from the declaring trait. +#[test] +fn execute_program_resolves_eval_trait_method_magic_constants() { + let program = parse_fragment( + br#"namespace EvalMagicTraitNs; +trait EvalMagicInner { + public function report() { + return __NAMESPACE__ . "|" . __CLASS__ . "|" . __TRAIT__ . "|" . __METHOD__ . "|" . __FUNCTION__; + } + public static function stat() { + return __NAMESPACE__ . "|" . __CLASS__ . "|" . __TRAIT__ . "|" . __METHOD__ . "|" . __FUNCTION__; + } +} +trait EvalMagicOuter { + use EvalMagicInner { + report as aliasReport; + stat as aliasStat; + } +} +class EvalMagicBox { + use EvalMagicOuter; +} +echo (new EvalMagicBox())->aliasReport(); echo ":"; +return EvalMagicBox::aliasStat();"#, + ) + .expect("parse eval trait magic fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let expected_instance = concat!( + "EvalMagicTraitNs|EvalMagicTraitNs\\EvalMagicBox|", + "EvalMagicTraitNs\\EvalMagicInner|", + "EvalMagicTraitNs\\EvalMagicInner::report|report:" + ); + let expected_static = concat!( + "EvalMagicTraitNs|EvalMagicTraitNs\\EvalMagicBox|", + "EvalMagicTraitNs\\EvalMagicInner|", + "EvalMagicTraitNs\\EvalMagicInner::stat|stat" + ); + + assert_eq!(values.output, expected_instance); + assert_eq!( + values.get(result), + FakeValue::String(expected_static.to_string()) + ); +} + +/// Verifies trait-imported constants and property defaults use PHP magic scopes. +#[test] +fn execute_program_resolves_eval_trait_member_default_magic_constants() { + let program = parse_fragment( + br#"namespace EvalMagicDefaultNs; +trait EvalMagicDefaultInner { + public const C = __CLASS__; + public const T = __TRAIT__; + public string $p = __CLASS__; + public string $pt = __TRAIT__; + public static string $sp = __CLASS__; + public static string $st = __TRAIT__; +} +trait EvalMagicDefaultOuter { + use EvalMagicDefaultInner; +} +class EvalMagicDefaultBase { + use EvalMagicDefaultOuter; +} +class EvalMagicDefaultChild extends EvalMagicDefaultBase {} +class EvalMagicDirect { + public const C = __CLASS__; + public const T = __TRAIT__; + public string $p = __CLASS__; + public string $pt = __TRAIT__; + public static string $sp = __CLASS__; + public static string $st = __TRAIT__; +} +$object = new EvalMagicDefaultChild(); +$traitProps = (new \ReflectionClass(EvalMagicDefaultInner::class))->getDefaultProperties(); +echo EvalMagicDefaultBase::C . "|" . EvalMagicDefaultBase::T . "|"; +echo EvalMagicDefaultChild::C . "|" . EvalMagicDefaultChild::T . "|"; +echo $object->p . "|" . $object->pt . "|"; +echo EvalMagicDefaultChild::$sp . "|" . EvalMagicDefaultChild::$st . "|"; +echo $traitProps["p"] . "|" . $traitProps["pt"] . ":"; +$direct = new EvalMagicDirect(); +return EvalMagicDirect::C . "|" . EvalMagicDirect::T . "|" . $direct->p . "|" . $direct->pt . "|" . EvalMagicDirect::$sp . "|" . EvalMagicDirect::$st;"#, + ) + .expect("parse eval trait member magic fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let expected_trait = concat!( + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner:" + ); + let expected_direct = concat!( + "EvalMagicDefaultNs\\EvalMagicDirect||", + "EvalMagicDefaultNs\\EvalMagicDirect||", + "EvalMagicDefaultNs\\EvalMagicDirect|" + ); + + assert_eq!(values.output, expected_trait); + assert_eq!( + values.get(result), + FakeValue::String(expected_direct.to_string()) + ); +} + +/// Verifies trait-to-trait composition rejects missing inner traits. +#[test] +fn execute_program_rejects_missing_eval_trait_used_by_trait() { + let program = parse_fragment( + br#"trait EvalMissingNestedOuter { + use EvalMissingNestedInner; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing nested trait use should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies same-name trait aliases that change visibility remain composition fatals. +#[test] +fn execute_program_rejects_eval_trait_same_name_visibility_alias() { + let program = parse_fragment( + br#"trait EvalAliasVisibilitySource { + public function source() { return "T"; } +} +class EvalAliasVisibilityBox { + use EvalAliasVisibilitySource { + source as private source; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("same-name trait alias with visibility change should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies trait adaptations reject missing trait or method targets. +#[test] +fn execute_program_rejects_invalid_eval_trait_adaptation_targets() { + let missing_method = parse_fragment( + br#"trait EvalAdaptMissingMethod { + public function source() { return "T"; } +} +class EvalAdaptMissingMethodBox { + use EvalAdaptMissingMethod { + EvalAdaptMissingMethod::missing insteadof EvalAdaptMissingMethod; + } +}"#, + ) + .expect("parse missing method adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_method, &mut scope, &mut values) + .expect_err("missing adaptation method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_trait = parse_fragment( + br#"trait EvalAdaptMissingTrait { + public function source() { return "T"; } +} +class EvalAdaptMissingTraitBox { + use EvalAdaptMissingTrait { + EvalAdaptMissingTrait::source insteadof EvalAdaptNotUsedTrait; + } +}"#, + ) + .expect("parse missing trait adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_trait, &mut scope, &mut values) + .expect_err("missing adaptation trait should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let self_exclusion = parse_fragment( + br#"trait EvalAdaptSelfExcluded { + public function source() { return "T"; } +} +class EvalAdaptSelfExcludedBox { + use EvalAdaptSelfExcluded { + EvalAdaptSelfExcluded::source insteadof EvalAdaptSelfExcluded; + } +}"#, + ) + .expect("parse self-excluded trait adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&self_exclusion, &mut scope, &mut values) + .expect_err("self-excluded adaptation trait should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_unqualified_alias = parse_fragment( + br#"trait EvalAdaptMissingAlias { + public function source() { return "T"; } +} +class EvalAdaptMissingAliasBox { + use EvalAdaptMissingAlias { + missing as alias; + } +}"#, + ) + .expect("parse missing unqualified alias"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_unqualified_alias, &mut scope, &mut values) + .expect_err("missing unqualified alias method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies unresolved same-name trait methods remain a declaration-time fatal. +#[test] +fn execute_program_rejects_unresolved_eval_trait_method_conflict() { + let program = parse_fragment( + br#"trait EvalConflictA { + public function talk() { return "A"; } +} +trait EvalConflictB { + public function talk() { return "B"; } +} +class EvalConflictBox { + use EvalConflictA, EvalConflictB; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unresolved trait method conflict should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies compatible same-name trait properties are deduplicated during composition. +#[test] +fn execute_program_allows_compatible_eval_trait_property_conflicts() { + let program = parse_fragment( + br#"trait EvalCompatibleTraitPropA { + public int $value; +} +trait EvalCompatibleTraitPropB { + public int $value; +} +class EvalCompatibleTraitPropBox { + use EvalCompatibleTraitPropA, EvalCompatibleTraitPropB; + public int $value; + public function __construct($value) { $this->value = $value; } +} +$box = new EvalCompatibleTraitPropBox(7); +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies incompatible same-name class and trait properties fail like PHP. +#[test] +fn execute_program_rejects_incompatible_eval_trait_property_conflicts() { + let class_conflict = parse_fragment( + br#"trait EvalClassTraitPropConflict { + public int $value; +} +class EvalClassTraitPropConflictBox { + use EvalClassTraitPropConflict; + public string $value; +}"#, + ) + .expect("parse class/trait property conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&class_conflict, &mut scope, &mut values) + .expect_err("incompatible class/trait property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let trait_conflict = parse_fragment( + br#"trait EvalTraitPropConflictA { + public int $value; +} +trait EvalTraitPropConflictB { + public string $value; +} +class EvalTraitPropConflictBox { + use EvalTraitPropConflictA, EvalTraitPropConflictB; +}"#, + ) + .expect("parse trait/trait property conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&trait_conflict, &mut scope, &mut values) + .expect_err("incompatible trait/trait property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/throwables.rs b/crates/elephc-magician/src/interpreter/throwables.rs new file mode 100644 index 0000000000..bdaa266c90 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/throwables.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Builds PHP Throwable objects for interpreter paths that need catchable runtime errors. +//! +//! Called from: +//! - `crate::interpreter::statements` and dynamic dispatch helpers. +//! +//! Key details: +//! - Helpers schedule the object in `ElephcEvalContext` and return `UncaughtThrowable` +//! so surrounding try/catch execution can consume it. + +use super::*; + +/// Creates and schedules an `Error` through eval's normal Throwable channel. +pub(in crate::interpreter) fn eval_throw_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("Error")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates and schedules a `TypeError` through eval's normal Throwable channel. +pub(in crate::interpreter) fn eval_throw_type_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("TypeError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} diff --git a/crates/elephc-magician/src/json_validate.rs b/crates/elephc-magician/src/json_validate.rs new file mode 100644 index 0000000000..3d327b0ff0 --- /dev/null +++ b/crates/elephc-magician/src/json_validate.rs @@ -0,0 +1,481 @@ +//! Purpose: +//! Parses JSON byte streams for eval-side `json_validate()` and `json_decode()`. +//! The parser checks syntax and can return a small JSON tree for runtime-cell materialization. +//! +//! Called from: +//! - `crate::interpreter` when dispatching eval JSON builtins. +//! +//! Key details: +//! - Container depth follows PHP decode/validate semantics: entering a container +//! is rejected when the active depth would reach the requested limit. +//! - String parsing accepts JSON escapes, paired UTF-16 surrogate escapes, and raw +//! UTF-8 bytes while rejecting control bytes; malformed UTF-8 is rejected by +//! default and can be ignored or substituted for PHP JSON UTF-8 flags. + +/// Parsed JSON value used by eval JSON builtins before runtime-cell allocation. +pub(crate) enum JsonValue { + Null, + Bool(bool), + Number(Vec), + String(Vec), + Array(Vec), + Object(Vec<(Vec, JsonValue)>), +} + +/// PHP JSON error produced while parsing eval-side JSON bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct JsonParseError { + kind: JsonParseErrorKind, + offset: usize, +} + +impl JsonParseError { + /// Creates one parser error at a zero-based byte offset. + const fn new(kind: JsonParseErrorKind, offset: usize) -> Self { + Self { kind, offset } + } + + /// Returns the PHP JSON error category for this parse failure. + pub(crate) const fn kind(self) -> JsonParseErrorKind { + self.kind + } + + /// Returns the zero-based byte offset where parsing failed. + pub(crate) const fn offset(self) -> usize { + self.offset + } +} + +/// PHP JSON error category produced while parsing eval-side JSON bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum JsonParseErrorKind { + Depth, + Syntax, + ControlChar, + Utf8, + Utf16, +} + +/// Parses one complete JSON document and preserves the PHP-visible error category. +pub(crate) fn decode_result(bytes: &[u8], depth_limit: usize) -> Result { + let mut parser = Parser::new(bytes, depth_limit); + parser.parse_document() +} + +/// Parses one complete JSON document while ignoring malformed raw UTF-8 string bytes. +pub(crate) fn decode_result_ignoring_invalid_utf8( + bytes: &[u8], + depth_limit: usize, +) -> Result { + let mut parser = Parser::new_with_invalid_utf8_ignore(bytes, depth_limit); + parser.parse_document() +} + +/// Parses one complete JSON document while replacing malformed raw UTF-8 with U+FFFD. +pub(crate) fn decode_result_substituting_invalid_utf8( + bytes: &[u8], + depth_limit: usize, +) -> Result { + let mut parser = Parser::new_with_invalid_utf8_substitute(bytes, depth_limit); + parser.parse_document() +} + +/// How malformed raw UTF-8 bytes inside JSON strings should be handled. +#[derive(Clone, Copy)] +enum InvalidUtf8Mode { + Reject, + Ignore, + Substitute, +} + +/// Cursor-based JSON parser for eval JSON builtin calls. +struct Parser<'a> { + bytes: &'a [u8], + cursor: usize, + depth_limit: usize, + invalid_utf8_mode: InvalidUtf8Mode, +} + +impl<'a> Parser<'a> { + /// Creates a JSON parser over one immutable byte slice. + fn new(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + invalid_utf8_mode: InvalidUtf8Mode::Reject, + } + } + + /// Creates a JSON parser that drops malformed raw UTF-8 bytes inside strings. + fn new_with_invalid_utf8_ignore(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + invalid_utf8_mode: InvalidUtf8Mode::Ignore, + } + } + + /// Creates a JSON parser that substitutes malformed raw UTF-8 bytes inside strings. + fn new_with_invalid_utf8_substitute(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + invalid_utf8_mode: InvalidUtf8Mode::Substitute, + } + } + + /// Parses one complete JSON document and rejects trailing non-whitespace bytes. + fn parse_document(&mut self) -> Result { + self.skip_ws(); + let value = self.parse_value(0)?; + self.skip_ws(); + if self.cursor == self.bytes.len() { + Ok(value) + } else { + Err(self.error(JsonParseErrorKind::Syntax)) + } + } + + /// Parses any JSON value at the given active container depth. + fn parse_value(&mut self, depth: usize) -> Result { + self.skip_ws(); + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { + b'n' => self.consume_literal_value(b"null", JsonValue::Null), + b't' => self.consume_literal_value(b"true", JsonValue::Bool(true)), + b'f' => self.consume_literal_value(b"false", JsonValue::Bool(false)), + b'"' => self.parse_string().map(JsonValue::String), + b'[' => self.parse_array(depth), + b'{' => self.parse_object(depth), + b'-' | b'0'..=b'9' => self.parse_number().map(JsonValue::Number), + _ => Err(self.error(JsonParseErrorKind::Syntax)), + } + } + + /// Consumes one JSON literal and returns its parsed value. + fn consume_literal_value( + &mut self, + literal: &[u8], + value: JsonValue, + ) -> Result { + if self.consume_literal(literal) { + Ok(value) + } else { + Err(self.error(JsonParseErrorKind::Syntax)) + } + } + + /// Parses a JSON array and enforces PHP's validate/decode depth threshold. + fn parse_array(&mut self, depth: usize) -> Result { + if depth + 1 >= self.depth_limit { + return Err(self.error(JsonParseErrorKind::Depth)); + } + self.cursor += 1; + self.skip_ws(); + let mut elements = Vec::new(); + if self.consume_byte(b']') { + return Ok(JsonValue::Array(elements)); + } + + loop { + elements.push(self.parse_value(depth + 1)?); + self.skip_ws(); + if self.consume_byte(b']') { + return Ok(JsonValue::Array(elements)); + } + if !self.consume_byte(b',') { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + } + } + + /// Parses a JSON object and enforces PHP's validate/decode depth threshold. + fn parse_object(&mut self, depth: usize) -> Result { + if depth + 1 >= self.depth_limit { + return Err(self.error(JsonParseErrorKind::Depth)); + } + self.cursor += 1; + self.skip_ws(); + let mut entries = Vec::new(); + if self.consume_byte(b'}') { + return Ok(JsonValue::Object(entries)); + } + + loop { + self.skip_ws(); + let key = self.parse_string()?; + self.skip_ws(); + if !self.consume_byte(b':') { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + entries.push((key, self.parse_value(depth + 1)?)); + self.skip_ws(); + if self.consume_byte(b'}') { + return Ok(JsonValue::Object(entries)); + } + if !self.consume_byte(b',') { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + } + } + + /// Parses a JSON string into UTF-8 bytes after applying JSON escapes. + fn parse_string(&mut self) -> Result, JsonParseError> { + if !self.consume_byte(b'"') { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + + let mut output = Vec::new(); + while let Some(byte) = self.peek() { + match byte { + b'"' => { + self.cursor += 1; + return Ok(output); + } + b'\\' => { + self.parse_string_escape(&mut output)?; + } + 0x00..=0x1f => return Err(self.error(JsonParseErrorKind::ControlChar)), + 0x00..=0x7f => { + output.push(byte); + self.cursor += 1; + } + _ => { + let start = self.cursor; + match self.consume_utf8_char() { + Ok(()) => output.extend_from_slice(&self.bytes[start..self.cursor]), + Err(error) => self.handle_invalid_utf8(error, start, &mut output)?, + } + } + } + } + Err(self.error(JsonParseErrorKind::Syntax)) + } + + /// Applies the configured malformed-UTF-8 policy for a raw string byte. + fn handle_invalid_utf8( + &mut self, + error: JsonParseError, + start: usize, + output: &mut Vec, + ) -> Result<(), JsonParseError> { + match self.invalid_utf8_mode { + InvalidUtf8Mode::Reject => Err(error), + InvalidUtf8Mode::Ignore => { + self.cursor = start + 1; + Ok(()) + } + InvalidUtf8Mode::Substitute => { + append_codepoint(output, 0xfffd) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf8))?; + self.cursor = start + 1; + Ok(()) + } + } + } + + /// Parses one JSON string escape sequence at the current backslash. + fn parse_string_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { + self.cursor += 1; + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { + b'"' => output.push(b'"'), + b'\\' => output.push(b'\\'), + b'/' => output.push(b'/'), + b'b' => output.push(0x08), + b'f' => output.push(0x0c), + b'n' => output.push(b'\n'), + b'r' => output.push(b'\r'), + b't' => output.push(b'\t'), + b'u' => { + self.parse_unicode_escape(output)?; + return Ok(()); + } + _ => return Err(self.error(JsonParseErrorKind::Syntax)), + } + self.cursor += 1; + Ok(()) + } + + /// Parses one JSON `\uXXXX` escape, including mandatory surrogate pairs. + fn parse_unicode_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { + let unit = self.parse_unicode_unit()?; + if (0xd800..=0xdbff).contains(&unit) { + if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { + return Err(self.error(JsonParseErrorKind::Utf16)); + } + let low = match self.parse_unicode_unit_after_u() { + Ok(low) => low, + Err(_) => return Err(self.error(JsonParseErrorKind::Utf16)), + }; + if !(0xdc00..=0xdfff).contains(&low) { + return Err(self.error(JsonParseErrorKind::Utf16)); + } + let high = u32::from(unit - 0xd800); + let low = u32::from(low - 0xdc00); + append_codepoint(output, 0x10000 + ((high << 10) | low)) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf16)) + } else if (0xdc00..=0xdfff).contains(&unit) { + Err(self.error(JsonParseErrorKind::Utf16)) + } else { + append_codepoint(output, u32::from(unit)) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf16)) + } + } + + /// Parses the `uXXXX` suffix after the backslash has already been consumed. + fn parse_unicode_unit(&mut self) -> Result { + if !self.consume_byte(b'u') { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + self.parse_unicode_unit_after_u() + } + + /// Parses the four hex digits after a consumed JSON unicode escape marker. + fn parse_unicode_unit_after_u(&mut self) -> Result { + if self.cursor + 4 > self.bytes.len() { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + let mut value = 0_u16; + for _ in 0..4 { + let digit = hex_value(self.bytes[self.cursor]) + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))?; + value = value * 16 + u16::from(digit); + self.cursor += 1; + } + Ok(value) + } + + /// Parses a JSON number with RFC-compatible leading-zero, fraction, and exponent rules. + fn parse_number(&mut self) -> Result, JsonParseError> { + let start = self.cursor; + if self.consume_byte(b'-') && self.peek().is_none() { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { + b'0' => { + self.cursor += 1; + if matches!(self.peek(), Some(b'0'..=b'9')) { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + } + b'1'..=b'9' => { + self.cursor += 1; + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + _ => return Err(self.error(JsonParseErrorKind::Syntax)), + } + + if self.consume_byte(b'.') { + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + + if matches!(self.peek(), Some(b'e' | b'E')) { + self.cursor += 1; + if matches!(self.peek(), Some(b'+' | b'-')) { + self.cursor += 1; + } + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return Err(self.error(JsonParseErrorKind::Syntax)); + } + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + + Ok(self.bytes[start..self.cursor].to_vec()) + } + + /// Consumes exactly one expected byte when it is present. + fn consume_byte(&mut self, expected: u8) -> bool { + if self.peek() == Some(expected) { + self.cursor += 1; + true + } else { + false + } + } + + /// Consumes one ASCII literal at the current cursor. + fn consume_literal(&mut self, literal: &[u8]) -> bool { + if self.bytes[self.cursor..].starts_with(literal) { + self.cursor += literal.len(); + true + } else { + false + } + } + + /// Consumes one valid UTF-8 codepoint from a raw JSON string segment. + fn consume_utf8_char(&mut self) -> Result<(), JsonParseError> { + let first = self.bytes[self.cursor]; + let width = match first { + 0xc2..=0xdf => 2, + 0xe0..=0xef => 3, + 0xf0..=0xf4 => 4, + _ => return Err(self.error(JsonParseErrorKind::Utf8)), + }; + if self.cursor + width > self.bytes.len() { + return Err(self.error(JsonParseErrorKind::Utf8)); + } + let slice = &self.bytes[self.cursor..self.cursor + width]; + if std::str::from_utf8(slice).is_err() { + return Err(self.error(JsonParseErrorKind::Utf8)); + } + self.cursor += width; + Ok(()) + } + + /// Skips JSON whitespace accepted between tokens. + fn skip_ws(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\n' | b'\r' | b'\t')) { + self.cursor += 1; + } + } + + /// Returns the current byte without advancing. + fn peek(&self) -> Option { + self.bytes.get(self.cursor).copied() + } + + /// Creates a parser error at the current cursor byte offset. + fn error(&self, kind: JsonParseErrorKind) -> JsonParseError { + JsonParseError::new(kind, self.cursor) + } +} + +/// Appends one Unicode codepoint to a decoded JSON string. +fn append_codepoint(output: &mut Vec, codepoint: u32) -> Option<()> { + let ch = char::from_u32(codepoint)?; + let mut buffer = [0_u8; 4]; + output.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes()); + Some(()) +} + +/// Returns one hexadecimal digit value for JSON unicode escapes. +fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/elephc-magician/src/lexer/mod.rs b/crates/elephc-magician/src/lexer/mod.rs new file mode 100644 index 0000000000..9c3ba4cdf3 --- /dev/null +++ b/crates/elephc-magician/src/lexer/mod.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Groups tokenization for runtime PHP eval fragments. +//! The lexer owns token definitions and source scanning before parser grammar +//! state consumes those tokens. +//! +//! Called from: +//! - `crate::parser::parse_fragment()`. +//! +//! Key details: +//! - Fragment line metadata is captured while scanning magic constants. +//! - PHP opening tags are rejected before tokenization by the parser entry point. + +mod scan; +mod token; + +pub(crate) use scan::tokenize; +pub(crate) use token::{Token, TokenKind}; diff --git a/crates/elephc-magician/src/lexer/scan.rs b/crates/elephc-magician/src/lexer/scan.rs new file mode 100644 index 0000000000..e10cf044a0 --- /dev/null +++ b/crates/elephc-magician/src/lexer/scan.rs @@ -0,0 +1,519 @@ +//! Purpose: +//! Scans UTF-8 eval source fragments into eval parser tokens. +//! This file owns trivia skipping, literal lexing, PHP string escapes, and +//! magic-constant token recognition. +//! +//! Called from: +//! - `crate::lexer::tokenize()` re-exported by `crate::lexer`. +//! +//! Key details: +//! - Comments and whitespace advance line metadata for `__LINE__`. +//! - Unterminated strings or block comments return parse errors before grammar parsing. + +use super::{Token, TokenKind}; +use crate::errors::EvalParseError; +use crate::eval_ir::EvalMagicConst; + +/// Tokenizes a complete source fragment and appends an EOF sentinel. +pub(crate) fn tokenize(source: &str) -> Result, EvalParseError> { + Lexer::new(source).tokenize() +} + +/// Converts a UTF-8 eval source fragment into parser tokens. +struct Lexer<'a> { + source: &'a str, + pos: usize, + line: i64, +} + +impl<'a> Lexer<'a> { + /// Creates a lexer over a UTF-8 eval fragment. + fn new(source: &'a str) -> Self { + Self { + source, + pos: 0, + line: 1, + } + } + + /// Tokenizes the complete source and appends an EOF sentinel. + fn tokenize(mut self) -> Result, EvalParseError> { + let mut tokens = Vec::new(); + loop { + let token = self.next_token()?; + let done = *token.kind() == TokenKind::Eof; + tokens.push(token); + if done { + break; + } + } + Ok(tokens) + } + + /// Reads the next token from the source. + fn next_token(&mut self) -> Result { + self.skip_trivia()?; + let Some(ch) = self.peek_char() else { + return Ok(Token::new(TokenKind::Eof, self.line)); + }; + let line = self.line; + let kind = match ch { + '$' => self.lex_variable(), + '\'' | '"' => self.lex_string(ch), + '0'..='9' => self.lex_number(), + '+' => { + self.bump_char(); + if self.peek_char() == Some('+') { + self.bump_char(); + Ok(TokenKind::PlusPlus) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PlusEqual) + } else { + Ok(TokenKind::Plus) + } + } + '-' => { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Arrow) + } else if self.peek_char() == Some('-') { + self.bump_char(); + Ok(TokenKind::MinusMinus) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::MinusEqual) + } else { + Ok(TokenKind::Minus) + } + } + '*' => { + self.bump_char(); + if self.peek_char() == Some('*') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarStarEqual) + } else { + Ok(TokenKind::StarStar) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarEqual) + } else { + Ok(TokenKind::Star) + } + } + '/' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::SlashEqual) + } else { + Ok(TokenKind::Slash) + } + } + '%' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PercentEqual) + } else { + Ok(TokenKind::Percent) + } + } + '.' => { + self.bump_char(); + if self.peek_char() == Some('.') && self.peek_next_char() == Some('.') { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::Ellipsis) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::DotEqual) + } else { + Ok(TokenKind::Dot) + } + } + '=' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::EqualEqualEqual) + } else { + Ok(TokenKind::EqualEqual) + } + } else if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::FatArrow) + } else { + Ok(TokenKind::Equal) + } + } + '!' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::NotEqualEqual) + } else { + Ok(TokenKind::NotEqual) + } + } else { + Ok(TokenKind::Bang) + } + } + '&' => { + self.bump_char(); + if self.peek_char() == Some('&') { + self.bump_char(); + Ok(TokenKind::AndAnd) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::AmpEqual) + } else { + Ok(TokenKind::Ampersand) + } + } + '|' => { + self.bump_char(); + if self.peek_char() == Some('|') { + self.bump_char(); + Ok(TokenKind::OrOr) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PipeEqual) + } else { + Ok(TokenKind::Pipe) + } + } + '^' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::CaretEqual) + } else { + Ok(TokenKind::Caret) + } + } + '~' => { + self.bump_char(); + Ok(TokenKind::Tilde) + } + '<' => { + self.bump_char(); + if self.peek_char() == Some('<') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::LessLessEqual) + } else { + Ok(TokenKind::LessLess) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Spaceship) + } else { + Ok(TokenKind::LessEqual) + } + } else { + Ok(TokenKind::Less) + } + } + '>' => { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterGreaterEqual) + } else { + Ok(TokenKind::GreaterGreater) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterEqual) + } else { + Ok(TokenKind::Greater) + } + } + '?' => { + self.bump_char(); + if self.peek_char() == Some('-') && self.peek_next_char() == Some('>') { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::QuestionArrow) + } else if self.peek_char() == Some('?') { + self.bump_char(); + Ok(TokenKind::QuestionQuestion) + } else { + Ok(TokenKind::Question) + } + } + ';' => { + self.bump_char(); + Ok(TokenKind::Semicolon) + } + '(' => { + self.bump_char(); + Ok(TokenKind::LParen) + } + ')' => { + self.bump_char(); + Ok(TokenKind::RParen) + } + '[' => { + self.bump_char(); + Ok(TokenKind::LBracket) + } + ']' => { + self.bump_char(); + Ok(TokenKind::RBracket) + } + '{' => { + self.bump_char(); + Ok(TokenKind::LBrace) + } + '}' => { + self.bump_char(); + Ok(TokenKind::RBrace) + } + ',' => { + self.bump_char(); + Ok(TokenKind::Comma) + } + ':' => { + self.bump_char(); + if self.peek_char() == Some(':') { + self.bump_char(); + Ok(TokenKind::DoubleColon) + } else { + Ok(TokenKind::Colon) + } + } + '\\' => { + self.bump_char(); + Ok(TokenKind::Backslash) + } + '#' if self.peek_next_char() == Some('[') => { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::AttributeStart) + } + _ if is_ident_start(ch) => { + let ident = self.lex_ident(); + Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) + } + _ => Err(EvalParseError::UnexpectedToken), + }?; + Ok(Token::new(kind, line)) + } + + /// Reads a `$name` token. + fn lex_variable(&mut self) -> Result { + self.bump_char(); + if self.peek_char() == Some('{') { + self.bump_char(); + return Ok(TokenKind::DollarLBrace); + } + let name = self.lex_ident(); + if name.is_empty() { + return Err(EvalParseError::ExpectedVariable); + } + Ok(TokenKind::DollarIdent(name)) + } + + /// Reads a PHP identifier body at the current byte offset. + fn lex_ident(&mut self) -> String { + let mut ident = String::new(); + while let Some(ch) = self.peek_char() { + if !is_ident_continue(ch) { + break; + } + ident.push(ch); + self.bump_char(); + } + ident + } + + /// Reads an integer or float literal. + fn lex_number(&mut self) -> Result { + let start = self.pos; + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + let mut is_float = false; + if self.peek_char() == Some('.') && matches!(self.peek_next_char(), Some('0'..='9')) { + is_float = true; + self.bump_char(); + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + } + let raw = &self.source[start..self.pos]; + if is_float { + raw.parse::() + .map(TokenKind::Float) + .map_err(|_| EvalParseError::InvalidNumber) + } else { + raw.parse::() + .map(TokenKind::Int) + .map_err(|_| EvalParseError::InvalidNumber) + } + } + + /// Reads a single- or double-quoted string literal. + fn lex_string(&mut self, quote: char) -> Result { + self.bump_char(); + let mut out = String::new(); + while let Some(ch) = self.peek_char() { + self.bump_char(); + if ch == quote { + return Ok(TokenKind::String(out)); + } + if ch == '\\' { + let Some(escaped) = self.peek_char() else { + return Err(EvalParseError::UnterminatedString); + }; + self.bump_char(); + if quote == '\'' { + match escaped { + '\\' => out.push('\\'), + '\'' => out.push('\''), + other => { + out.push('\\'); + out.push(other); + } + } + } else { + match escaped { + 'n' => out.push('\n'), + 'r' => out.push('\r'), + 't' => out.push('\t'), + 'v' => out.push('\x0b'), + 'e' => out.push('\x1b'), + 'f' => out.push('\x0c'), + '\\' => out.push('\\'), + '"' => out.push('"'), + '$' => out.push('$'), + other => { + out.push('\\'); + out.push(other); + } + } + } + } else { + out.push(ch); + } + } + Err(EvalParseError::UnterminatedString) + } + + /// Advances past ASCII/Unicode whitespace and PHP comments. + fn skip_trivia(&mut self) -> Result<(), EvalParseError> { + loop { + while self.peek_char().is_some_and(char::is_whitespace) { + self.bump_char(); + } + match (self.peek_char(), self.peek_next_char()) { + (Some('/'), Some('/')) => self.skip_line_comment(), + (Some('#'), Some('[')) => return Ok(()), + (Some('#'), _) => self.skip_line_comment(), + (Some('/'), Some('*')) => self.skip_block_comment()?, + _ => return Ok(()), + } + } + } + + /// Advances past a `//` or `#` comment, including its trailing newline when present. + fn skip_line_comment(&mut self) { + while let Some(ch) = self.peek_char() { + self.bump_char(); + if ch == '\n' { + break; + } + } + } + + /// Advances past a `/* ... */` comment while preserving fragment line metadata. + fn skip_block_comment(&mut self) -> Result<(), EvalParseError> { + self.bump_char(); + self.bump_char(); + while let Some(ch) = self.peek_char() { + if ch == '*' && self.peek_next_char() == Some('/') { + self.bump_char(); + self.bump_char(); + return Ok(()); + } + self.bump_char(); + } + Err(EvalParseError::UnterminatedComment) + } + + /// Returns the current char without advancing. + fn peek_char(&self) -> Option { + self.source[self.pos..].chars().next() + } + + /// Returns the char after the current char without advancing. + fn peek_next_char(&self) -> Option { + let mut chars = self.source[self.pos..].chars(); + chars.next()?; + chars.next() + } + + /// Advances by one UTF-8 char. + fn bump_char(&mut self) { + if let Some(ch) = self.peek_char() { + self.pos += ch.len_utf8(); + if ch == '\n' { + self.line += 1; + } + } + } +} + +/// Returns true for the first character of a PHP variable/function identifier. +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +/// Returns true for subsequent characters in a PHP variable/function identifier. +fn is_ident_continue(ch: char) -> bool { + is_ident_start(ch) || ch.is_ascii_digit() +} + +/// Converts a PHP magic-constant identifier into a parser token when recognized. +fn magic_const_token(name: &str, line: i64) -> Option { + let magic = if ident_eq(name, "__FILE__") { + EvalMagicConst::File + } else if ident_eq(name, "__DIR__") { + EvalMagicConst::Dir + } else if ident_eq(name, "__LINE__") { + EvalMagicConst::Line(line) + } else if ident_eq(name, "__FUNCTION__") { + EvalMagicConst::Function + } else if ident_eq(name, "__CLASS__") { + EvalMagicConst::Class + } else if ident_eq(name, "__METHOD__") { + EvalMagicConst::Method + } else if ident_eq(name, "__NAMESPACE__") { + EvalMagicConst::Namespace + } else if ident_eq(name, "__TRAIT__") { + EvalMagicConst::Trait + } else { + return None; + }; + Some(TokenKind::Magic(magic)) +} + +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} diff --git a/crates/elephc-magician/src/lexer/token.rs b/crates/elephc-magician/src/lexer/token.rs new file mode 100644 index 0000000000..3c7726beaa --- /dev/null +++ b/crates/elephc-magician/src/lexer/token.rs @@ -0,0 +1,113 @@ +//! Purpose: +//! Defines token kinds for runtime PHP eval fragment parsing. +//! Tokens are intentionally scoped to the eval subset and do not expose the +//! main compiler lexer token contract. +//! +//! Called from: +//! - `crate::lexer::scan::tokenize()` +//! - `crate::parser::state::Parser` +//! +//! Key details: +//! - Magic constants carry precomputed fragment line metadata when needed. + +use crate::eval_ir::EvalMagicConst; + +/// One token plus its eval-fragment source line. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Token { + kind: TokenKind, + line: i64, +} + +impl Token { + /// Creates one token at the given eval-fragment line. + pub(crate) const fn new(kind: TokenKind, line: i64) -> Self { + Self { kind, line } + } + + /// Returns the parser-visible token kind. + pub(crate) fn kind(&self) -> &TokenKind { + &self.kind + } + + /// Consumes the token and returns its parser-visible kind. + pub(crate) fn into_kind(self) -> TokenKind { + self.kind + } + + /// Returns the one-based eval-fragment line where this token starts. + pub(crate) const fn line(&self) -> i64 { + self.line + } +} + +/// Token kinds used by the initial eval fragment parser. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum TokenKind { + DollarLBrace, + DollarIdent(String), + Ident(String), + Magic(EvalMagicConst), + Int(i64), + Float(f64), + String(String), + Plus, + PlusPlus, + PlusEqual, + Minus, + MinusMinus, + MinusEqual, + Arrow, + Star, + StarStar, + StarStarEqual, + StarEqual, + Slash, + SlashEqual, + Percent, + PercentEqual, + Ampersand, + AmpEqual, + Pipe, + PipeEqual, + Caret, + CaretEqual, + Tilde, + Dot, + DotEqual, + Ellipsis, + Equal, + EqualEqual, + EqualEqualEqual, + Bang, + NotEqual, + NotEqualEqual, + AndAnd, + OrOr, + Less, + LessEqual, + Spaceship, + LessLess, + LessLessEqual, + Greater, + GreaterEqual, + GreaterGreater, + GreaterGreaterEqual, + FatArrow, + Question, + QuestionArrow, + QuestionQuestion, + Semicolon, + LParen, + RParen, + LBracket, + RBracket, + LBrace, + RBrace, + Comma, + Colon, + DoubleColon, + Backslash, + AttributeStart, + Eof, +} diff --git a/crates/elephc-magician/src/lib.rs b/crates/elephc-magician/src/lib.rs new file mode 100644 index 0000000000..0fdac35156 --- /dev/null +++ b/crates/elephc-magician/src/lib.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Optional C ABI bridge crate for elephc's runtime `eval()` support. +//! The crate root owns only the public module map and re-exports stable FFI +//! entry points whose implementations live in focused modules. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_*` symbols. +//! - `cargo test -p elephc-magician` for ABI-shape validation. +//! +//! Key details: +//! - No Rust panic or Rust-specific enum crosses the ABI boundary. +//! - Non-test builds execute EvalIR through generated runtime value wrappers. + +pub mod abi; +pub mod context; +pub mod errors; +pub mod eval_ir; +mod ffi; +pub mod interpreter; +mod json_validate; +mod lexer; +pub mod lower; +mod parse_cache; +pub mod parser; +pub mod runtime_hooks; +pub mod scope; +mod stream_resources; +mod stream_wrappers; +pub mod value; + +pub use interpreter::builtin_metadata; +pub use ffi::*; diff --git a/crates/elephc-magician/src/lower.rs b/crates/elephc-magician/src/lower.rs new file mode 100644 index 0000000000..b31757e4ac --- /dev/null +++ b/crates/elephc-magician/src/lower.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Placeholder for lowering parsed eval fragments into EvalIR. +//! Keeps parse, lowering, and interpretation responsibilities split from the +//! start of the eval bridge crate. +//! +//! Called from: +//! - Future `crate::interpreter` execution flow. +//! +//! Key details: +//! - EvalIR will use by-name scope operations rather than static local slots. + +/// Validates that the lowering stub is intentionally unavailable. +pub fn lowering_is_stubbed() -> bool { + true +} diff --git a/crates/elephc-magician/src/parse_cache.rs b/crates/elephc-magician/src/parse_cache.rs new file mode 100644 index 0000000000..164ba6ca1c --- /dev/null +++ b/crates/elephc-magician/src/parse_cache.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Caches parsed eval fragments before interpreter execution. +//! This removes repeated tokenization and parsing for identical runtime source +//! bytes while keeping execution context and scope fully dynamic. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` +//! - `crate::interpreter::include_exec` for nested eval/include parsing. +//! +//! Key details: +//! - The cache stores immutable EvalIR parse results only, never runtime cells, +//! declarations, scope entries, or context-derived magic-constant values. +//! - Large fragments bypass the cache to avoid pinning one-off source strings. + +use crate::errors::EvalParseError; +use crate::eval_ir::EvalProgram; +use crate::parser; +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; + +const EVAL_PARSE_CACHE_CAPACITY: usize = 256; +const MAX_CACHEABLE_FRAGMENT_BYTES: usize = 64 * 1024; + +type CachedParseResult = Result, EvalParseError>; + +static EVAL_PARSE_CACHE: OnceLock> = OnceLock::new(); + +/// Parses an eval fragment, reusing a cached immutable EvalIR program when available. +pub(crate) fn parse_fragment_cached(code: &[u8]) -> CachedParseResult { + if !is_cacheable_fragment(code) { + return parser::parse_fragment(code).map(Arc::new); + } + if let Some(result) = lock_eval_parse_cache().lookup(code) { + return result; + } + let result = parser::parse_fragment(code).map(Arc::new); + lock_eval_parse_cache().insert(code.to_vec(), result.clone()); + result +} + +/// Returns true when a fragment is small enough to retain in the parse cache. +fn is_cacheable_fragment(code: &[u8]) -> bool { + code.len() <= MAX_CACHEABLE_FRAGMENT_BYTES +} + +/// Returns the process-wide eval parse cache singleton. +fn eval_parse_cache() -> &'static Mutex { + EVAL_PARSE_CACHE.get_or_init(|| Mutex::new(EvalParseCache::new(EVAL_PARSE_CACHE_CAPACITY))) +} + +/// Locks the parse cache and recovers the inner cache if a previous panic poisoned it. +fn lock_eval_parse_cache() -> MutexGuard<'static, EvalParseCache> { + eval_parse_cache() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +/// Bounded FIFO cache for immutable eval parse results. +struct EvalParseCache { + capacity: usize, + entries: HashMap, CachedParseResult>, + order: VecDeque>, +} + +impl EvalParseCache { + /// Creates an empty cache with the requested maximum entry count. + fn new(capacity: usize) -> Self { + Self { + capacity, + entries: HashMap::new(), + order: VecDeque::new(), + } + } + + /// Returns a cloned cached parse result for the exact source bytes. + fn lookup(&self, code: &[u8]) -> Option { + self.entries.get(code).cloned() + } + + /// Inserts a parse result and evicts the oldest distinct source when full. + fn insert(&mut self, code: Vec, result: CachedParseResult) { + if self.capacity == 0 { + return; + } + if self.entries.contains_key(code.as_slice()) { + self.entries.insert(code, result); + return; + } + while self.entries.len() >= self.capacity { + let Some(oldest) = self.order.pop_front() else { + break; + }; + self.entries.remove(&oldest); + } + self.order.push_back(code.clone()); + self.entries.insert(code, result); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies repeated successful parses reuse the stored EvalIR allocation. + #[test] + fn cache_reuses_successful_parse_result() { + let mut cache = EvalParseCache::new(4); + let source = b"return 1;"; + let parsed = Arc::new(parser::parse_fragment(source).expect("fragment should parse")); + + cache.insert(source.to_vec(), Ok(parsed.clone())); + let hit = cache + .lookup(source) + .expect("source should be cached") + .expect("cached source should be successful"); + + assert!(Arc::ptr_eq(&parsed, &hit)); + } + + /// Verifies parse errors are cached too so repeated invalid fragments avoid reparsing. + #[test] + fn cache_reuses_parse_errors() { + let mut cache = EvalParseCache::new(4); + let source = b" Result<(), EvalParseError> { + if self.consume(expected) { + Ok(()) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + + /// Consumes a semicolon or returns the semicolon-specific parse error. + pub(super) fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { + if self.consume_semicolon() { + Ok(()) + } else { + Err(EvalParseError::ExpectedSemicolon) + } + } + + /// Consumes a semicolon if present. + pub(super) fn consume_semicolon(&mut self) -> bool { + self.consume(TokenKind::Semicolon) + } + + /// Consumes `expected` if the current token matches it. + pub(super) fn consume(&mut self, expected: TokenKind) -> bool { + if *self.current() == expected { + self.advance(); + true + } else { + false + } + } + + /// Returns the current token. + pub(super) fn current(&self) -> &TokenKind { + self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) + } + + /// Returns the line attached to the current token. + pub(super) fn current_line(&self) -> i64 { + self.token_lines.get(self.pos).copied().unwrap_or(1) + } + + /// Returns the next token without advancing. + pub(super) fn peek(&self) -> &TokenKind { + self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) + } + + /// Advances to the next token. + pub(super) fn advance(&mut self) { + if self.pos < self.tokens.len() { + self.pos += 1; + } + } +} + +/// Returns true when the current token closes or starts a switch case arm. +pub(super) fn is_switch_case_boundary(token: &TokenKind) -> bool { + matches!(token, TokenKind::RBrace) + || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) +} + +/// Maps simple variable assignment tokens to an optional compound EvalIR operator. +pub(super) fn assignment_op(token: &TokenKind) -> Option> { + match token { + TokenKind::Equal => Some(None), + TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), + TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), + TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), + TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), + TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), + TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), + TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), + TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), + TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), + TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), + TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), + _ => None, + } +} + +/// Builds the assigned value expression for plain and compound variable assignment. +pub(super) fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { + match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::LoadVar(name.to_string())), + right: Box::new(value), + }, + None => value, + } +} + +/// Builds the StoreVar statement for a simple variable increment or decrement. +pub(super) fn inc_dec_store(name: String, increment: bool) -> EvalStmt { + EvalStmt::StoreVar { + value: EvalExpr::Binary { + op: if increment { + EvalBinOp::Add + } else { + EvalBinOp::Sub + }, + left: Box::new(EvalExpr::LoadVar(name.clone())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + name, + } +} + +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} + +/// Returns true when PHP forbids a name for class, interface, trait, or enum declarations. +pub(super) fn is_reserved_class_like_name(name: &str) -> bool { + [ + "__halt_compiler", + "abstract", + "and", + "array", + "as", + "break", + "callable", + "case", + "catch", + "class", + "clone", + "const", + "continue", + "declare", + "default", + "die", + "do", + "echo", + "else", + "elseif", + "empty", + "enddeclare", + "endfor", + "endforeach", + "endif", + "endswitch", + "endwhile", + "eval", + "exit", + "extends", + "false", + "final", + "finally", + "fn", + "for", + "foreach", + "function", + "global", + "goto", + "if", + "implements", + "include", + "include_once", + "instanceof", + "insteadof", + "interface", + "isset", + "list", + "match", + "namespace", + "new", + "null", + "or", + "print", + "private", + "protected", + "public", + "readonly", + "require", + "require_once", + "return", + "static", + "switch", + "throw", + "trait", + "true", + "try", + "unset", + "use", + "var", + "while", + "xor", + "yield", + "bool", + "int", + "float", + "string", + "object", + "mixed", + "never", + "void", + "iterable", + "self", + "parent", + ] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. +pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { + let _ = name; + false +} + +/// Returns true for class member modifiers outside the current eval class subset. +pub(super) fn is_unsupported_class_member_modifier(name: &str) -> bool { + let _ = name; + false +} + +/// Returns true when an identifier is an include/require expression construct. +pub(super) fn is_include_construct_name(name: &str) -> bool { + ["include", "include_once", "require", "require_once"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns the first namespace segment and the optional remaining suffix. +pub(super) fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { + name.split_once('\\') + .map_or((name, None), |(first, tail)| (first, Some(tail))) +} + +/// Returns the final segment of a PHP qualified name. +pub(super) fn last_name_segment(name: &str) -> &str { + name.rsplit('\\').next().unwrap_or(name) +} + +/// Combines a grouped use prefix with one relative member name. +pub(super) fn join_grouped_use_name(prefix: &str, member: &str) -> String { + format!("{prefix}\\{member}") +} + +/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. +pub(super) fn is_unsupported_expression_keyword(name: &str) -> bool { + ["yield"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs new file mode 100644 index 0000000000..c784206323 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Parses PHP eval expressions using PHP-compatible precedence and postfix syntax. +//! +//! Called from: +//! - `crate::parser::statements` for expression-bearing statements. +//! +//! Key details: +//! - Logical keyword precedence, ternary associativity, coalesce, and exponentiation follow PHP grammar. +//! - Name resolution uses parser namespace/import state while building EvalIR call and constant nodes. + +use super::cursor::*; +use super::state::*; +use super::statements::{EvalTypePosition, ParsedMethodParams}; +use crate::errors::EvalParseError; +use crate::eval_ir::{ + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCastType, EvalClosureCapture, EvalConst, + EvalExpr, EvalFunction, EvalInstanceOfTarget, EvalMagicConst, EvalMatchArm, EvalSourceLocation, + EvalUnaryOp, +}; +use crate::lexer::TokenKind; + +mod callables_arrays; +mod postfix; +mod precedence; +mod primary; +mod static_names; diff --git a/crates/elephc-magician/src/parser/expressions/callables_arrays.rs b/crates/elephc-magician/src/parser/expressions/callables_arrays.rs new file mode 100644 index 0000000000..00caae4421 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/callables_arrays.rs @@ -0,0 +1,258 @@ +//! Purpose: +//! Parses call arguments, first-class callable markers, closures/captures, and +//! modern or legacy array literals. +//! +//! Called from: +//! - Primary, postfix, static-member, and object-construction expression parsing. +//! +//! Key details: +//! - Source-order arguments, spread/named syntax, and closure captures remain intact. + +use super::*; + +impl Parser { + + /// Parses a parenthesized source-order argument list. + pub(in crate::parser) fn parse_call_args(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LParen)?; + let mut args = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(args); + } + loop { + args.push(self.parse_call_arg()?); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RParen) { + return Ok(args); + } + } + self.expect(TokenKind::RParen)?; + Ok(args) + } + + /// Parses one positional or named argument within a call argument list. + pub(in crate::parser) fn parse_call_arg(&mut self) -> Result { + if self.consume(TokenKind::Ellipsis) { + return self.parse_expr().map(EvalCallArg::spread); + } + if matches!(self.peek(), TokenKind::Colon) { + if let TokenKind::Ident(name) = self.current() { + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + return Ok(EvalCallArg::named(name, value)); + } + } + self.parse_expr().map(EvalCallArg::positional) + } + + /// Consumes PHP's `(...)` first-class callable marker when it is the whole argument list. + pub(super) fn consume_first_class_callable_marker(&mut self) -> bool { + if matches!(self.current(), TokenKind::LParen) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::Ellipsis)) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) + { + self.advance(); + self.advance(); + self.advance(); + true + } else { + false + } + } + + /// Builds an eval function-callable expression with namespace fallback metadata. + pub(super) fn function_callable_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return Self::function_callable_value(imported.to_ascii_lowercase(), None); + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + Self::function_callable_value(fallback_name, None) + } else { + Self::function_callable_value( + self.qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + Some(fallback_name), + ) + } + } + + /// Builds the EvalIR node that resolves a first-class function callable at runtime. + pub(super) fn function_callable_value(name: String, fallback_name: Option) -> EvalExpr { + EvalExpr::FunctionCallable { + name, + fallback_name, + } + } + + /// Builds the EvalIR node used for object method first-class callables. + pub(super) fn method_callable_expr(object: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::MethodCallable { + object: Box::new(object), + method: Box::new(method), + } + } + + /// Builds the EvalIR node used for invokable-object first-class callables. + pub(super) fn invokable_callable_expr(object: EvalExpr) -> EvalExpr { + EvalExpr::InvokableCallable { + object: Box::new(object), + } + } + + /// Builds the EvalIR node used for runtime-class static first-class callables. + pub(super) fn dynamic_static_method_callable_expr(class_name: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::DynamicStaticMethodCallable { + class_name: Box::new(class_name), + method: Box::new(method), + } + } + + /// Parses an anonymous function expression into a runtime eval closure payload. + pub(super) fn parse_closure_expr(&mut self, is_static: bool) -> Result { + let source_start_line = self.current_line(); + if is_static { + self.advance(); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("", false)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let captures = self.parse_optional_closure_use_captures(¶ms)?; + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; + let (body, source_end_line) = self.parse_block_with_end_line()?; + let function = EvalFunction::new(next_closure_function_name(), params, body) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_parameter_attributes(parameter_attributes) + .with_parameter_types(parameter_types) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type); + Ok(EvalExpr::Closure { + function, + captures, + is_static, + }) + } + + /// Parses an optional closure `use (...)` capture list. + pub(super) fn parse_optional_closure_use_captures( + &mut self, + params: &[String], + ) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { + return Ok(Vec::new()); + } + self.advance(); + self.expect(TokenKind::LParen)?; + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let mut captures = Vec::new(); + loop { + let by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + if params.iter().any(|param| param == name) + || captures + .iter() + .any(|capture: &EvalClosureCapture| capture.name() == name) + { + return Err(EvalParseError::UnsupportedConstruct); + } + captures.push(EvalClosureCapture::new(name.clone(), by_ref)); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(captures) + } + + /// Parses an array literal with source-order optional key/value element expressions. + pub(in crate::parser) fn parse_array_literal(&mut self) -> Result { + self.expect(TokenKind::LBracket)?; + self.parse_array_elements_until(TokenKind::RBracket) + } + + /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. + pub(in crate::parser) fn parse_legacy_array_literal(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + self.parse_array_elements_until(TokenKind::RParen) + } + + /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. + pub(in crate::parser) fn current_starts_legacy_array_literal(&self) -> bool { + matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) + && matches!(self.peek(), TokenKind::LParen) + } + + /// Parses comma-separated array elements until the supplied closing delimiter. + pub(in crate::parser) fn parse_array_elements_until( + &mut self, + close: TokenKind, + ) -> Result { + let mut elements = Vec::new(); + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + loop { + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::Reference(value)); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + continue; + } + let first = self.parse_expr()?; + if self.consume(TokenKind::FatArrow) { + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyReference { + key: first, + value, + }); + } else { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } + } else { + elements.push(EvalArrayElement::Value(first)); + } + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + } + self.expect(close)?; + Ok(EvalExpr::Array(elements)) + } +} diff --git a/crates/elephc-magician/src/parser/expressions/postfix.rs b/crates/elephc-magician/src/parser/expressions/postfix.rs new file mode 100644 index 0000000000..baf7064f07 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/postfix.rs @@ -0,0 +1,277 @@ +//! Purpose: +//! Parses `instanceof`, exponentiation, and postfix property, method, index, and +//! dynamic-member expression forms. +//! +//! Called from: +//! - The unary/precedence parser and primary-expression parser. +//! +//! Key details: +//! - Postfix operations preserve PHP chaining and dynamic member evaluation order. + +use super::*; + +impl Parser { + + /// Parses left-associative `instanceof` with PHP's high operator precedence. + pub(in crate::parser) fn parse_instanceof(&mut self) -> Result { + let mut expr = self.parse_power()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "instanceof")) { + self.advance(); + let target = self.parse_instanceof_target()?; + expr = EvalExpr::InstanceOf { + value: Box::new(expr), + target, + }; + } + Ok(expr) + } + + /// Parses a static or dynamic target after PHP's `instanceof` operator. + pub(in crate::parser) fn parse_instanceof_target( + &mut self, + ) -> Result { + if self.consume(TokenKind::LParen) { + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(expr))); + } + if matches!(self.current(), TokenKind::DollarIdent(_)) { + let target = self.parse_instanceof_variable_target()?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(target))); + } + let name = self.parse_class_reference_name(true)?; + let class_name = self.resolve_static_class_name(name); + if self.consume(TokenKind::DoubleColon) { + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let property = property.clone(); + self.advance(); + return Ok(EvalInstanceOfTarget::Expr(Box::new( + EvalExpr::StaticPropertyGet { + class_name, + property, + }, + ))); + } + Ok(EvalInstanceOfTarget::ClassName(class_name)) + } + + /// Parses PHP's unparenthesized dynamic `instanceof` variable/property/array target. + pub(in crate::parser) fn parse_instanceof_variable_target(&mut self) -> Result { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut expr = EvalExpr::LoadVar(name.clone()); + self.advance(); + loop { + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + return Err(EvalParseError::UnexpectedToken); + } + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + continue; + } + break; + } + Ok(expr) + } + + /// Parses right-associative exponentiation with higher precedence than unary prefix operators. + pub(in crate::parser) fn parse_power(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + if self.consume(TokenKind::StarStar) { + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses postfix array reads, property reads, method calls, and dynamic calls. + pub(in crate::parser) fn parse_postfix(&mut self) -> Result { + let mut expr = self.parse_primary()?; + loop { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + expr = Self::invokable_callable_expr(expr); + continue; + } + let args = self.parse_call_args()?; + expr = EvalExpr::DynamicCall { + callee: Box::new(expr), + args, + }; + continue; + } + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::DoubleColon) { + expr = self.parse_dynamic_static_member_expr(expr)?; + continue; + } + let nullsafe = if self.consume(TokenKind::Arrow) { + false + } else if self.consume(TokenKind::QuestionArrow) { + true + } else { + break; + }; + expr = self.parse_object_member_postfix(expr, nullsafe)?; + continue; + } + Ok(expr) + } + + /// Parses the member name after `->` or `?->` and builds the corresponding postfix expression. + pub(super) fn parse_object_member_postfix( + &mut self, + object: EvalExpr, + nullsafe: bool, + ) -> Result { + match self.current() { + TokenKind::Ident(member) => { + let member = member.clone(); + self.advance(); + self.parse_named_object_member_postfix(object, member, nullsafe) + } + TokenKind::DollarIdent(name) => { + let member = EvalExpr::LoadVar(name.clone()); + self.advance(); + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Builds a static-name property read or method call after parsing the member name. + pub(super) fn parse_named_object_member_postfix( + &mut self, + object: EvalExpr, + member: String, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::method_callable_expr( + object, + EvalExpr::Const(EvalConst::String(member)), + )); + } + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeMethodCall { + object: Box::new(object), + method: member, + args, + } + } else { + EvalExpr::MethodCall { + object: Box::new(object), + method: member, + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafePropertyGet { + object: Box::new(object), + property: member, + } + } else { + EvalExpr::PropertyGet { + object: Box::new(object), + property: member, + } + }) + } + + /// Builds a runtime-name property read or method call after parsing the member expression. + pub(super) fn parse_dynamic_object_member_postfix( + &mut self, + object: EvalExpr, + member: EvalExpr, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::method_callable_expr(object, member)); + } + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + } else { + EvalExpr::DynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + } else { + EvalExpr::DynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + }) + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/precedence.rs b/crates/elephc-magician/src/parser/expressions/precedence.rs new file mode 100644 index 0000000000..ae46fdf79c --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/precedence.rs @@ -0,0 +1,372 @@ +//! Purpose: +//! Parses PHP expression precedence from keyword logical operators through +//! unary operators and scalar casts. +//! +//! Called from: +//! - `Parser::parse_expr()` and the next tighter parser precedence layer. +//! +//! Key details: +//! - Ternary, coalesce, exponentiation handoff, and PHP keyword precedence are +//! kept explicit. + +use super::*; + +impl Parser { + /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. + pub(in crate::parser) fn parse_expr(&mut self) -> Result { + self.parse_keyword_or() + } + + /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. + pub(in crate::parser) fn parse_keyword_or(&mut self) -> Result { + let mut expr = self.parse_keyword_xor()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { + self.advance(); + let right = self.parse_keyword_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. + pub(in crate::parser) fn parse_keyword_xor(&mut self) -> Result { + let mut expr = self.parse_keyword_and()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { + self.advance(); + let right = self.parse_keyword_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. + pub(in crate::parser) fn parse_keyword_and(&mut self) -> Result { + let mut expr = self.parse_ternary()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { + self.advance(); + let right = self.parse_ternary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. + pub(in crate::parser) fn parse_ternary(&mut self) -> Result { + let condition = self.parse_null_coalesce()?; + if !self.consume(TokenKind::Question) { + return Ok(condition); + } + let then_branch = if self.consume(TokenKind::Colon) { + None + } else { + let expr = self.parse_expr()?; + self.expect(TokenKind::Colon)?; + Some(Box::new(expr)) + }; + let else_branch = self.parse_expr()?; + Ok(EvalExpr::Ternary { + condition: Box::new(condition), + then_branch, + else_branch: Box::new(else_branch), + }) + } + + /// Parses right-associative null coalescing below logical OR and above ternary. + pub(in crate::parser) fn parse_null_coalesce(&mut self) -> Result { + let value = self.parse_logical_or()?; + if !self.consume(TokenKind::QuestionQuestion) { + return Ok(value); + } + let default = self.parse_null_coalesce()?; + Ok(EvalExpr::NullCoalesce { + value: Box::new(value), + default: Box::new(default), + }) + } + + /// Parses left-associative logical OR with lower precedence than logical AND. + pub(in crate::parser) fn parse_logical_or(&mut self) -> Result { + let mut expr = self.parse_logical_and()?; + while self.consume(TokenKind::OrOr) { + let right = self.parse_logical_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative logical AND with lower precedence than equality. + pub(in crate::parser) fn parse_logical_and(&mut self) -> Result { + let mut expr = self.parse_bit_or()?; + while self.consume(TokenKind::AndAnd) { + let right = self.parse_bit_or()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. + pub(in crate::parser) fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + while self.consume(TokenKind::Pipe) { + let right = self.parse_bit_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. + pub(in crate::parser) fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + while self.consume(TokenKind::Caret) { + let right = self.parse_bit_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise AND with lower precedence than equality. + pub(in crate::parser) fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::Ampersand) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative equality and inequality comparisons. + pub(in crate::parser) fn parse_equality(&mut self) -> Result { + let mut expr = self.parse_ordering()?; + loop { + let op = if self.consume(TokenKind::EqualEqual) { + EvalBinOp::LooseEq + } else if self.consume(TokenKind::NotEqual) { + EvalBinOp::LooseNotEq + } else if self.consume(TokenKind::EqualEqualEqual) { + EvalBinOp::StrictEq + } else if self.consume(TokenKind::NotEqualEqual) { + EvalBinOp::StrictNotEq + } else { + break; + }; + let right = self.parse_ordering()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative ordered comparisons. + pub(in crate::parser) fn parse_ordering(&mut self) -> Result { + let mut expr = self.parse_shift()?; + loop { + let op = if self.consume(TokenKind::Less) { + EvalBinOp::Lt + } else if self.consume(TokenKind::LessEqual) { + EvalBinOp::LtEq + } else if self.consume(TokenKind::Greater) { + EvalBinOp::Gt + } else if self.consume(TokenKind::GreaterEqual) { + EvalBinOp::GtEq + } else if self.consume(TokenKind::Spaceship) { + EvalBinOp::Spaceship + } else { + break; + }; + let right = self.parse_shift()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative integer shift operators. + pub(in crate::parser) fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::LessLess) { + EvalBinOp::ShiftLeft + } else if self.consume(TokenKind::GreaterGreater) { + EvalBinOp::ShiftRight + } else { + break; + }; + let right = self.parse_concat()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative string concatenation. + pub(in crate::parser) fn parse_concat(&mut self) -> Result { + let mut expr = self.parse_add()?; + while self.consume(TokenKind::Dot) { + let right = self.parse_add()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric addition and subtraction. + pub(in crate::parser) fn parse_add(&mut self) -> Result { + let mut expr = self.parse_mul()?; + loop { + let op = if self.consume(TokenKind::Plus) { + EvalBinOp::Add + } else if self.consume(TokenKind::Minus) { + EvalBinOp::Sub + } else { + break; + }; + let right = self.parse_mul()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric multiplication, division, and modulo. + pub(in crate::parser) fn parse_mul(&mut self) -> Result { + let mut expr = self.parse_unary()?; + loop { + let op = if self.consume(TokenKind::Star) { + EvalBinOp::Mul + } else if self.consume(TokenKind::Slash) { + EvalBinOp::Div + } else if self.consume(TokenKind::Percent) { + EvalBinOp::Mod + } else { + break; + }; + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses right-associative unary prefix expressions. + pub(in crate::parser) fn parse_unary(&mut self) -> Result { + if let Some(target) = self.peek_scalar_cast_type() { + self.advance(); + self.advance(); + self.advance(); + let expr = self.parse_concat()?; + return Ok(EvalExpr::Cast { + target, + expr: Box::new(expr), + }); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "clone")) { + self.advance(); + let expr = self.parse_unary()?; + return Ok(EvalExpr::Clone(Box::new(expr))); + } + if self.consume(TokenKind::Plus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Minus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Bang) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Tilde) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(expr), + }); + } + self.parse_instanceof() + } + + /// Returns the scalar cast target represented by the current `(type)` token window. + pub(super) fn peek_scalar_cast_type(&self) -> Option { + if !matches!(self.current(), TokenKind::LParen) { + return None; + } + let Some(TokenKind::Ident(name)) = self.tokens.get(self.pos + 1) else { + return None; + }; + if !matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) { + return None; + } + if ident_eq(name, "int") || ident_eq(name, "integer") { + Some(EvalCastType::Int) + } else if ident_eq(name, "float") || ident_eq(name, "double") || ident_eq(name, "real") { + Some(EvalCastType::Float) + } else if ident_eq(name, "string") { + Some(EvalCastType::String) + } else if ident_eq(name, "bool") || ident_eq(name, "boolean") { + Some(EvalCastType::Bool) + } else { + None + } + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/primary.rs b/crates/elephc-magician/src/parser/expressions/primary.rs new file mode 100644 index 0000000000..3ee26b7924 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/primary.rs @@ -0,0 +1,220 @@ +//! Purpose: +//! Parses primary literals, variables, closures, includes, match expressions, +//! calls, and qualified-name constants. +//! +//! Called from: +//! - `Parser::parse_postfix()`. +//! +//! Key details: +//! - Token dispatch remains centralized for exhaustive primary syntax handling. + +use super::*; + +impl Parser { + + /// Parses primary expressions supported by the initial eval subset. + pub(in crate::parser) fn parse_primary(&mut self) -> Result { + match self.current() { + TokenKind::Int(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Int(value))) + } + TokenKind::Float(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Float(value))) + } + TokenKind::String(value) => { + let value = value.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(value))) + } + TokenKind::DollarIdent(name) => { + let name = name.clone(); + self.advance(); + let expr = EvalExpr::LoadVar(name); + if self.consume(TokenKind::DoubleColon) { + self.parse_dynamic_static_member_expr(expr) + } else { + Ok(expr) + } + } + TokenKind::Magic(EvalMagicConst::Namespace) => { + let namespace = self.namespace.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(namespace))) + } + TokenKind::Magic(magic) => { + let magic = magic.clone(); + self.advance(); + Ok(EvalExpr::Magic(magic)) + } + TokenKind::Ident(name) if ident_eq(name, "null") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Null)) + } + TokenKind::Ident(name) if ident_eq(name, "true") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(true))) + } + TokenKind::Ident(name) if ident_eq(name, "false") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(false))) + } + TokenKind::Ident(name) if ident_eq(name, "print") => { + self.advance(); + let expr = self.parse_expr()?; + Ok(EvalExpr::Print(Box::new(expr))) + } + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_closure_expr(false), + TokenKind::Ident(name) + if ident_eq(name, "static") + && matches!(self.peek(), TokenKind::Ident(next) if ident_eq(next, "function")) => + { + self.parse_closure_expr(true) + } + TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { + self.parse_legacy_array_literal() + } + TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), + TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), + TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), + TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Backslash => self.parse_qualified_name_expr(), + TokenKind::Ident(_) + if matches!(self.peek(), TokenKind::Backslash | TokenKind::DoubleColon) => + { + self.parse_qualified_name_expr() + } + TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { + self.parse_call_expr(name.clone()) + } + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(self.const_fetch_expr(name)) + } + TokenKind::LBracket => self.parse_array_literal(), + TokenKind::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + Ok(expr) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses PHP include/require expression constructs and their path expression. + pub(in crate::parser) fn parse_include_expr(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let required = ident_eq(name, "require") || ident_eq(name, "require_once"); + let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); + self.advance(); + let path = if self.consume(TokenKind::LParen) { + let path = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + path + } else { + self.parse_expr()? + }; + Ok(EvalExpr::Include { + path: Box::new(path), + required, + once, + }) + } + + /// Parses `match (expr) { pattern, other => value, default => fallback }`. + pub(in crate::parser) fn parse_match_expr(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let subject = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + + let mut arms = Vec::new(); + let mut default = None; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + self.expect(TokenKind::FatArrow)?; + default = Some(Box::new(self.parse_expr()?)); + } else { + arms.push(self.parse_match_arm()?); + } + if self.consume(TokenKind::Comma) { + continue; + } + self.expect(TokenKind::RBrace)?; + break; + } + + Ok(EvalExpr::Match { + subject: Box::new(subject), + arms, + default, + }) + } + + /// Parses one non-default `match` arm and its comma-separated pattern list. + pub(in crate::parser) fn parse_match_arm(&mut self) -> Result { + let mut patterns = Vec::new(); + loop { + patterns.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::FatArrow) { + return Err(EvalParseError::UnexpectedToken); + } + if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + } + self.expect(TokenKind::FatArrow)?; + let value = self.parse_expr()?; + Ok(EvalMatchArm { patterns, value }) + } + + /// Parses a function-like call expression and its source-order arguments. + pub(in crate::parser) fn parse_call_expr(&mut self, name: String) -> Result { + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(self.function_callable_expr(name)); + } + let args = self.parse_call_args()?; + Ok(self.call_expr(name, args)) + } + + /// Parses an explicitly qualified call or constant-fetch expression. + pub(in crate::parser) fn parse_qualified_name_expr(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + let class_name = self.resolve_static_class_name(name); + return self.parse_static_member_expr(class_name); + } + let name = self.resolve_qualified_name(name); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::function_callable_value(name.to_ascii_lowercase(), None)); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }); + } + Ok(EvalExpr::ConstFetch(name)) + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/static_names.rs b/crates/elephc-magician/src/parser/expressions/static_names.rs new file mode 100644 index 0000000000..a19c3c33c5 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/static_names.rs @@ -0,0 +1,377 @@ +//! Purpose: +//! Parses static member and object-construction expressions and resolves PHP +//! class, function, and constant names. +//! +//! Called from: +//! - Primary and postfix parsing for qualified names, `new`, and `Class::member`. +//! +//! Key details: +//! - Namespace/import fallback and reserved class-reference rules stay shared. + +use super::*; + +impl Parser { + + /// Parses `Class::$property` and `Class::method(...)` expressions. + pub(in crate::parser) fn parse_static_member_expr( + &mut self, + class_name: String, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(property) => { + let property = property.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::LoadVar(property)), + }); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(EvalExpr::LoadVar(property)), + args, + }); + } + Ok(EvalExpr::StaticPropertyGet { + class_name, + property, + }) + } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + property: Box::new(property), + }) + } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::ClassNameFetch { class_name }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.clone(); + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + }); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::StaticMethodCall { + class_name, + method, + args, + }) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(member), + }); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(member), + args, + }); + } + Ok(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + constant: Box::new(member), + }) + } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::ClassConstantFetch { + class_name, + constant, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses `$class::member` expressions whose static receiver is runtime-valued. + pub(super) fn parse_dynamic_static_member_expr( + &mut self, + class_name: EvalExpr, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(member) => { + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr( + class_name, + EvalExpr::LoadVar(member), + )); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::LoadVar(member)), + args, + }); + } + Ok(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name), + property: member, + }) + } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name), + property: Box::new(property), + }) + } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(class_name), + }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.clone(); + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr( + class_name, + EvalExpr::Const(EvalConst::String(method)), + )); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + args, + }) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr(class_name, member)); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(member), + args, + }); + } + Ok(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(class_name), + constant: Box::new(member), + }) + } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(class_name), + constant, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses `new ClassName(...)` and anonymous `new class {}` expressions in eval fragments. + pub(in crate::parser) fn parse_new_object_expr(&mut self) -> Result { + self.advance(); + let is_readonly_anonymous = matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "readonly")) + && matches!(self.peek(), TokenKind::Ident(name) if ident_eq(name, "class")); + if is_readonly_anonymous { + self.advance(); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return self.parse_anonymous_class_expr(is_readonly_anonymous); + } + if let TokenKind::DollarIdent(name) = self.current() { + let class_name = EvalExpr::LoadVar(name.clone()); + self.advance(); + let args = self.parse_optional_constructor_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } + if self.consume(TokenKind::LParen) { + let class_name = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let args = self.parse_optional_constructor_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } + let class_name = self.parse_class_reference_name(true)?; + let class_name = self.resolve_static_class_name(class_name); + let args = self.parse_optional_constructor_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + + /// Parses an optional constructor argument list after `new` class targets. + pub(super) fn parse_optional_constructor_args(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args() + } else { + Ok(Vec::new()) + } + } + + /// Parses a simple or explicitly qualified PHP name. + pub(in crate::parser) fn parse_qualified_name(&mut self) -> Result { + let absolute = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok(ParsedQualifiedName { name, absolute }) + } + + /// Parses a class-like reference name while rejecting PHP-reserved unqualified names. + pub(in crate::parser) fn parse_class_reference_name( + &mut self, + allow_relative_keywords: bool, + ) -> Result { + let name = self.parse_qualified_name()?; + if self.class_reference_name_is_reserved(&name, allow_relative_keywords) { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(name) + } + + /// Returns whether a parsed class-like reference uses a PHP-reserved bare name. + pub(in crate::parser) fn class_reference_name_is_reserved( + &self, + name: &ParsedQualifiedName, + allow_relative_keywords: bool, + ) -> bool { + if name.absolute || name.name.contains('\\') { + return false; + } + if allow_relative_keywords + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return false; + } + is_reserved_class_like_name(&name.name) + } + + /// Resolves a class name used before `::`, preserving PHP relative class keywords. + pub(in crate::parser) fn resolve_static_class_name(&self, name: ParsedQualifiedName) -> String { + if !name.absolute + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return name.name.to_ascii_lowercase(); + } + self.resolve_class_name(name) + } + + /// Builds a call expression, adding namespace fallback for unqualified names. + pub(in crate::parser) fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return EvalExpr::Call { + name: imported.to_ascii_lowercase(), + args, + }; + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + EvalExpr::Call { + name: fallback_name, + args, + } + } else { + EvalExpr::NamespacedCall { + name: self + .qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + fallback_name, + args, + } + } + } + + /// Builds a constant fetch expression, adding namespace fallback for unqualified names. + pub(in crate::parser) fn const_fetch_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_constant(&name) { + return EvalExpr::ConstFetch(imported.to_string()); + } + if self.namespace.is_empty() { + EvalExpr::ConstFetch(name) + } else { + EvalExpr::NamespacedConstFetch { + name: self.qualify_name_in_current_namespace(&name), + fallback_name: name, + } + } + } + + /// Prefixes a name with the parser's current namespace when one is active. + pub(in crate::parser) fn qualify_name_in_current_namespace(&self, name: &str) -> String { + if self.namespace.is_empty() { + name.to_string() + } else { + format!("{}\\{}", self.namespace, name) + } + } + + /// Resolves a class name through active imports before namespace qualification. + pub(in crate::parser) fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute { + return name.name; + } + if let Some(imported) = self.imports.resolve_class(&name.name) { + return imported; + } + self.resolve_qualified_name(name) + } + + /// Resolves a parsed PHP name according to the current namespace. + pub(in crate::parser) fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute || self.namespace.is_empty() { + name.name + } else { + self.qualify_name_in_current_namespace(&name.name) + } + } + +} diff --git a/crates/elephc-magician/src/parser/mod.rs b/crates/elephc-magician/src/parser/mod.rs new file mode 100644 index 0000000000..ab9bda44fa --- /dev/null +++ b/crates/elephc-magician/src/parser/mod.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Parses runtime PHP eval fragments into EvalIR statement form. +//! The module entry point validates fragment boundaries, delegates tokenization, +//! and hands tokens to focused parser state. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` +//! - `crate::interpreter` tests and nested eval execution paths. +//! +//! Key details: +//! - PHP eval fragments are statement fragments and must not include opening +//! ` Result { + if contains_php_open_tag(code) { + return Err(EvalParseError::PhpOpenTag); + } + let source = std::str::from_utf8(code).map_err(|_| EvalParseError::InvalidUtf8)?; + let tokens = tokenize(source)?; + Parser::new(tokens, code.len()).parse_program() +} + +/// Returns true when a fragment contains a PHP opening tag sequence. +fn contains_php_open_tag(code: &[u8]) -> bool { + code.windows(2).any(|window| window == b", + pub(super) token_lines: Vec, + pub(super) pos: usize, + pub(super) source_len: usize, + pub(super) namespace: String, + pub(super) imports: NamespaceImports, + pub(super) allow_use_imports: bool, +} + +/// A parsed PHP name plus whether it used a leading global namespace separator. +pub(super) struct ParsedQualifiedName { + pub(super) name: String, + pub(super) absolute: bool, +} + +/// Import alias tables active for the current namespace declaration region. +#[derive(Default)] +pub(super) struct NamespaceImports { + classes: HashMap, + functions: HashMap, + constants: HashMap, +} + +/// The `use` declaration namespace being imported. +#[derive(Copy, Clone, Eq, PartialEq)] +pub(super) enum UseImportKind { + Class, + Function, + Const, +} + +/// Returns a parser-global synthetic class name for one eval anonymous class expression. +pub(super) fn next_anonymous_class_name() -> String { + let id = ANONYMOUS_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("class@anonymous#eval{id}") +} + +/// Returns a parser-global synthetic function name for one eval closure expression. +pub(super) fn next_closure_function_name() -> String { + let id = CLOSURE_FUNCTION_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("{{closure:eval:function:{id}}}") +} + +impl NamespaceImports { + /// Stores one class import under PHP's case-insensitive class alias key. + pub(super) fn insert_class(&mut self, alias: String, name: String) { + self.classes.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one function import under PHP's case-insensitive function alias key. + pub(super) fn insert_function(&mut self, alias: String, name: String) { + self.functions.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one constant import under PHP's case-sensitive constant alias key. + pub(super) fn insert_constant(&mut self, alias: String, name: String) { + self.constants.insert(alias, name); + } + + /// Resolves a class import, including aliases used as the first segment of a class name. + pub(super) fn resolve_class(&self, name: &str) -> Option { + let (first, tail) = split_first_name_segment(name); + let imported = self.classes.get(&first.to_ascii_lowercase())?; + Some(match tail { + Some(tail) => format!("{imported}\\{tail}"), + None => imported.clone(), + }) + } + + /// Resolves an unqualified function alias. + pub(super) fn resolve_function(&self, name: &str) -> Option<&str> { + self.functions + .get(&name.to_ascii_lowercase()) + .map(String::as_str) + } + + /// Resolves a case-sensitive unqualified constant alias. + pub(super) fn resolve_constant(&self, name: &str) -> Option<&str> { + self.constants.get(name).map(String::as_str) + } +} + +impl Parser { + /// Creates a parser over tokens produced from a source fragment. + pub(super) fn new(tokens: Vec, source_len: usize) -> Self { + let token_lines = tokens.iter().map(Token::line).collect(); + let tokens = tokens.into_iter().map(Token::into_kind).collect(); + Self { + tokens, + token_lines, + pos: 0, + source_len, + namespace: String::new(), + imports: NamespaceImports::default(), + allow_use_imports: true, + } + } + + /// Parses a complete eval fragment until EOF. + pub(super) fn parse_program(mut self) -> Result { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::Eof) { + statements.extend(self.parse_stmt()?); + } + Ok(EvalProgram::new(self.source_len, statements)) + } +} diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs new file mode 100644 index 0000000000..753d4bd75d --- /dev/null +++ b/crates/elephc-magician/src/parser/statements.rs @@ -0,0 +1,410 @@ +//! Purpose: +//! Dispatches PHP eval statements and owns shared parser statement metadata. +//! Syntax families and analysis helpers live in focused child modules. +//! +//! Called from: +//! - `crate::parser::state::Parser::parse_program()`. +//! +//! Key details: +//! - Statement parsing expands multi-variable constructs such as `unset($a, $b)` into multiple EvalIR statements. +//! - Namespace/use parsing lives here because declarations are statement-level syntax in PHP. + +mod assignments; +mod class_declarations; +mod class_members; +mod control_flow; +mod default_validation; +mod enums; +mod functions_namespaces; +mod globals_exceptions; +mod interfaces; +mod loops; +mod parameters_types; +mod property_builders; +mod property_hook_analysis; +mod traits; + +use super::cursor::*; +use super::state::*; +use crate::errors::EvalParseError; +use crate::eval_ir::{ + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, + EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, + EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, + EvalSourceLocation, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, + EvalVisibility, +}; +use crate::lexer::TokenKind; + +use default_validation::*; +use property_builders::*; +use property_hook_analysis::*; + +/// Parsed method parameters plus constructor-promotion side products. +pub(super) struct ParsedMethodParams { + pub(super) params: Vec, + pub(super) parameter_attributes: Vec>, + pub(super) parameter_types: Vec>, + pub(super) parameter_defaults: Vec>, + pub(super) parameter_is_by_ref: Vec, + pub(super) parameter_is_variadic: Vec, + pub(super) promoted_properties: Vec, + pub(super) promoted_assignments: Vec, +} + +/// Class-body members collected while parsing a named or anonymous eval class. +struct ParsedClassBody { + source_end_line: i64, + constants: Vec, + properties: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, +} + +/// Type-declaration position controls PHP-only atoms such as `void`, `static`, and `callable`. +#[derive(Clone, Copy)] +pub(super) enum EvalTypePosition { + FunctionParameter, + MethodParameter, + Property, + FunctionReturn, + MethodReturn, +} + +impl Parser { + /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. + pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::AttributeStart) { + return self.parse_attributed_stmt(); + } + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "break") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Break]) + } + TokenKind::Ident(name) if ident_eq(name, "continue") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Continue]) + } + TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), + TokenKind::Ident(name) if ident_eq(name, "echo") => { + self.advance(); + let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; + while self.consume(TokenKind::Comma) { + statements.push(EvalStmt::Echo(self.parse_expr()?)); + } + self.expect_semicolon()?; + Ok(statements) + } + TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), + TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) + if ident_eq(name, "abstract") + || ident_eq(name, "final") + || ident_eq(name, "readonly") => + { + self.parse_class_decl_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "enum") => self.parse_enum_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), + TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "interface") => { + self.parse_interface_decl_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), + TokenKind::Ident(name) if ident_eq(name, "return") => { + self.advance(); + if self.consume_semicolon() { + return Ok(vec![EvalStmt::Return(None)]); + } + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Return(Some(expr))]) + } + TokenKind::Ident(name) + if ident_eq(name, "static") && self.current_starts_static_property_assignment() => + { + self.parse_static_property_set_stmt(true) + } + TokenKind::Ident(name) + if ident_eq(name, "static") && !matches!(self.peek(), TokenKind::DoubleColon) => + { + self.parse_static_var_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), + TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), + TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), + TokenKind::Ident(name) if ident_eq(name, "trait") => self.parse_trait_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), + TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { + self.parse_use_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "use") => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), + TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_assignment() => + { + self.parse_static_property_set_stmt(true) + } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_postfix_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(false, true) + } + TokenKind::DollarIdent(_) if self.current_starts_dynamic_static_property_assignment() => { + self.parse_dynamic_static_property_set_stmt(true) + } + TokenKind::DollarIdent(_) + if self.current_starts_dynamic_static_property_postfix_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(false, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_static_property_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(true, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_dynamic_static_property_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(true, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_property_inc_dec() => + { + self.parse_prefixed_property_inc_dec_stmt(true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => { + self.parse_prefix_inc_dec_stmt(true) + } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(true) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_stmt(name.clone()) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), true) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, true) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => { + let expr = self.parse_expr()?; + self.parse_property_like_stmt_tail(expr, true) + } + } + } + + /// Parses one declaration preceded by PHP attribute groups. + pub(super) fn parse_attributed_stmt(&mut self) -> Result, EvalParseError> { + let attributes = self.parse_attribute_groups()?; + match self.current() { + TokenKind::Ident(name) + if ident_eq(name, "abstract") + || ident_eq(name, "final") + || ident_eq(name, "readonly") + || ident_eq(name, "class") => + { + self.parse_class_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "enum") => { + self.parse_enum_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "interface") => { + self.parse_interface_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "trait") => { + self.parse_trait_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "function") => { + self.parse_function_decl_stmt_with_attributes(attributes) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses one or more PHP `#[...]` attribute groups. + pub(super) fn parse_attribute_groups(&mut self) -> Result, EvalParseError> { + let mut attributes = Vec::new(); + while self.consume(TokenKind::AttributeStart) { + loop { + attributes.push(self.parse_attribute()?); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RBracket)?; + } + Ok(attributes) + } + + /// Parses one attribute name and optional literal positional/named arguments. + pub(super) fn parse_attribute(&mut self) -> Result { + let name = self.parse_qualified_name()?; + let name = self.resolve_class_name(name); + let args = if self.consume(TokenKind::LParen) { + let mut args = Vec::new(); + let mut supported = true; + if !self.consume(TokenKind::RParen) { + loop { + let call_arg = self.parse_call_arg()?; + if supported { + if call_arg.is_spread() { + supported = false; + } else if let Some(arg) = eval_attribute_arg_from_expr(call_arg.value()) { + args.push(match call_arg.name() { + Some(name) => EvalAttributeArg::Named { + name: name.to_string(), + value: Box::new(arg), + }, + None => arg, + }); + } else { + supported = false; + } + } + if self.consume(TokenKind::RParen) { + break; + } + self.expect(TokenKind::Comma)?; + } + } + supported.then_some(args) + } else { + Some(Vec::new()) + }; + Ok(EvalAttribute::new(name, args)) + } + + /// Returns true when current tokens form direct or indexed `Class::$property` assignment. + pub(super) fn current_starts_static_property_assignment(&self) -> bool { + self.static_property_tokens_end(self.pos) + .is_some_and(|pos| self.static_property_assignment_suffix_starts(pos)) + } + + /// Returns true when current tokens form direct or indexed `$class::$property` assignment. + pub(super) fn current_starts_dynamic_static_property_assignment(&self) -> bool { + matches!(self.current(), TokenKind::DollarIdent(_)) + && matches!(self.peek(), TokenKind::DoubleColon) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DollarIdent(_))) + && self.static_property_assignment_suffix_starts(self.pos + 3) + } + + /// Returns true when the current tokens form `Class::$property++` or `Class::$property--`. + pub(super) fn current_starts_static_property_postfix_inc_dec(&self) -> bool { + self.static_property_tokens_end(self.pos).is_some_and(|pos| { + matches!( + self.tokens.get(pos), + Some(TokenKind::PlusPlus | TokenKind::MinusMinus) + ) + }) + } + + /// Returns true when the current tokens form `$class::$property++` or `$class::$property--`. + pub(super) fn current_starts_dynamic_static_property_postfix_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::DollarIdent(_)) + && matches!(self.peek(), TokenKind::DoubleColon) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DollarIdent(_))) + && matches!( + self.tokens.get(self.pos + 3), + Some(TokenKind::PlusPlus | TokenKind::MinusMinus) + ) + } + + /// Returns true when the current tokens form `++Class::$property` or `--Class::$property`. + pub(super) fn current_starts_prefixed_static_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && self.static_property_tokens_end(self.pos + 1).is_some() + } + + /// Returns true when the current tokens form `++$class::$property` or `--$class::$property`. + pub(super) fn current_starts_prefixed_dynamic_static_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::DollarIdent(_))) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DoubleColon)) + && matches!(self.tokens.get(self.pos + 3), Some(TokenKind::DollarIdent(_))) + } + + /// Returns true when the current tokens form `++$object->property` or `--$object->property`. + pub(super) fn current_starts_prefixed_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::DollarIdent(_))) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::Arrow)) + } + + /// Returns the token position after `Class::$property` when present at `pos`. + fn static_property_tokens_end(&self, mut pos: usize) -> Option { + if matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return None; + } + pos += 1; + while matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return None; + } + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::DoubleColon)) { + return None; + } + pos += 1; + if !matches!(self.tokens.get(pos), Some(TokenKind::DollarIdent(_))) { + return None; + } + Some(pos + 1) + } + + /// Returns true when tokens after a static property form direct or indexed assignment. + fn static_property_assignment_suffix_starts(&self, pos: usize) -> bool { + self.tokens + .get(pos) + .is_some_and(|token| assignment_op(token).is_some()) + || self.static_property_array_assignment_suffix_starts(pos) + } + + /// Returns true when tokens at `pos` form `[expr] ` or `[] =`. + fn static_property_array_assignment_suffix_starts(&self, pos: usize) -> bool { + if !matches!(self.tokens.get(pos), Some(TokenKind::LBracket)) { + return false; + } + let mut cursor = pos; + let mut depth = 0usize; + loop { + match self.tokens.get(cursor) { + Some(TokenKind::LBracket) => depth += 1, + Some(TokenKind::RBracket) => { + depth = depth.saturating_sub(1); + if depth == 0 { + return self + .tokens + .get(cursor + 1) + .is_some_and(|token| assignment_op(token).is_some()); + } + } + Some(TokenKind::Eof) | None => return false, + _ => {} + } + cursor += 1; + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/assignments.rs b/crates/elephc-magician/src/parser/statements/assignments.rs new file mode 100644 index 0000000000..d42e0e0227 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/assignments.rs @@ -0,0 +1,600 @@ +//! Purpose: +//! Parses for clauses, variable stores, property/static assignments, and increment/decrement forms. +//! +//! Called from: +//! - Statement dispatch and for-loop clause parsing. +//! +//! Key details: +//! - Compound assignment and property targets lower directly into explicit EvalIR statement variants. + +use super::*; + +impl Parser { + /// Parses the optional first clause of a `for` loop. + pub(in crate::parser) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Semicolon) { + return Ok(Vec::new()); + } + self.parse_for_clause_stmt() + } + + /// Parses the optional update clause of a `for` loop. + pub(in crate::parser) fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let statements = self.parse_for_clause_stmt()?; + self.expect(TokenKind::RParen)?; + Ok(statements) + } + + /// Parses one statement-like `for` clause without consuming a delimiter. + pub(in crate::parser) fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_static_property_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_dynamic_static_property_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_property_inc_dec() => + { + self.parse_prefixed_property_inc_dec_stmt(false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => { + self.parse_prefix_inc_dec_stmt(false) + } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_postfix_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(false, false) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_clause(name.clone()) + } + TokenKind::DollarIdent(_) + if self.current_starts_dynamic_static_property_postfix_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(false, false) + } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(false) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), false) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, false) + } + _ => { + let expr = self.parse_expr()?; + self.parse_property_like_stmt_tail(expr, false) + } + } + } + + /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. + pub(in crate::parser) fn parse_array_set_clause( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `$name = expr` and simple variable compound assignments. + pub(in crate::parser) fn parse_var_store_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { + self.advance(); + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::ReferenceAssign { + target: name, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = assignment_value(&name, op, value); + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + + /// Parses `Class::$property = expr` and simple static-property compound assignments. + pub(in crate::parser) fn parse_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: class_name.clone(), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::StaticPropertySet { + class_name, + property, + value, + }]) + } + + /// Parses `$class::$property = expr` and compound assignments with a dynamic receiver. + pub(in crate::parser) fn parse_dynamic_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } + + /// Parses static property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::StaticPropertyIncDec { + class_name, + property, + increment, + }]) + } + + /// Parses dynamic static property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_dynamic_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::DynamicStaticPropertyIncDec { + class_name, + property, + increment, + }]) + } + + /// Parses prefix `++$name` / `--$name` and supported property-like prefix mutations. + pub(in crate::parser) fn parse_prefix_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if let TokenKind::DollarIdent(name) = self.current() { + if !matches!( + self.peek(), + TokenKind::DoubleColon + | TokenKind::Arrow + | TokenKind::QuestionArrow + | TokenKind::LBracket + ) { + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![inc_dec_store(name, increment)]); + } + } + let target = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) + } + + /// Parses postfix `$name++` and `$name--` as simple statement effects. + pub(in crate::parser) fn parse_postfix_inc_dec_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses prefix property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_prefixed_property_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let target = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) + } + + /// Parses `$object->property` as either an expression statement or property write. + pub(in crate::parser) fn parse_property_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let target = self.parse_expr()?; + self.parse_property_like_stmt_tail(target, require_semicolon) + } + + /// Parses assignment, array-write, or inc/dec tails after a parsed property-like target. + pub(super) fn parse_property_like_stmt_tail( + &mut self, + target: EvalExpr, + require_semicolon: bool, + ) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); + } + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_append_stmt(target, value).map(|stmt| vec![stmt]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_set_stmt(target, index, op, value).map(|stmt| vec![stmt]); + } + let Some(op) = assignment_op(self.current()) else { + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::Expr(target)]); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_reference_bind_stmt(target, source).map(|stmt| vec![stmt]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + match (target, op) { + (EvalExpr::ArrayGet { array, index }, op) => { + property_array_set_stmt(*array, *index, op, value).map(|stmt| vec![stmt]) + } + (EvalExpr::PropertyGet { object, property }, None) => Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]), + (EvalExpr::PropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::PropertyCompoundAssign { + object: *object, + property, + op, + value, + }]) + } + (EvalExpr::DynamicPropertyGet { object, property }, None) => { + Ok(vec![EvalStmt::DynamicPropertySet { + object: *object, + property: *property, + value, + }]) + } + (EvalExpr::DynamicPropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::DynamicPropertyCompoundAssign { + object: *object, + property: *property, + op, + value, + }]) + } + ( + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } + ( + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let property = *property; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name.clone()), + property: Box::new(property.clone()), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + }]) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/class_declarations.rs b/crates/elephc-magician/src/parser/statements/class_declarations.rs new file mode 100644 index 0000000000..e6361e2f82 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/class_declarations.rs @@ -0,0 +1,627 @@ +//! Purpose: +//! Parses class declarations, anonymous classes, modifiers, traits, constants, and member headers. +//! +//! Called from: +//! - Statement and expression parsing for class-like syntax. +//! +//! Key details: +//! - Class body collection preserves promoted-property and trait-adaptation side products. + +use super::*; + +impl Parser { + /// Parses `[abstract|final|readonly] class Name [extends Parent] [implements Iface, ...] { ... }`. + pub(in crate::parser) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_class_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a class declaration and attaches already parsed class attributes. + pub(in crate::parser) fn parse_class_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let (is_abstract, is_final, is_readonly_class) = self.parse_class_decl_modifiers()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl( + EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_source_location(source_location) + .with_attributes(attributes), + )]) + } + + /// Parses `class [(args)] [extends Parent] [implements Iface, ...] { ... }` after `new`. + pub(in crate::parser) fn parse_anonymous_class_expr( + &mut self, + is_readonly_class: bool, + ) -> Result { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let args = if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args()? + } else { + Vec::new() + }; + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); + let name = next_anonymous_class_name(); + let class = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + false, + false, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_source_location(source_location) + .with_anonymous(); + Ok(EvalExpr::NewAnonymousClass { class, args }) + } + + /// Parses and namespace-qualifies a declared class, interface, trait, or enum name. + pub(super) fn parse_class_like_decl_name(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if is_reserved_class_like_name(name) { + return Err(EvalParseError::UnsupportedConstruct); + } + let qualified_name = self.qualify_name_in_current_namespace(name); + self.advance(); + Ok(qualified_name) + } + + /// Parses members inside a class body after relation clauses. + pub(super) fn parse_class_body_members( + &mut self, + is_readonly_class: bool, + ) -> Result { + self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + return Ok(ParsedClassBody { + source_end_line, + constants, + properties, + methods, + traits, + trait_adaptations, + }); + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_class_member( + is_readonly_class, + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + } + } + + /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. + pub(in crate::parser) fn parse_class_decl_modifiers( + &mut self, + ) -> Result<(bool, bool, bool), EvalParseError> { + let mut is_abstract = false; + let mut is_final = false; + let mut is_readonly_class = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly_class { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly_class = true; + self.advance(); + } + _ => return Ok((is_abstract, is_final, is_readonly_class)), + } + } + } + + /// Parses an optional `extends Parent` class declaration clause. + pub(in crate::parser) fn parse_class_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(None); + } + self.advance(); + let parent = self.parse_class_reference_name(false)?; + Ok(Some(self.resolve_class_name(parent))) + } + + /// Parses an optional `implements Iface, ...` class declaration clause. + pub(in crate::parser) fn parse_class_interface_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "implements")) { + return Ok(Vec::new()); + } + self.advance(); + let mut interfaces = Vec::new(); + loop { + let interface = self.parse_class_reference_name(false)?; + interfaces.push(self.resolve_class_name(interface)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(interfaces) + } + + /// Parses one public property or method from an eval class body. + pub(in crate::parser) fn parse_class_member( + &mut self, + is_readonly_class: bool, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + + if is_abstract && is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + + if visibility.is_none() + && !is_static + && !is_abstract + && !is_final + && !is_readonly + && set_visibility.is_none() + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), + is_readonly_class, + properties, + methods, + )?; + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + + let visibility = visibility.unwrap_or(EvalVisibility::Public); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + visibility, + set_visibility, + is_static, + is_final, + is_readonly, + is_readonly_class, + is_abstract, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } + + /// Parses PHP's legacy `var` public-property marker after the keyword is recognized. + pub(super) fn parse_legacy_var_property_member( + &mut self, + attributes: Vec, + has_other_modifiers: bool, + is_readonly_class: bool, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + if has_other_modifiers { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + EvalVisibility::Public, + None, + false, + false, + false, + is_readonly_class, + false, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } + + /// Parses optional attributes that decorate one class-like member. + pub(in crate::parser) fn parse_optional_member_attributes( + &mut self, + ) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::AttributeStart) { + self.parse_attribute_groups() + } else { + Ok(Vec::new()) + } + } + + /// Parses one eval class constant declaration, including comma-separated constants. + pub(in crate::parser) fn parse_class_const_decl( + &mut self, + visibility: EvalVisibility, + is_final: bool, + attributes: &[EvalAttribute], + ) -> Result, EvalParseError> { + self.advance(); + let mut constants = Vec::new(); + loop { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if ident_eq(name, "class") { + return Err(EvalParseError::UnsupportedConstruct); + } + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + constants.push( + EvalClassConstant::with_visibility_and_final(name, visibility, is_final, value) + .with_attributes(attributes.to_vec()), + ); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(constants) + } + + /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. + pub(in crate::parser) fn parse_class_trait_use( + &mut self, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + self.advance(); + loop { + let trait_name = self.parse_class_reference_name(false)?; + traits.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + if self.consume(TokenKind::LBrace) { + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + trait_adaptations.push(self.parse_trait_adaptation()?); + self.expect_semicolon()?; + } + self.consume_semicolon(); + Ok(()) + } else { + self.expect_semicolon() + } + } + + /// Parses one `as` or `insteadof` trait adaptation clause. + pub(in crate::parser) fn parse_trait_adaptation(&mut self) -> Result { + let (trait_name, method) = self.parse_trait_adaptation_target()?; + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "as") => { + self.advance(); + let visibility = self.parse_optional_trait_adaptation_visibility()?; + let alias = if let TokenKind::Ident(alias) = self.current() { + let alias = alias.clone(); + self.advance(); + Some(alias) + } else { + None + }; + if visibility.is_none() && alias.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::Alias { + trait_name, + method, + alias, + visibility, + }) + } + TokenKind::Ident(name) if ident_eq(name, "insteadof") => { + self.advance(); + let mut instead_of = Vec::new(); + loop { + let trait_name = self.parse_class_reference_name(false)?; + instead_of.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + if instead_of.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses the target before `as` or `insteadof`. + pub(in crate::parser) fn parse_trait_adaptation_target( + &mut self, + ) -> Result<(Option, String), EvalParseError> { + let first = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + if self.class_reference_name_is_reserved(&first, false) { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::Ident(method) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let method = method.clone(); + self.advance(); + Ok((Some(self.resolve_class_name(first)), method)) + } else { + let method = first + .name + .rsplit('\\') + .next() + .filter(|segment| !segment.is_empty()) + .ok_or(EvalParseError::UnexpectedToken)? + .to_string(); + Ok((None, method)) + } + } + + /// Parses an optional visibility modifier inside a trait `as` adaptation. + pub(in crate::parser) fn parse_optional_trait_adaptation_visibility( + &mut self, + ) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + Ok(Some(EvalVisibility::Public)) + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + Ok(Some(EvalVisibility::Protected)) + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + Ok(Some(EvalVisibility::Private)) + } + _ => Ok(None), + } + } + + /// Parses method modifiers supported by eval class declarations. + pub(in crate::parser) fn parse_class_member_modifiers( + &mut self, + ) -> Result< + ( + Option, + Option, + bool, + bool, + bool, + bool, + ), + EvalParseError, + > { + let mut visibility = None; + let mut set_visibility = None; + let mut is_static = false; + let mut is_abstract = false; + let mut is_final = false; + let mut is_readonly = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Public); + } + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Protected); + } + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Private); + } + } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => { + return Ok(( + visibility, + set_visibility, + is_static, + is_abstract, + is_final, + is_readonly, + )) + } + } + } + } + + /// Consumes a PHP asymmetric visibility `(set)` marker after a visibility keyword. + pub(super) fn consume_set_marker(&mut self) -> Result { + if !self.consume(TokenKind::LParen) { + return Ok(false); + } + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "set") => self.advance(), + _ => return Err(EvalParseError::UnsupportedConstruct), + } + self.expect(TokenKind::RParen)?; + Ok(true) + } + + /// Returns a comparable visibility rank where larger means less restrictive. + pub(super) fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/class_members.rs b/crates/elephc-magician/src/parser/statements/class_members.rs new file mode 100644 index 0000000000..e0055b540c --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/class_members.rs @@ -0,0 +1,344 @@ +//! Purpose: +//! Parses class methods, properties, property hooks, and promoted property metadata. +//! +//! Called from: +//! - Class, trait, enum, and interface member parsing. +//! +//! Key details: +//! - Hook bodies, asymmetric visibility, types, defaults, and promotion remain aligned. + +use super::*; + +impl Parser { + /// Parses one class/trait/enum method and returns constructor-promoted properties. + pub(in crate::parser) fn parse_class_method_decl( + &mut self, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + ) -> Result<(EvalClassMethod, Vec), EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name, true)?; + if !promoted_properties.is_empty() && (is_abstract || is_static) { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; + let (body, source_end_line) = if is_abstract { + let source_end_line = self.current_line(); + self.expect_semicolon()?; + (Vec::new(), source_end_line) + } else { + let (body, source_end_line) = self.parse_block_with_end_line()?; + let body = if promoted_assignments.is_empty() { + body + } else { + promoted_assignments.into_iter().chain(body).collect() + }; + (body, source_end_line) + }; + Ok(( + EvalClassMethod::with_visibility_and_modifiers( + name, + visibility, + is_static, + is_abstract, + is_final, + params, + body, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type), + promoted_properties, + )) + } + + /// Parses one property declaration, including comma-separated simple properties. + pub(in crate::parser) fn parse_class_property_decl( + &mut self, + visibility: EvalVisibility, + set_visibility: Option, + is_static: bool, + is_final: bool, + is_readonly: bool, + is_readonly_class: bool, + is_abstract: bool, + ) -> Result<(Vec, Vec), EvalParseError> { + if is_static && is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + if set_visibility.is_some() && is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + if set_visibility.is_some_and(|set_visibility| { + Self::eval_visibility_rank(set_visibility) > Self::eval_visibility_rank(visibility) + }) { + return Err(EvalParseError::UnsupportedConstruct); + } + let effective_readonly = is_readonly || (is_readonly_class && !is_static); + let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let mut properties = Vec::new(); + let mut hook_methods = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + if is_abstract || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(self.parse_expr()?) + } else { + None + }; + if is_abstract { + if is_static || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get_hook, requires_set_hook) = + self.parse_property_hook_contracts()?; + let property = EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + None, + ) + .with_type(property_type) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(requires_get_hook, requires_set_hook); + return Ok((vec![property], Vec::new())); + } + let default_is_some = default.is_some(); + if self.consume(TokenKind::Comma) { + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + continue; + } + if !properties.is_empty() { + self.expect_semicolon()?; + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + break; + } + let (has_get_hook, has_set_hook, set_hook_type, parsed_hook_methods) = self + .parse_property_hook_tail( + &name, + property_type.as_ref(), + is_static, + effective_readonly, + default_is_some, + )?; + if set_hook_type.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let is_virtual = (has_get_hook || has_set_hook) + && !property_hook_methods_use_backing_slot(&parsed_hook_methods, &name); + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_hook_type(set_hook_type) + .with_set_visibility(set_visibility) + .with_hooks(has_get_hook, has_set_hook) + .with_virtual(is_virtual), + ); + hook_methods.extend(parsed_hook_methods); + break; + } + Ok((properties, hook_methods)) + } + + /// Parses `;` or a concrete eval property hook block after one property declaration. + pub(in crate::parser) fn parse_property_hook_tail( + &mut self, + property_name: &str, + property_type: Option<&EvalParameterType>, + is_static: bool, + is_readonly: bool, + has_default: bool, + ) -> Result<(bool, bool, Option, Vec), EvalParseError> { + if self.consume(TokenKind::Semicolon) { + return Ok((false, false, None, Vec::new())); + } + if !matches!(self.current(), TokenKind::LBrace) { + return Err(EvalParseError::UnexpectedToken); + } + if is_static || is_readonly || has_default { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let mut has_get_hook = false; + let mut has_set_hook = false; + let mut set_hook_type = None; + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + let (is_get, hook_set_type, method) = + self.parse_property_hook_decl(property_name, property_type)?; + if is_get { + if has_get_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_get_hook = true; + } else { + if has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_set_hook = true; + set_hook_type = hook_set_type; + } + methods.push(method); + } + if !has_get_hook && !has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((has_get_hook, has_set_hook, set_hook_type, methods)) + } + + /// Parses one concrete `get` or `set` property hook declaration. + pub(in crate::parser) fn parse_property_hook_decl( + &mut self, + property_name: &str, + property_type: Option<&EvalParameterType>, + ) -> Result<(bool, Option, EvalClassMethod), EvalParseError> { + let returns_by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let (params, set_hook_type) = if is_set { + let (param, set_hook_type, has_explicit_param) = + self.parse_property_set_hook_param()?; + if has_explicit_param && set_hook_type.is_none() && property_type.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + (vec![param], set_hook_type) + } else { + (Vec::new(), None) + }; + let (body, source_end_line) = match self.current() { + TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), + TokenKind::FatArrow => { + self.advance(); + let expr = self.parse_expr()?; + let source_end_line = self.current_line(); + self.expect_semicolon()?; + let body = if is_get { + vec![EvalStmt::Return(Some(expr))] + } else { + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: property_name.to_string(), + value: expr, + }] + }; + (body, source_end_line) + } + TokenKind::LBrace => self.parse_block_with_end_line()?, + _ => return Err(EvalParseError::UnexpectedToken), + }; + let method_name = if is_get { + property_hook_get_method(property_name) + } else { + property_hook_set_method(property_name) + }; + let mut method = EvalClassMethod::with_visibility_and_modifiers( + method_name, + EvalVisibility::Public, + false, + false, + false, + params, + body, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)); + if is_set { + method = method.with_parameter_types(vec![ + set_hook_type.clone().or_else(|| property_type.cloned()), + ]); + } + Ok((is_get, set_hook_type, method)) + } + + /// Parses an optional set-hook parameter list and returns the value variable metadata. + pub(in crate::parser) fn parse_property_set_hook_param( + &mut self, + ) -> Result<(String, Option, bool), EvalParseError> { + if !self.consume(TokenKind::LParen) { + return Ok(("value".to_string(), None, false)); + } + let set_hook_type = self.parse_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::RParen)?; + Ok((name, set_hook_type, true)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/control_flow.rs b/crates/elephc-magician/src/parser/statements/control_flow.rs new file mode 100644 index 0000000000..d04cf28312 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/control_flow.rs @@ -0,0 +1,242 @@ +//! Purpose: +//! Parses if, switch, unset, while, and nested statement blocks. +//! +//! Called from: +//! - Statement dispatch and every construct that owns a nested body. +//! +//! Key details: +//! - Alternative PHP syntax and source-end lines are preserved in block parsing. + +use super::*; + +impl Parser { + /// Parses a complete `if` statement after consuming the `if` keyword. + pub(in crate::parser) fn parse_if_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } + + /// Parses the condition, then block, and optional else branch for an `if` chain. + pub(in crate::parser) fn parse_if_after_keyword(&mut self) -> Result { + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let then_branch = self.parse_statement_body()?; + let else_branch = self.parse_optional_else_branch()?; + Ok(EvalStmt::If { + condition, + then_branch, + else_branch, + }) + } + + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. + pub(in crate::parser) fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { + self.advance(); + return Ok(vec![self.parse_if_after_keyword()?]); + } + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { + return Ok(Vec::new()); + } + self.advance(); + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } else { + self.parse_statement_body() + } + } + + /// Parses `switch (expr) { case expr: ... default: ... }`. + pub(in crate::parser) fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + cases.push(self.parse_switch_case()?); + } + self.expect(TokenKind::RBrace)?; + Ok(vec![EvalStmt::Switch { expr, cases }]) + } + + /// Parses one `case` or `default` arm inside a switch body. + pub(in crate::parser) fn parse_switch_case(&mut self) -> Result { + let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) + { + self.advance(); + let condition = self.parse_expr()?; + Some(condition) + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + None + } else { + return Err(EvalParseError::UnexpectedToken); + }; + self.expect(TokenKind::Colon)?; + let body = self.parse_switch_case_body()?; + Ok(EvalSwitchCase { condition, body }) + } + + /// Parses case body statements until the next case boundary or switch close. + pub(in crate::parser) fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { + let mut body = Vec::new(); + while !is_switch_case_boundary(self.current()) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + body.extend(self.parse_stmt()?); + } + Ok(body) + } + + /// Parses `unset($name[, ...]);` with variable, array-access, and property operands. + pub(in crate::parser) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut statements = Vec::new(); + loop { + let target = self.parse_expr()?; + let stmt = match target { + EvalExpr::ArrayGet { array, index } => EvalStmt::UnsetArrayElement { + array: *array, + index: *index, + }, + EvalExpr::LoadVar(name) => EvalStmt::UnsetVar { name }, + EvalExpr::PropertyGet { object, property } => EvalStmt::UnsetProperty { + object: *object, + property, + }, + EvalExpr::DynamicPropertyGet { object, property } => { + EvalStmt::UnsetDynamicProperty { + object: *object, + property: *property, + } + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetStaticProperty { + class_name, + property, + }, + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticProperty { + class_name: *class_name, + property, + }, + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticPropertyName { + class_name: *class_name, + property: *property, + }, + _ => return Err(EvalParseError::ExpectedVariable), + }; + statements.push(stmt); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(statements) + } + + /// Parses `while (expr) { ... }`. + pub(in crate::parser) fn parse_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::While { condition, body }]) + } + + /// Parses either a brace-delimited block or one braceless statement body. + pub(in crate::parser) fn parse_statement_body(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LBrace) { + self.parse_block() + } else { + self.parse_nested_stmt() + } + } + + /// Parses a brace-delimited statement block. + pub(in crate::parser) fn parse_block(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents() + } + + /// Parses a brace-delimited statement block and returns the closing-brace line. + pub(in crate::parser) fn parse_block_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents_with_end_line() + } + + /// Parses one nested statement where import declarations are not legal. + pub(in crate::parser) fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_stmt(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block while preserving active imports for name resolution. + pub(in crate::parser) fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block and returns the closing-brace line. + pub(in crate::parser) fn parse_nested_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents_with_end_line(); + self.allow_use_imports = previous; + result + } + + /// Parses statements until the closing brace for the current block. + pub(in crate::parser) fn parse_block_contents(&mut self) -> Result, EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + self.expect(TokenKind::RBrace)?; + Ok(statements) + } + + /// Parses statements until the closing brace and returns that brace's line. + pub(in crate::parser) fn parse_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + let source_end_line = self.current_line(); + self.expect(TokenKind::RBrace)?; + Ok((statements, source_end_line)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/default_validation.rs b/crates/elephc-magician/src/parser/statements/default_validation.rs new file mode 100644 index 0000000000..ab9b7d7101 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/default_validation.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Validates whether parameter and property defaults can be materialized safely. +//! +//! Called from: +//! - Parameter, method, and property declaration parsing. +//! +//! Key details: +//! - Constant expressions, arrays, calls, and class receivers are accepted conservatively. + +use super::*; + +/// Returns whether an eval method parameter default can be materialized safely. +pub(super) fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { + eval_constant_expression_default_is_supported(default) +} + +/// Returns whether an EvalIR expression is safe to retain as a method default. +pub(super) fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements.iter().all(eval_array_element_default_is_supported), + EvalExpr::Const(_) => true, + EvalExpr::Magic(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + eval_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_call_arg_default_is_supported) + } + EvalExpr::NewAnonymousClass { .. } => false, + EvalExpr::NullCoalesce { value, default } => { + eval_constant_expression_default_is_supported(value) + && eval_constant_expression_default_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_constant_expression_default_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_constant_expression_default_is_supported) + && eval_constant_expression_default_is_supported(else_branch) + } + EvalExpr::Cast { expr, .. } => eval_constant_expression_default_is_supported(expr), + EvalExpr::Unary { expr, .. } => eval_constant_expression_default_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_constant_expression_default_is_supported(left) + && eval_constant_expression_default_is_supported(right) + } + _ => false, + } +} + +/// Returns whether one object-construction argument is safe inside a method default. +pub(super) fn eval_call_arg_default_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_constant_expression_default_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +pub(super) fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_constant_expression_default_is_supported(value), + EvalArrayElement::Reference(_) => false, + EvalArrayElement::KeyValue { key, value } => { + eval_constant_expression_default_is_supported(key) + && eval_constant_expression_default_is_supported(value) + } + EvalArrayElement::KeyReference { .. } => false, + } +} + +/// Returns whether a type list contains return-only standalone atoms. +pub(super) fn type_variants_contain_standalone_return_only_atoms( + variants: &[EvalParameterTypeVariant], +) -> bool { + variants.iter().any(|variant| { + matches!( + variant, + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void + ) + }) +} + +/// Returns whether the type position accepts standalone return-only atoms. +pub(super) fn type_position_allows_return_only_atoms(position: EvalTypePosition) -> bool { + matches!( + position, + EvalTypePosition::FunctionReturn | EvalTypePosition::MethodReturn + ) +} + +/// Returns whether `self` and `parent` are legal in this type position. +pub(super) fn type_position_allows_class_scope_atoms(position: EvalTypePosition) -> bool { + !matches!( + position, + EvalTypePosition::FunctionParameter | EvalTypePosition::FunctionReturn + ) +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +pub(super) fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} diff --git a/crates/elephc-magician/src/parser/statements/enums.rs b/crates/elephc-magician/src/parser/statements/enums.rs new file mode 100644 index 0000000000..7b520ccedf --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/enums.rs @@ -0,0 +1,164 @@ +//! Purpose: +//! Parses enum declarations, backing types, cases, traits, and methods. +//! +//! Called from: +//! - Statement dispatch for attributed and plain enum declarations. +//! +//! Key details: +//! - Backed and unit cases retain source metadata and literal backing expressions. + +use super::*; + +impl Parser { + /// Parses `enum Name [: int|string] [implements Iface, ...] { ... }`. + pub(in crate::parser) fn parse_enum_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_enum_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an enum declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_enum_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let backing_type = self.parse_enum_backing_type()?; + let interfaces = self.parse_class_interface_clause()?; + self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + let mut cases = Vec::new(); + let mut constants = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_enum_member( + &mut cases, + &mut constants, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::EnumDecl( + EvalEnum::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses an optional backed-enum scalar type after the enum name. + pub(in crate::parser) fn parse_enum_backing_type( + &mut self, + ) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let backing_type = if ident_eq(name, "int") { + EvalEnumBackingType::Int + } else if ident_eq(name, "string") { + EvalEnumBackingType::String + } else { + return Err(EvalParseError::UnsupportedConstruct); + }; + self.advance(); + Ok(Some(backing_type)) + } + + /// Parses one enum case, constant, or method declaration. + pub(in crate::parser) fn parse_enum_member( + &mut self, + cases: &mut Vec, + constants: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { + cases.push(self.parse_enum_case_decl()?.with_attributes(attributes)); + return Ok(()); + } + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + if is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + if visibility.is_none() + && !is_static + && !is_final + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + false, + is_final, + )?; + if !promoted_properties.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + Err(EvalParseError::UnsupportedConstruct) + } + + /// Parses `case Name;` or `case Name = expr;` inside an eval enum body. + pub(in crate::parser) fn parse_enum_case_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let value = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalEnumCase::new(name, value)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/functions_namespaces.rs b/crates/elephc-magician/src/parser/statements/functions_namespaces.rs new file mode 100644 index 0000000000..e209d45a0e --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/functions_namespaces.rs @@ -0,0 +1,246 @@ +//! Purpose: +//! Parses function declarations, namespaces, and use imports. +//! +//! Called from: +//! - Top-level statement dispatch. +//! +//! Key details: +//! - Namespace and grouped-use resolution update parser import state before later statements. + +use super::*; + +impl Parser { + /// Parses `function name($param, ...) { ... }` declarations. + pub(in crate::parser) fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_function_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses `function name($param, ...) { ... }` declarations with attributes. + pub(in crate::parser) fn parse_function_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("", false)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; + let (body, source_end_line) = self.parse_block_with_end_line()?; + Ok(vec![EvalStmt::FunctionDecl { + name, + source_location: Some(EvalSourceLocation::new(source_start_line, source_end_line)), + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type, + body, + }]) + } + + /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. + pub(in crate::parser) fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let namespace = if self.consume(TokenKind::LBrace) { + return self.parse_namespace_block(String::new()); + } else { + self.parse_namespace_name()? + }; + if self.consume_semicolon() { + self.namespace = namespace; + self.imports = NamespaceImports::default(); + return Ok(Vec::new()); + } + self.expect(TokenKind::LBrace)?; + self.parse_namespace_block(namespace) + } + + /// Parses statements inside an already opened namespace block. + pub(in crate::parser) fn parse_namespace_block( + &mut self, + namespace: String, + ) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.namespace, namespace); + let previous_imports = std::mem::take(&mut self.imports); + let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); + let result = self.parse_block_contents(); + self.namespace = previous; + self.imports = previous_imports; + self.allow_use_imports = previous_allow_use_imports; + result + } + + /// Parses a namespace declaration name without a leading global separator. + pub(in crate::parser) fn parse_namespace_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses PHP `use`, `use function`, and `use const` import declarations. + pub(in crate::parser) fn parse_use_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let kind = self.parse_use_import_kind(); + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses an optional top-level `function` or `const` use-import kind. + pub(in crate::parser) fn parse_use_import_kind(&mut self) -> UseImportKind { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + self.advance(); + UseImportKind::Function + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + self.advance(); + UseImportKind::Const + } else { + UseImportKind::Class + } + } + + /// Parses and registers one comma-separated import entry. + pub(in crate::parser) fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let (name, grouped) = self.parse_use_name_or_group_start()?; + if grouped { + return self.parse_grouped_use_imports(kind, name); + } + self.parse_use_alias_and_register(kind, name) + } + + /// Parses a use-import name, stopping after a trailing namespace separator before `{`. + pub(in crate::parser) fn parse_use_name_or_group_start( + &mut self, + ) -> Result<(String, bool), EvalParseError> { + let _ = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + if self.consume(TokenKind::LBrace) { + return Ok((name, true)); + } + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok((name, false)) + } + + /// Parses all members inside a grouped namespace import declaration. + pub(in crate::parser) fn parse_grouped_use_imports( + &mut self, + default_kind: UseImportKind, + prefix: String, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + loop { + let kind = self.parse_grouped_use_entry_kind(default_kind)?; + let member = self.parse_grouped_use_member_name()?; + let name = join_grouped_use_name(&prefix, &member); + self.parse_use_alias_and_register(kind, name)?; + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RBrace) { + return Ok(()); + } + } + self.expect(TokenKind::RBrace) + } + + /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. + pub(in crate::parser) fn parse_grouped_use_entry_kind( + &mut self, + default_kind: UseImportKind, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Function); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Const); + } + Ok(default_kind) + } + + /// Parses one non-absolute member name inside a grouped use declaration. + pub(in crate::parser) fn parse_grouped_use_member_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses an optional alias and stores one namespace import. + pub(in crate::parser) fn parse_use_alias_and_register( + &mut self, + kind: UseImportKind, + name: String, + ) -> Result<(), EvalParseError> { + let alias = if matches!( + self.current(), + TokenKind::Ident(keyword) if ident_eq(keyword, "as") + ) { + self.advance(); + let TokenKind::Ident(alias) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let alias = alias.clone(); + self.advance(); + alias + } else { + last_name_segment(&name).to_string() + }; + + match kind { + UseImportKind::Class => self.imports.insert_class(alias, name), + UseImportKind::Function => self.imports.insert_function(alias, name), + UseImportKind::Const => self.imports.insert_constant(alias, name), + } + Ok(()) + } +} diff --git a/crates/elephc-magician/src/parser/statements/globals_exceptions.rs b/crates/elephc-magician/src/parser/statements/globals_exceptions.rs new file mode 100644 index 0000000000..66bef6fea1 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/globals_exceptions.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Parses global/static declarations and throw/try/catch statements. +//! +//! Called from: +//! - Top-level and nested statement dispatch. +//! +//! Key details: +//! - Catch union types are canonicalized when parsed into EvalIR. + +use super::*; + +impl Parser { + /// Parses `global $name, $other;` declarations in eval fragments. + pub(in crate::parser) fn parse_global_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let mut vars = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + vars.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(vec![EvalStmt::Global { vars }]) + } + + /// Parses `static $name = expr;` declarations in eval fragments. + pub(in crate::parser) fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let init = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StaticVar { name, init }]) + } + + /// Parses `throw expr;` statements in eval fragments. + pub(in crate::parser) fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Throw(expr)]) + } + + /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. + pub(in crate::parser) fn parse_try_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + let mut catches = Vec::new(); + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { + catches.push(self.parse_catch_clause()?); + } + let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) + { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + if catches.is_empty() && finally_body.is_empty() { + return Err(EvalParseError::UnexpectedToken); + } + Ok(vec![EvalStmt::Try { + body, + catches, + finally_body, + }]) + } + + /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. + pub(in crate::parser) fn parse_catch_clause(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let class_names = self.parse_catch_types()?; + let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { + let var_name = var_name.clone(); + self.advance(); + Some(var_name) + } else { + None + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(EvalCatch { + class_names, + var_name, + body, + }) + } + + /// Parses one or more unioned catch types in source order. + pub(in crate::parser) fn parse_catch_types(&mut self) -> Result, EvalParseError> { + let class_name = self.parse_class_reference_name(false)?; + let mut class_names = vec![self.resolve_class_name(class_name)]; + while self.consume(TokenKind::Pipe) { + let class_name = self.parse_class_reference_name(false)?; + class_names.push(self.resolve_class_name(class_name)); + } + Ok(class_names) + } +} diff --git a/crates/elephc-magician/src/parser/statements/interfaces.rs b/crates/elephc-magician/src/parser/statements/interfaces.rs new file mode 100644 index 0000000000..b799661781 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/interfaces.rs @@ -0,0 +1,293 @@ +//! Purpose: +//! Parses interfaces, parents, abstract methods, properties, and hook contracts. +//! +//! Called from: +//! - Statement dispatch for attributed and plain interface declarations. +//! +//! Key details: +//! - Interface property hook requirements and types remain declarative EvalIR metadata. + +use super::*; + +impl Parser { + /// Parses `interface Name [extends Parent, ...] { function name(...); }`. + pub(in crate::parser) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_interface_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an interface declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_interface_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let parents = self.parse_interface_parent_clause()?; + self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_interface_member(&mut constants, &mut properties, &mut methods)?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + name, parents, constants, properties, methods, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses an optional `extends Parent, ...` interface declaration clause. + pub(in crate::parser) fn parse_interface_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(Vec::new()); + } + self.advance(); + let mut parents = Vec::new(); + loop { + let parent = self.parse_class_reference_name(false)?; + parents.push(self.resolve_class_name(parent)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(parents) + } + + /// Parses one eval interface constant, property contract, or method signature. + pub(in crate::parser) fn parse_interface_member( + &mut self, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let mut is_static = false; + let mut is_final = false; + let mut saw_public = false; + let mut set_visibility = None; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if saw_public { + return Err(EvalParseError::UnsupportedConstruct); + } else { + saw_public = true; + } + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); + } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => break, + } + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend(self.parse_class_const_decl( + EvalVisibility::Public, + is_final, + &attributes, + )?); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_final || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + methods.push( + self.parse_interface_method_decl_after_function_keyword(is_static)? + .with_attributes(attributes), + ); + return Ok(()); + } + if is_static || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push( + self.parse_interface_property_decl(set_visibility)? + .with_attributes(attributes), + ); + Ok(()) + } + + /// Parses one eval interface method signature after `function` has been selected. + pub(in crate::parser) fn parse_interface_method_decl_after_function_keyword( + &mut self, + is_static: bool, + ) -> Result { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name, true)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; + let source_end_line = self.current_line(); + self.expect_semicolon()?; + Ok(EvalInterfaceMethod::new(name, params) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_static(is_static) + .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type)) + } + + /// Parses one interface property hook contract. + pub(in crate::parser) fn parse_interface_property_decl( + &mut self, + set_visibility: Option, + ) -> Result { + let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if matches!(self.current(), TokenKind::Equal) { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; + if set_visibility.is_some() && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalInterfaceProperty::new(name, requires_get, requires_set) + .with_type(property_type) + .with_set_visibility(set_visibility)) + } + + /// Parses `{ get; set; }` hook contracts for an abstract or interface property. + pub(in crate::parser) fn parse_property_hook_contracts(&mut self) -> Result<(bool, bool), EvalParseError> { + self.expect(TokenKind::LBrace)?; + let mut requires_get = false; + let mut requires_set = false; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + let returns_by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + if matches!( + self.current(), + TokenKind::LParen | TokenKind::FatArrow | TokenKind::LBrace + ) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.expect_semicolon()?; + if is_get { + if requires_get { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_get = true; + } else { + if requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_set = true; + } + } + if !requires_get && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((requires_get, requires_set)) + } + + /// Parses `{ get; set; }` hook contracts for an interface property. + pub(in crate::parser) fn parse_interface_property_hook_contracts( + &mut self, + ) -> Result<(bool, bool), EvalParseError> { + self.parse_property_hook_contracts() + } + + /// Parses retained property type metadata before the `$property` token. + pub(in crate::parser) fn parse_optional_property_type( + &mut self, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) + } +} diff --git a/crates/elephc-magician/src/parser/statements/loops.rs b/crates/elephc-magician/src/parser/statements/loops.rs new file mode 100644 index 0000000000..429c9856d4 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/loops.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Parses do-while, for, foreach, and array-set loop-adjacent statements. +//! +//! Called from: +//! - `Parser::parse_stmt()` and for-clause parsing. +//! +//! Key details: +//! - Foreach key/value targets and statement bodies retain EvalIR source order. + +use super::*; + +impl Parser { + /// Parses `do { ... } while (expr);`. + pub(in crate::parser) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_statement_body()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::DoWhile { body, condition }]) + } + + /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. + pub(in crate::parser) fn parse_array_set_stmt( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `for (init; condition; update) { ... }`. + pub(in crate::parser) fn parse_for_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let init = self.parse_for_init_clause()?; + self.expect_semicolon()?; + let condition = if matches!(self.current(), TokenKind::Semicolon) { + None + } else { + Some(self.parse_expr()?) + }; + self.expect_semicolon()?; + let update = self.parse_for_update_clause()?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::For { + init, + condition, + update, + body, + }]) + } + + /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. + pub(in crate::parser) fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let array = self.parse_expr()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + let TokenKind::DollarIdent(value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let value_name = value_name.clone(); + self.advance(); + let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { + self.advance(); + let TokenKind::DollarIdent(next_value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let key_name = value_name; + let value_name = next_value_name.clone(); + self.advance(); + (Some(key_name), value_name) + } else { + (None, value_name) + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::Foreach { + array, + key_name, + value_name, + body, + }]) + } +} diff --git a/crates/elephc-magician/src/parser/statements/parameters_types.rs b/crates/elephc-magician/src/parser/statements/parameters_types.rs new file mode 100644 index 0000000000..e89c1b23a3 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/parameters_types.rs @@ -0,0 +1,333 @@ +//! Purpose: +//! Parses callable parameters, promoted modifiers, and PHP type declarations. +//! +//! Called from: +//! - Function, method, closure, property, and hook parsing. +//! +//! Key details: +//! - Union/intersection atoms, nullability, defaults, variadics, and promotion stay indexed together. + +use super::*; + +impl Parser { + /// Parses a method parameter list and records metadata plus promotion side effects. + pub(in crate::parser) fn parse_method_params( + &mut self, + method_name: &str, + allow_class_scope_types: bool, + ) -> Result { + let mut params = Vec::new(); + let mut parameter_attributes = Vec::new(); + let mut parameter_types = Vec::new(); + let mut parameter_defaults = Vec::new(); + let mut parameter_is_by_ref = Vec::new(); + let mut parameter_is_variadic = Vec::new(); + let mut promoted_properties = Vec::new(); + let mut promoted_assignments = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + }); + } + loop { + let attributes = self.parse_attribute_groups()?; + let promotion = self.parse_promoted_parameter_modifiers()?; + if promotion.is_some() && !method_name.eq_ignore_ascii_case("__construct") { + return Err(EvalParseError::UnsupportedConstruct); + } + let param_type = if promotion.is_some() { + self.parse_optional_promoted_property_type()? + } else { + let position = if allow_class_scope_types { + EvalTypePosition::MethodParameter + } else { + EvalTypePosition::FunctionParameter + }; + self.parse_optional_parameter_type(position)? + }; + let is_by_ref = self.consume(TokenKind::Ampersand); + let is_variadic = self.consume(TokenKind::Ellipsis); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + if promotion.is_some() && is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + if let Some((visibility, is_readonly)) = promotion { + promoted_properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name.clone(), + visibility, + false, + false, + is_readonly, + None, + ) + .with_type(param_type.clone()) + .with_promoted() + .with_attributes(attributes.clone()), + ); + promoted_assignments.push(promoted_property_assignment(name, is_by_ref)); + } + params.push(name.clone()); + parameter_attributes.push(attributes); + parameter_types.push(param_type); + parameter_is_by_ref.push(is_by_ref); + parameter_is_variadic.push(is_variadic); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + let default = self.parse_expr()?; + if !method_parameter_default_is_supported(&default) { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(default) + } else { + None + }; + parameter_defaults.push(default); + if !self.consume(TokenKind::Comma) { + break; + } + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + }) + } + + /// Parses visibility and readonly modifiers on a promoted constructor parameter. + pub(super) fn parse_promoted_parameter_modifiers( + &mut self, + ) -> Result, EvalParseError> { + let mut visibility = None; + let mut is_readonly = false; + let mut saw_modifier = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Public); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Protected); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Private); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + is_readonly = true; + self.advance(); + } + _ => break, + } + } + if saw_modifier { + Ok(Some(( + visibility.unwrap_or(EvalVisibility::Public), + is_readonly, + ))) + } else { + Ok(None) + } + } + + /// Parses a constructor-promoted parameter type using PHP property-type restrictions. + pub(super) fn parse_optional_promoted_property_type( + &mut self, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) + } + + /// Consumes a supported method parameter type and returns retained metadata. + pub(super) fn parse_optional_parameter_type( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(position) + } + + /// Consumes a supported function or method return type after `:`. + pub(in crate::parser) fn parse_optional_return_type( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + self.parse_type_decl(position) + } + + /// Parses one PHP type declaration and returns retained eval metadata. + pub(super) fn parse_type_decl( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + let nullable_shorthand = self.consume(TokenKind::Question); + if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { + return Err(EvalParseError::UnexpectedToken); + } + let first = self.parse_type_name(position)?; + let mut variants = Vec::new(); + let mut allows_null = nullable_shorthand || matches!(first, None); + if let Some(first) = first { + variants.push(first); + } + if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { + return Err(EvalParseError::UnsupportedConstruct); + } + if nullable_shorthand + && matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + while self.consume(TokenKind::Ampersand) { + let Some(variant) = self.parse_type_name(position)? else { + return Err(EvalParseError::UnsupportedConstruct); + }; + variants.push(variant); + } + if type_variants_contain_standalone_return_only_atoms(&variants) { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Some(EvalParameterType::intersection(variants))); + } + while self.consume(TokenKind::Pipe) { + match self.parse_type_name(position)? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } + } + if type_variants_contain_standalone_return_only_atoms(&variants) + && (variants.len() != 1 || allows_null) + { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(Some(EvalParameterType::new(variants, allows_null))) + } + + /// Returns whether `&` belongs to by-reference parameter storage. + pub(super) fn next_token_starts_parameter_storage(&self) -> bool { + matches!(self.peek(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis) + } + + /// Consumes one simple qualified method type name. + pub(super) fn parse_type_name( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let name = self.parse_qualified_name()?; + self.type_variant_from_name(name, position) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Converts one parsed PHP type name to retained eval metadata. + pub(super) fn type_variant_from_name( + &self, + name: ParsedQualifiedName, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if !name.absolute { + let lower = name.name.to_ascii_lowercase(); + let builtin = match lower.as_str() { + "array" => Some(EvalParameterTypeVariant::Array), + "bool" => Some(EvalParameterTypeVariant::Bool), + "callable" if matches!(position, EvalTypePosition::Property) => { + return Err(EvalParseError::UnsupportedConstruct); + } + "callable" => Some(EvalParameterTypeVariant::Callable), + "float" => Some(EvalParameterTypeVariant::Float), + "int" => Some(EvalParameterTypeVariant::Int), + "iterable" => Some(EvalParameterTypeVariant::Iterable), + "mixed" => Some(EvalParameterTypeVariant::Mixed), + "never" if type_position_allows_return_only_atoms(position) => { + Some(EvalParameterTypeVariant::Never) + } + "null" => return Ok(None), + "object" => Some(EvalParameterTypeVariant::Object), + "string" => Some(EvalParameterTypeVariant::String), + "void" if type_position_allows_return_only_atoms(position) => { + Some(EvalParameterTypeVariant::Void) + } + "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), + "static" if matches!(position, EvalTypePosition::MethodReturn) => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + "static" => return Err(EvalParseError::UnsupportedConstruct), + "self" | "parent" if !type_position_allows_class_scope_atoms(position) => { + return Err(EvalParseError::UnsupportedConstruct); + } + "self" | "parent" => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + _ => None, + }; + if let Some(builtin) = builtin { + return Ok(Some(builtin)); + } + } + Ok(Some(EvalParameterTypeVariant::Class( + self.resolve_class_name(name), + ))) + } +} diff --git a/crates/elephc-magician/src/parser/statements/property_builders.rs b/crates/elephc-magician/src/parser/statements/property_builders.rs new file mode 100644 index 0000000000..839b3a9d8d --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/property_builders.rs @@ -0,0 +1,279 @@ +//! Purpose: +//! Builds EvalIR statements and attribute values from parsed property-like syntax. +//! +//! Called from: +//! - Assignment, member, attribute, and property-hook parsing. +//! +//! Key details: +//! - Reference, inc/dec, array mutation, literals, and class-name attribute args are normalized here. + +use super::*; + +/// Builds a property by-reference binding statement from a parsed property target. +pub(super) fn property_reference_bind_stmt( + target: EvalExpr, + source: String, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyReferenceBind { + object: *object, + property, + source, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyReferenceBind { + object: *object, + property: *property, + source, + }) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: *class_name, + property, + source, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: *class_name, + property: *property, + source, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property increment/decrement statement from a parsed property target. +pub(super) fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyIncDec { + object: *object, + property, + increment, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyIncDec { + object: *object, + property: *property, + increment, + }), + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyIncDec { + class_name: *class_name, + property, + increment, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: *class_name, + property: *property, + increment, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property array append statement from a parsed property target. +pub(super) fn property_array_append_stmt(target: EvalExpr, value: EvalExpr) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArrayAppend { + object: *object, + property, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyArrayAppend { + object: *object, + property: *property, + value, + }) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: *class_name, + property, + value, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: *class_name, + property: *property, + value, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property array write statement from a parsed property target. +pub(super) fn property_array_set_stmt( + target: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArraySet { + object: *object, + property, + index, + op, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyArraySet { + object: *object, + property: *property, + index, + op, + value, + }), + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyArraySet { + class_name: *class_name, + property, + index, + op, + value, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameArraySet { + class_name: *class_name, + property: *property, + index, + op, + value, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Converts a parsed attribute argument expression into retained literal metadata. +pub(super) fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { + match expr { + EvalExpr::Const(EvalConst::String(value)) => Some(EvalAttributeArg::String(value.clone())), + EvalExpr::Const(EvalConst::Int(value)) => Some(EvalAttributeArg::Int(*value)), + EvalExpr::Const(EvalConst::Float(value)) => Some(EvalAttributeArg::Float(value.to_bits())), + EvalExpr::Const(EvalConst::Bool(value)) => Some(EvalAttributeArg::Bool(*value)), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Null), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => match expr.as_ref() { + EvalExpr::Const(EvalConst::Int(value)) => { + Some(EvalAttributeArg::Int(value.wrapping_neg())) + } + EvalExpr::Const(EvalConst::Float(value)) => { + Some(EvalAttributeArg::Float((-*value).to_bits())) + } + _ => None, + }, + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(EvalAttributeArg::String) + } + EvalExpr::Array(elements) => eval_attribute_array_arg_from_elements(elements), + _ => None, + } +} + +/// Converts an eval array literal into retained attribute metadata. +pub(super) fn eval_attribute_array_arg_from_elements( + elements: &[EvalArrayElement], +) -> Option { + elements + .iter() + .map(|element| match element { + EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), + EvalArrayElement::Reference(_) => None, + EvalArrayElement::KeyValue { key, value } => { + let value = eval_attribute_arg_from_expr(value)?; + eval_attribute_array_keyed_arg(key, value) + } + EvalArrayElement::KeyReference { .. } => None, + }) + .collect::>>() + .map(EvalAttributeArg::Array) +} + +/// Wraps an attribute array value with the PHP-normalized literal key metadata. +pub(super) fn eval_attribute_array_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { + name: name.clone(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key, + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Bool(key)) => Some(EvalAttributeArg::IntKeyed { + key: i64::from(*key), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Named { + name: String::new(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key as i64, + value: Box::new(value), + }), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_attribute_array_negated_keyed_arg(expr, value), + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(|name| EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } + _ => None, + } +} + +/// Wraps an attribute array value with a normalized negative numeric literal key. +pub(super) fn eval_attribute_array_negated_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: key.wrapping_neg(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: (-*key) as i64, + value: Box::new(value), + }), + _ => None, + } +} + +/// Returns a compile-time class-name string for named `ClassName::class` attribute args. +pub(super) fn eval_attribute_class_name_arg(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if ["self", "parent", "static"] + .iter() + .any(|special| class_name.eq_ignore_ascii_case(special)) + { + return None; + } + Some(class_name.to_string()) +} diff --git a/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs b/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs new file mode 100644 index 0000000000..b960cf8a85 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs @@ -0,0 +1,537 @@ +//! Purpose: +//! Detects property backing-slot usage inside parsed hook statements and expressions. +//! +//! Called from: +//! - Property-hook declaration parsing after hook bodies are available. +//! +//! Key details: +//! - The recursive walk covers every EvalIR statement and expression shape without executing code. + +use super::*; + +/// Returns whether any parsed property hook accessor uses its own backing slot. +pub(super) fn property_hook_methods_use_backing_slot( + hook_methods: &[EvalClassMethod], + property_name: &str, +) -> bool { + hook_methods.iter().any(|method| { + method + .body() + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) + }) +} + +/// Returns whether one statement touches `$this->{$property_name}` directly. +pub(super) fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { + match stmt { + EvalStmt::ArrayAppendVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::ArraySetVar { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Break + | EvalStmt::Continue + | EvalStmt::ClassDecl(_) + | EvalStmt::EnumDecl(_) + | EvalStmt::FunctionDecl { .. } + | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetVar { .. } => false, + EvalStmt::UnsetArrayElement { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } + EvalStmt::DoWhile { body, condition } | EvalStmt::While { condition, body } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Echo(expr) + | EvalStmt::Expr(expr) + | EvalStmt::StaticVar { init: expr, .. } + | EvalStmt::Throw(expr) => eval_expr_uses_this_property(expr, property_name), + EvalStmt::For { + init, + condition, + update, + body, + } => { + eval_stmt_list_uses_this_property(init, property_name) + || condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(update, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Foreach { array, body, .. } => { + eval_expr_uses_this_property(array, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(then_branch, property_name) + || eval_stmt_list_uses_this_property(else_branch, property_name) + } + EvalStmt::Return(expr) => expr + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)), + EvalStmt::PropertyReferenceBind { + object, property, .. + } + | EvalStmt::UnsetProperty { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalStmt::DynamicPropertyReferenceBind { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::UnsetDynamicProperty { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } + | EvalStmt::UnsetStaticProperty { .. } => false, + EvalStmt::DynamicPropertySet { + object, + property, + value, + } + | EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } + | EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + value, + .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicPropertyIncDec { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::PropertySet { + object, + property, + value, + } + | EvalStmt::PropertyArrayAppend { + object, + property, + value, + } + | EvalStmt::PropertyCompoundAssign { + object, + property, + value, + .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::PropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::PropertyIncDec { + object, property, .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalStmt::DynamicStaticPropertySet { + class_name, value, .. + } + | EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, value, .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::DynamicStaticPropertyReferenceBind { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::StaticPropertySet { value, .. } + | EvalStmt::StaticPropertyArrayAppend { value, .. } + | EvalStmt::StoreVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::StaticPropertyArraySet { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Switch { expr, cases } => { + eval_expr_uses_this_property(expr, property_name) + || cases.iter().any(|case| { + case.condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(&case.body, property_name) + }) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + eval_stmt_list_uses_this_property(body, property_name) + || catches + .iter() + .any(|catch| eval_stmt_list_uses_this_property(&catch.body, property_name)) + || eval_stmt_list_uses_this_property(finally_body, property_name) + } + } +} + +/// Returns whether any statement in a list touches `$this->{$property_name}` directly. +pub(super) fn eval_stmt_list_uses_this_property(stmts: &[EvalStmt], property_name: &str) -> bool { + stmts + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) +} + +/// Returns whether one expression touches `$this->{$property_name}` directly. +pub(super) fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { + match expr { + EvalExpr::Array(elements) => elements.iter().any(|element| match element { + EvalArrayElement::Value(value) => eval_expr_uses_this_property(value, property_name), + EvalArrayElement::Reference(value) => { + eval_expr_uses_this_property(value, property_name) + } + EvalArrayElement::KeyValue { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalArrayElement::KeyReference { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } + }), + EvalExpr::ArrayGet { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } + EvalExpr::Call { args, .. } + | EvalExpr::NamespacedCall { args, .. } + | EvalExpr::NewObject { args, .. } + | EvalExpr::StaticMethodCall { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::DynamicCall { callee, args } => { + eval_expr_uses_this_property(callee, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicNewObject { class_name, args } => { + eval_expr_uses_this_property(class_name, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicStaticMethodCall { + class_name, + method, + args, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicStaticPropertyGet { class_name, .. } + | EvalExpr::DynamicClassConstantFetch { class_name, .. } + | EvalExpr::DynamicClassNameFetch { class_name } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalExpr::DynamicClassConstantNameFetch { + class_name, + constant, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(constant, property_name) + } + EvalExpr::DynamicMethodCall { + object, + method, + args, + } + | EvalExpr::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::Const(_) + | EvalExpr::ConstFetch(_) + | EvalExpr::Closure { .. } + | EvalExpr::FunctionCallable { .. } + | EvalExpr::ClassConstantFetch { .. } + | EvalExpr::ClassNameFetch { .. } + | EvalExpr::LoadVar(_) + | EvalExpr::Magic(_) + | EvalExpr::NamespacedConstFetch { .. } + | EvalExpr::StaticPropertyGet { .. } => false, + EvalExpr::MethodCallable { object, method } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + } + EvalExpr::StaticMethodCallable { method, .. } => { + eval_expr_uses_this_property(method, property_name) + } + EvalExpr::InvokableCallable { object } => { + eval_expr_uses_this_property(object, property_name) + } + EvalExpr::DynamicStaticMethodCallable { class_name, method } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + } + EvalExpr::Include { path, .. } + | EvalExpr::Cast { expr: path, .. } + | EvalExpr::Clone(path) + | EvalExpr::Print(path) + | EvalExpr::Unary { expr: path, .. } => eval_expr_uses_this_property(path, property_name), + EvalExpr::InstanceOf { value, target } => { + eval_expr_uses_this_property(value, property_name) + || matches!( + target, + EvalInstanceOfTarget::Expr(target) + if eval_expr_uses_this_property(target, property_name) + ) + } + EvalExpr::Match { + subject, + arms, + default, + } => { + eval_expr_uses_this_property(subject, property_name) + || arms.iter().any(|arm| { + arm.patterns + .iter() + .any(|pattern| eval_expr_uses_this_property(pattern, property_name)) + || eval_expr_uses_this_property(&arm.value, property_name) + }) + || default + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + } + EvalExpr::MethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::NullsafeMethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::NewAnonymousClass { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::NullCoalesce { value, default } => { + eval_expr_uses_this_property(value, property_name) + || eval_expr_uses_this_property(default, property_name) + } + EvalExpr::PropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalExpr::NullsafePropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalExpr::DynamicPropertyGet { object, property } + | EvalExpr::NullsafeDynamicPropertyGet { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || then_branch + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_expr_uses_this_property(else_branch, property_name) + } + EvalExpr::Binary { left, right, .. } => { + eval_expr_uses_this_property(left, property_name) + || eval_expr_uses_this_property(right, property_name) + } + } +} + +/// Returns whether one object/property pair is exactly `$this->{$property_name}`. +pub(super) fn eval_is_this_property(object: &EvalExpr, property: &str, property_name: &str) -> bool { + matches!(object, EvalExpr::LoadVar(name) if name == "this") && property == property_name +} + +/// Returns whether one dynamic object/property pair is exactly `$this->{"property"}`. +pub(super) fn eval_is_this_dynamic_property( + object: &EvalExpr, + property: &EvalExpr, + property_name: &str, +) -> bool { + matches!( + (object, property), + ( + EvalExpr::LoadVar(object_name), + EvalExpr::Const(EvalConst::String(property)) + ) if object_name == "this" && property == property_name + ) +} + +/// Returns the synthetic get-hook method name for one property. +pub(super) fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +pub(super) fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} + +/// Builds the implicit constructor assignment or alias for a promoted parameter. +pub(super) fn promoted_property_assignment(name: &str, is_by_ref: bool) -> EvalStmt { + if is_by_ref { + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + source: name.to_string(), + } + } else { + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + value: EvalExpr::LoadVar(name.to_string()), + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/traits.rs b/crates/elephc-magician/src/parser/statements/traits.rs new file mode 100644 index 0000000000..241c09f309 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/traits.rs @@ -0,0 +1,153 @@ +//! Purpose: +//! Parses trait declarations and trait-specific members. +//! +//! Called from: +//! - Statement dispatch for attributed and plain trait declarations. +//! +//! Key details: +//! - Trait properties and hooks reuse class-member parsing while preserving metadata. + +use super::*; + +impl Parser { + /// Parses `trait Name { ... }` declarations into dynamic trait metadata. + pub(in crate::parser) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_trait_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a trait declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_trait_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_trait_member( + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::TraitDecl( + EvalTrait::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses one property or method from an eval trait body. + pub(in crate::parser) fn parse_trait_member( + &mut self, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + if is_abstract && is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + if visibility.is_none() + && !is_static + && !is_abstract + && !is_final + && !is_readonly + && set_visibility.is_none() + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), + false, + properties, + methods, + )?; + return Ok(()); + } + let visibility = visibility.unwrap_or(EvalVisibility::Public); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + visibility, + set_visibility, + is_static, + is_final, + is_readonly, + false, + is_abstract, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs new file mode 100644 index 0000000000..6936dbddeb --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs @@ -0,0 +1,243 @@ +//! Purpose: +//! Parser tests for array literals/access and object property, method, dynamic, +//! and nullsafe access expressions. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover postfix and aggregate expression syntax. + +use super::super::support::*; + +/// Verifies indexed array literals and reads parse as runtime array expressions. +#[test] +fn parse_fragment_accepts_indexed_array_read_source() { + let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }))] + ); +} +/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. +#[test] +fn parse_fragment_accepts_legacy_array_literal_source() { + let program = + parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + }, + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))] + ); +} +/// Verifies associative array literals preserve explicit key/value expressions. +#[test] +fn parse_fragment_accepts_assoc_array_literal_source() { + let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + } + ])))] + ); +} + +/// Verifies array literals preserve by-reference element syntax. +#[test] +fn parse_fragment_accepts_array_reference_elements_source() { + let program = parse_fragment(br#"return [&$value, "named" => &$other];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Reference(EvalExpr::LoadVar("value".to_string())), + EvalArrayElement::KeyReference { + key: EvalExpr::Const(EvalConst::String("named".to_string())), + value: EvalExpr::LoadVar("other".to_string()), + }, + ])))] + ); +} + +/// Verifies indexed array writes parse as variable-target array set statements. +#[test] +fn parse_fragment_accepts_indexed_array_write_source() { + let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArraySetVar { + name: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(1)), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies indexed array append syntax parses as a variable-target append statement. +#[test] +fn parse_fragment_accepts_indexed_array_append_source() { + let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies array append syntax is accepted inside `for` update clauses. +#[test] +fn parse_fragment_accepts_array_append_in_for_update_source() { + let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) + .expect("fragment should parse"); + let [EvalStmt::For { update, .. }] = program.statements() else { + panic!("expected for statement"); + }; + assert_eq!( + update, + &vec![EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::LoadVar("i".to_string()), + }] + ); +} +/// Verifies object property reads parse as postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_property_read_source() { + let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies property names preserve source case while keywords remain case-insensitive. +#[test] +fn parse_fragment_preserves_property_case_source() { + let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "camelName".to_string(), + }))] + ); +} +/// Verifies object method calls parse as postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_method_call_source() { + let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "Answer".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies first-class object method syntax keeps callable-capture semantics in EvalIR. +#[test] +fn parse_fragment_accepts_first_class_method_callable_source() { + let program = parse_fragment(br#"return $this->Answer(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCallable { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Answer".to_string()))), + }))] + ); +} +/// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies variable-name dynamic object method calls parse as runtime-name EvalIR calls. +#[test] +fn parse_fragment_accepts_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} +/// Verifies nullsafe object property reads parse as dedicated postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_property_read_source() { + let program = parse_fragment(br#"return $this?->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafePropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies nullsafe object method calls parse as dedicated postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_method_call_source() { + let program = parse_fragment(br#"return $this?->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "Answer".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies nullsafe braced dynamic property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this?->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies nullsafe dynamic method calls parse as runtime-name EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this?->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs new file mode 100644 index 0000000000..0abc59751a --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs @@ -0,0 +1,189 @@ +//! Purpose: +//! Parser tests for named/dynamic/anonymous object construction, clone, and +//! method call argument forms. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parenthesis-free construction and named call arguments are retained. + +use super::super::support::*; + +/// Verifies object construction parses as a named EvalIR expression. +#[test] +fn parse_fragment_accepts_new_object_source() { + let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction accepts a runtime class-name variable. +#[test] +fn parse_fragment_accepts_dynamic_new_object_source() { + let program = + parse_fragment(br#"return new $className("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} +/// Verifies object construction accepts a parenthesized runtime class-name expression. +#[test] +fn parse_fragment_accepts_expression_new_object_source() { + let program = + parse_fragment(br#"return new ($factory->className)("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("factory".to_string())), + property: "className".to_string(), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} +/// Verifies PHP constructor parentheses are optional for named and runtime class targets. +#[test] +fn parse_fragment_accepts_new_object_without_constructor_parentheses_source() { + let named = parse_fragment(br#"return new Box;"#).expect("fragment should parse"); + assert_eq!( + named.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); + + let dynamic = parse_fragment(br#"return new $className;"#).expect("fragment should parse"); + assert_eq!( + dynamic.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction accepts explicitly qualified class names. +#[test] +fn parse_fragment_accepts_qualified_new_object_source() { + let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "EvalNs\\Box".to_string(), + args: Vec::new(), + }))] + ); +} + +/// Verifies clone expressions parse as unary object expressions. +#[test] +fn parse_fragment_accepts_clone_expression_source() { + let program = parse_fragment(br#"return clone $box;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Clone(Box::new( + EvalExpr::LoadVar("box".to_string()) + ))))] + ); +} + +/// Verifies anonymous class expressions parse as executable eval class metadata. +#[test] +fn parse_fragment_accepts_anonymous_class_source() { + let program = parse_fragment( + br#"return new readonly class("Ada") extends BaseBox implements Labelled { + public string $name; + public function label() { return $this->name; } +};"#, + ) + .expect("fragment should parse"); + let [EvalStmt::Return(Some(EvalExpr::NewAnonymousClass { class, args }))] = + program.statements() + else { + panic!("expected anonymous class return"); + }; + + assert!(class.name().starts_with("class@anonymous#eval")); + assert!(class.is_anonymous()); + assert!(class.is_readonly_class()); + assert_eq!(class.parent(), Some("BaseBox")); + assert_eq!(class.interfaces(), &["Labelled".to_string()]); + assert_eq!(class.properties().len(), 1); + assert_eq!(class.methods().len(), 1); + assert_eq!( + args, + &[EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string(), + )))] + ); +} + +/// Verifies object method calls preserve source-order argument expressions. +#[test] +fn parse_fragment_accepts_method_call_args_source() { + let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "add".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + })], + }))] + ); +} +/// Verifies object method calls parse multiple argument expressions in source order. +#[test] +fn parse_fragment_accepts_method_call_multiple_args_source() { + let program = + parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), + ], + }))] + ); +} + +/// Verifies object method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_method_call_args_source() { + let program = parse_fragment(br#"return $this->label(right: "ok", left: $x);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::named( + "right", + EvalExpr::Const(EvalConst::String("ok".to_string())), + ), + EvalCallArg::named("left", EvalExpr::LoadVar("x".to_string())), + ], + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs new file mode 100644 index 0000000000..3058447988 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs @@ -0,0 +1,13 @@ +//! Purpose: +//! Organizes parser tests for arrays, object access/construction, calls, and +//! property mutations. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod arrays_access; +mod construction_calls; +mod property_mutations; diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs new file mode 100644 index 0000000000..f05f2e27e5 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs @@ -0,0 +1,264 @@ +//! Purpose: +//! Parser tests for object property assignment, references, array writes, +//! compound operations, increment/decrement, and unset. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Named and dynamic property mutations retain distinct EvalIR statements. + +use super::super::support::*; + +/// Verifies object property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_write_source() { + let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); +} + +/// Verifies object property reference bindings parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_reference_bind_source() { + let program = + parse_fragment(br#"$this->x =& $source; $this->{$name} =& $other;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicPropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + ] + ); +} + +/// Verifies object property array writes and appends parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_array_write_source() { + let program = parse_fragment(br#"$this->items[0] = "x"; $this->items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::PropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies property array compound assignment retains the indexed property target. +#[test] +fn parse_fragment_accepts_property_array_compound_assignment_source() { + let program = parse_fragment(br#"$this->items[0] += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Add), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + +/// Verifies object property increment/decrement parses as dedicated member updates. +#[test] +fn parse_fragment_accepts_property_inc_dec_source() { + let program = parse_fragment(br#"$this->x++; --$this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: true, + }, + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic object property writes parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_write_source() { + let program = parse_fragment(br#"$this->{$name} = 7;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicPropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::Int(7)), + }] + ); +} + +/// Verifies dynamic property array writes keep the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_array_write_source() { + let program = parse_fragment(br#"$this->{$name}[0] = "x"; $this->{$name}[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicPropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies object property compound assignment parses as a dedicated member update. +#[test] +fn parse_fragment_accepts_property_compound_assignment_source() { + let program = parse_fragment(br#"$this->x += 2; $this->label .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "label".to_string(), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + +/// Verifies dynamic object property compound assignment keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_compound_assignment_source() { + let program = + parse_fragment(br#"$this->{$name} += 2; $this->{$label} .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("label".to_string()), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + +/// Verifies dynamic object property increment/decrement keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_inc_dec_source() { + let program = + parse_fragment(br#"$this->{$name}++; --$this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: true, + }, + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic object property unsets parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_unset_source() { + let program = parse_fragment(br#"unset($this->{$name});"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetDynamicProperty { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }] + ); +} + +/// Verifies unsetting object-property array elements keeps the property expression target. +#[test] +fn parse_fragment_accepts_property_array_unset_source() { + let program = parse_fragment(br#"unset($this->items[0], $this->{$name}[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/assignments.rs b/crates/elephc-magician/src/parser/tests/assignments.rs new file mode 100644 index 0000000000..f95023c4b8 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/assignments.rs @@ -0,0 +1,288 @@ +//! Purpose: +//! Parser tests for assignment, compound assignment, increment/decrement, and echo statements. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases assert direct statement lowering into EvalIR stores and echoes. + +use super::support::*; + +/// Verifies assignment fragments lower to by-name StoreVar statements. +#[test] +fn parse_fragment_accepts_assignment_source() { + let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); + assert_eq!(program.source_len(), 7); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::Int(1)), + }] + ); +} +/// Verifies reference assignments lower to by-name ReferenceAssign statements. +#[test] +fn parse_fragment_accepts_reference_assignment_source() { + let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ReferenceAssign { + target: "left".to_string(), + source: "right".to_string(), + }] + ); +} +/// Verifies multiplicative operators preserve PHP precedence and associativity. +#[test] +fn parse_fragment_accepts_division_and_modulo_source() { + let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::Const(EvalConst::Int(10))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies exponentiation is right-associative and binds tighter than unary negation. +#[test] +fn parse_fragment_accepts_power_source() { + let program = + parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + ] + ); +} +/// Verifies bitwise operators preserve PHP precedence. +#[test] +fn parse_fragment_accepts_bitwise_source() { + let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + }), + }))] + ); +} +/// Verifies shift operators bind lower than additive expressions. +#[test] +fn parse_fragment_accepts_shift_source() { + let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies simple variable compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_compound_assignment_source() { + let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(5))), + }, + }, + EvalStmt::StoreVar { + name: "s".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("s".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), + }, + }, + ] + ); +} +/// Verifies exponentiation compound assignment lowers through the binary power operator. +#[test] +fn parse_fragment_accepts_power_compound_assignment_source() { + let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }] + ); +} +/// Verifies bitwise compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_bitwise_compound_assignment_source() { + let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftRight, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); +} +/// Verifies simple variable increment and decrement statements lower to StoreVar. +#[test] +fn parse_fragment_accepts_inc_dec_statement_source() { + let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + inc_dec_store("i".to_string(), true), + inc_dec_store("j".to_string(), true), + inc_dec_store("k".to_string(), false), + inc_dec_store("m".to_string(), false), + ] + ); +} +/// Verifies echo fragments preserve expression source order. +#[test] +fn parse_fragment_accepts_echo_source() { + let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), + right: Box::new(EvalExpr::LoadVar("name".to_string())), + })] + ); +} +/// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. +#[test] +fn parse_fragment_accepts_echo_comma_list_source() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), + EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/calls.rs b/crates/elephc-magician/src/parser/tests/calls.rs new file mode 100644 index 0000000000..85f9391599 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/calls.rs @@ -0,0 +1,224 @@ +//! Purpose: +//! Parser tests for print, strings, calls, includes, constants, named args, spread args, isset, and empty. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover function-like and call-expression parsing. + +use super::support::*; + +/// Verifies print fragments lower to expression-form print with the printed value. +#[test] +fn parse_fragment_accepts_print_source() { + let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( + EvalConst::String("hi".to_string()) + ))))] + ); +} +/// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. +#[test] +fn parse_fragment_preserves_php_string_escape_semantics() { + let program = parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\nB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\qB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\x0b\x1b\x0cB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), + ])))] + ); +} +/// Verifies call expressions preserve their callee name and source-order arguments. +#[test] +fn parse_fragment_accepts_call_expression_source() { + let program = parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "eval".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "return 1;".to_string() + )))], + }))] + ); +} +/// Verifies include and require constructs parse as expressions with path metadata. +#[test] +fn parse_fragment_accepts_include_require_expression_source() { + let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Include { + path: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), + }), + required: false, + once: false, + })), + EvalStmt::Expr(EvalExpr::Include { + path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), + required: true, + once: true, + }), + ] + ); +} +/// Verifies explicitly qualified call expressions normalize away the leading slash. +#[test] +fn parse_fragment_accepts_qualified_call_expression_source() { + let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "strlen".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} +/// Verifies first-class function callable syntax lowers to runtime callable resolution metadata. +#[test] +fn parse_fragment_accepts_first_class_function_callable_source() { + let program = parse_fragment(br#"namespace App; return strlen(...);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::FunctionCallable { + name: "app\\strlen".to_string(), + fallback_name: Some("strlen".to_string()), + }))] + ); +} +/// Verifies variable callable expressions lower to dynamic calls with source-order args. +#[test] +fn parse_fragment_accepts_dynamic_call_expression_source() { + let program = + parse_fragment(br#"return $fn(first: "a", ...$rest);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::LoadVar("fn".to_string())), + args: vec![ + EvalCallArg::named("first", EvalExpr::Const(EvalConst::String("a".to_string())),), + EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), + ], + }))] + ); +} + +/// Verifies first-class invokable object syntax parses as a callable value. +#[test] +fn parse_fragment_accepts_first_class_invokable_object_callable_source() { + let program = parse_fragment(br#"return $box(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::InvokableCallable { + object: Box::new(EvalExpr::LoadVar("box".to_string())) + }))] + ); +} + +/// Verifies dynamic calls can be applied after another postfix expression. +#[test] +fn parse_fragment_accepts_postfix_dynamic_call_source() { + let program = + parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} +/// Verifies bare constant names lower to dynamic constant-fetch expressions. +#[test] +fn parse_fragment_accepts_constant_fetch_source() { + let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ConstFetch( + "Dyn\\EvalConst".to_string() + )))] + ); +} +/// Verifies function calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_call_argument_source() { + let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![ + EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), + EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), + ], + }))] + ); +} +/// Verifies function calls preserve spread arguments in source order. +#[test] +fn parse_fragment_accepts_spread_call_argument_source() { + let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], + }))] + ); +} +/// Verifies `isset` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_isset_source() { + let program = + parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "isset".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + }), + ], + }))] + ); +} +/// Verifies `empty` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_empty_source() { + let program = parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "empty".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + })], + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/class_constants.rs b/crates/elephc-magician/src/parser/tests/class_constants.rs new file mode 100644 index 0000000000..966f567067 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/class_constants.rs @@ -0,0 +1,162 @@ +//! Purpose: +//! Parser tests for eval class constant declarations and fetches. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `const` members and `Class::CONST` lower to EvalIR. + +use super::support::*; + +/// Verifies class constants lower into eval class metadata. +#[test] +fn parse_fragment_accepts_class_constant_declarations() { + let program = parse_fragment( + br#"class EvalConstBox { + final public const SEED = 2; + protected const LABEL = "box"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_and_constants( + "EvalConstBox", + false, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassConstant::with_visibility_and_final( + "SEED", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(2)), + ), + EvalClassConstant::with_visibility( + "LABEL", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::String("box".to_string())), + ), + ], + Vec::new(), + Vec::new(), + ) + )] + ); +} + +/// Verifies class constant fetches lower to EvalIR expressions. +#[test] +fn parse_fragment_accepts_class_constant_fetches() { + let program = parse_fragment(br#"return EvalConstBox::SEED;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ClassConstantFetch { + class_name: "EvalConstBox".to_string(), + constant: "SEED".to_string(), + }))] + ); +} + +/// Verifies scoped class constant fetches preserve the class-like receiver. +#[test] +fn parse_fragment_accepts_scoped_class_constant_fetches() { + let program = parse_fragment(br#"return self::SEED + parent::SEED + static::SEED;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "SEED".to_string(), + }), + right: Box::new(EvalExpr::ClassConstantFetch { + class_name: "parent".to_string(), + constant: "SEED".to_string(), + }), + }), + right: Box::new(EvalExpr::ClassConstantFetch { + class_name: "static".to_string(), + constant: "SEED".to_string(), + }), + }))] + ); +} + +/// Verifies `ClassName::class` lowers to a class-name fetch rather than a user constant. +#[test] +fn parse_fragment_accepts_class_name_fetches() { + let program = parse_fragment(br#"return EvalConstBox::class;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ClassNameFetch { + class_name: "EvalConstBox".to_string(), + }))] + ); +} + +/// Verifies interface constants lower into eval interface metadata. +#[test] +fn parse_fragment_accepts_interface_constant_declarations() { + let program = parse_fragment( + br#"interface EvalConstIface { + final public const SEED = 4; + function read(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::with_constants( + "EvalConstIface", + Vec::new(), + vec![EvalClassConstant::with_visibility_and_final( + "SEED", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(4)), + )], + vec![EvalInterfaceMethod::new("read", Vec::new())], + ))] + ); +} + +/// Verifies trait constants lower into eval trait metadata. +#[test] +fn parse_fragment_accepts_trait_constant_declarations() { + let program = parse_fragment( + br#"trait EvalConstTrait { + final public const SEED = 6; + public function read() { return self::SEED; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::with_constants( + "EvalConstTrait", + vec![EvalClassConstant::with_visibility_and_final( + "SEED", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(6)), + )], + Vec::new(), + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "SEED".to_string(), + }))], + )], + ))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs b/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs new file mode 100644 index 0000000000..26b29cba02 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs @@ -0,0 +1,282 @@ +//! Purpose: +//! Parser tests for class/member attributes and legacy `var` properties. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Attribute literal metadata and legacy modifier compatibility are covered. + +use super::super::support::*; + +/// Verifies class attributes lower to eval class metadata with supported literal args. +#[test] +fn parse_fragment_accepts_class_attribute_metadata() { + let program = parse_fragment( + br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", "key" => 2])] +#[Tag(name: "named")] +class DynEvalAttributed {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::new("DynEvalAttributed", Vec::new(), Vec::new()).with_attributes(vec![ + EvalAttribute::new( + "Route", + Some(vec![ + EvalAttributeArg::String("/home".to_string()), + EvalAttributeArg::Int(-1), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Float((-2.5f64).to_bits()), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + EvalAttributeArg::String("EvalAttrDep".to_string()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::String("nested".to_string()), + EvalAttributeArg::Named { + name: "key".to_string(), + value: Box::new(EvalAttributeArg::Int(2)), + }, + ]), + ]), + ), + EvalAttribute::new( + "Tag", + Some(vec![EvalAttributeArg::Named { + name: "name".to_string(), + value: Box::new(EvalAttributeArg::String("named".to_string())), + }]), + ), + ]) + )] + ); +} + +/// Verifies class-like declaration attributes attach to interfaces, traits, and enums. +#[test] +fn parse_fragment_accepts_class_like_attribute_metadata() { + let program = parse_fragment( + br#"#[IfaceMark] interface DynEvalAttrIface {} +#[TraitMark] trait DynEvalAttrTrait {} +#[EnumMark] enum DynEvalAttrEnum { case Ready; }"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl( + EvalInterface::new("DynEvalAttrIface", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMark", Some(Vec::new()))]) + ), + EvalStmt::TraitDecl( + EvalTrait::new("DynEvalAttrTrait", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("TraitMark", Some(Vec::new()))]) + ), + EvalStmt::EnumDecl( + EvalEnum::new( + "DynEvalAttrEnum", + None, + vec![EvalEnumCase::new("Ready", None)] + ) + .with_attributes(vec![EvalAttribute::new("EnumMark", Some(Vec::new()))]) + ), + ] + ); +} + +/// Verifies attributes on class constants, properties, and methods are retained. +#[test] +fn parse_fragment_accepts_class_member_attribute_metadata() { + let program = parse_fragment( + br#"class DynEvalMemberAttrs { + #[ConstMark] + public const SEED = 1; + #[PropMark("p")] + public int $value; + #[MethodMark(true)] + public function read() { return 1; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_and_constants( + "DynEvalMemberAttrs", + false, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(1))) + .with_attributes(vec![EvalAttribute::new("ConstMark", Some(Vec::new()))]) + ], + vec![EvalClassProperty::new("value", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_attributes(vec![EvalAttribute::new( + "PropMark", + Some(vec![EvalAttributeArg::String("p".to_string())]) + )])], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))] + ) + .with_attributes(vec![EvalAttribute::new( + "MethodMark", + Some(vec![EvalAttributeArg::Bool(true)]) + )])], + ) + )] + ); +} + +/// Verifies PHP's legacy `var` property marker parses as public property syntax. +#[test] +fn parse_fragment_accepts_legacy_var_properties() { + let program = parse_fragment( + br#"class DynEvalVarProps { + var $plain = "p"; + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVarProps", + vec![ + EvalClassProperty::new( + "plain", + Some(EvalExpr::Const(EvalConst::String("p".to_string()))) + ), + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + +/// Verifies legacy `var` property syntax also works inside eval traits. +#[test] +fn parse_fragment_accepts_legacy_var_trait_properties() { + let program = parse_fragment( + br#"trait DynEvalVarTrait { + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalVarTrait", + vec![ + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + +/// Verifies legacy `var` cannot be combined with other property modifiers. +#[test] +fn parse_fragment_rejects_legacy_var_modifier_combinations() { + for source in [ + b"class DynEvalBadPublicVar { public var $value; }" as &[u8], + b"class DynEvalBadStaticVar { static var $value; }", + b"class DynEvalBadReadonlyVar { readonly var $value; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies interface, trait, and enum member attributes are retained. +#[test] +fn parse_fragment_accepts_class_like_member_attribute_metadata() { + let program = parse_fragment( + br#"interface DynEvalMemberIface { + #[IfaceProp] + public string $value { get; } + #[IfaceMethod] + function read(); +} +trait DynEvalMemberTrait { + #[TraitProp] + public int $seed; + #[TraitMethod] + public function add() { return 2; } +} +enum DynEvalMemberEnum { + #[CaseMark] + case Ready; + #[EnumMethod] + public function label() { return "ready"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl(EvalInterface::with_constants_and_properties( + "DynEvalMemberIface", + Vec::new(), + Vec::new(), + vec![EvalInterfaceProperty::new("value", true, false) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_attributes(vec![EvalAttribute::new("IfaceProp", Some(Vec::new()))])], + vec![EvalInterfaceMethod::new("read", Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMethod", Some(Vec::new()))])], + )), + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalMemberTrait", + vec![EvalClassProperty::new("seed", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_attributes(vec![EvalAttribute::new("TraitProp", Some(Vec::new()))])], + vec![EvalClassMethod::new( + "add", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(2))))] + ) + .with_attributes(vec![EvalAttribute::new("TraitMethod", Some(Vec::new()))])], + )), + EvalStmt::EnumDecl(EvalEnum::with_members( + "DynEvalMemberEnum", + None, + Vec::new(), + vec![EvalEnumCase::new("Ready", None) + .with_attributes(vec![EvalAttribute::new("CaseMark", Some(Vec::new()))])], + Vec::new(), + vec![EvalClassMethod::new( + "label", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "ready".to_string() + ))))] + ) + .with_attributes(vec![EvalAttribute::new("EnumMethod", Some(Vec::new()))])], + )), + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs b/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs new file mode 100644 index 0000000000..2337970b10 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs @@ -0,0 +1,311 @@ +//! Purpose: +//! Parser tests for basic class declarations, types, reserved names, references, +//! and class-like constants. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover dynamic class metadata and malformed fragment errors. + +use super::super::support::*; + +/// Verifies empty class declarations lower to dynamic class-registration statements. +#[test] +fn parse_fragment_accepts_empty_class_declaration_source() { + let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalClass", + Vec::new(), + Vec::new() + ))] + ); +} +/// Verifies class relation clauses lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_class_extends_and_implements_source() { + let program = parse_fragment( + br#"class DynEvalChild extends DynEvalBase implements DynEvalIface, \Root\Iface {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_relations( + "DynEvalChild", + Some("DynEvalBase".to_string()), + vec!["DynEvalIface".to_string(), "Root\\Iface".to_string()], + Vec::new(), + Vec::new(), + ))] + ); +} + +/// Verifies function, interface, and class method return types are retained. +#[test] +fn parse_fragment_accepts_return_type_metadata() { + let program = parse_fragment( + br#"function DynEvalReturn(): ?int { return 1; } +interface DynEvalReturnIface { + public function read(): string; +} +class DynEvalReturnClass { + public function selfReturn(): static { return $this; } + public function done(): void {} +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::FunctionDecl { + return_type: Some(function_return_type), + .. + } = &statements[0] + else { + panic!("expected function declaration with return type"); + }; + assert_eq!( + function_return_type.variants(), + &[EvalParameterTypeVariant::Int] + ); + assert!(function_return_type.allows_null()); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + let interface_return_type = interface.methods()[0] + .return_type() + .expect("interface method return type"); + assert_eq!( + interface_return_type.variants(), + &[EvalParameterTypeVariant::String] + ); + + let EvalStmt::ClassDecl(class) = &statements[2] else { + panic!("expected class declaration"); + }; + let self_return_type = class.methods()[0] + .return_type() + .expect("class method return type"); + assert_eq!( + self_return_type.variants(), + &[EvalParameterTypeVariant::Class("static".to_string())] + ); + let void_return_type = class.methods()[1] + .return_type() + .expect("void method return type"); + assert_eq!( + void_return_type.variants(), + &[EvalParameterTypeVariant::Void] + ); +} + +/// Verifies type atoms are rejected in positions where PHP forbids them. +#[test] +fn parse_fragment_rejects_invalid_type_atom_forms() { + for source in [ + b"function DynEvalBadVoid(): ?void {}" as &[u8], + b"function DynEvalBadVoidUnion(): void|null {}", + b"function DynEvalBadNeverUnion(): never|int {}", + b"function DynEvalBadNeverIntersection(): never&Countable {}", + b"function DynEvalBadVoidParam(void $value) {}", + b"function DynEvalBadSelfParam(self $value) {}", + b"function DynEvalBadParentParam(parent $value) {}", + b"function DynEvalBadStaticParam(static $value) {}", + b"function DynEvalBadSelfReturn(): self {}", + b"function DynEvalBadParentReturn(): parent {}", + b"function DynEvalBadStaticReturn(): static {}", + b"class DynEvalBadStaticMethodParam { public function read(static $value) {} }", + b"class DynEvalBadCallableProperty { public callable $value; }", + b"class DynEvalBadStaticProperty { public static static $value; }", + b"interface DynEvalBadCallableInterfaceProperty { public callable $value { get; } }", + b"class DynEvalBadCallablePromoted { public function __construct(public callable $value) {} }", + b"class DynEvalBadStaticPromoted { public function __construct(public static $value) {} }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval rejects PHP's reserved `class` class-constant name. +#[test] +fn parse_fragment_rejects_reserved_class_constant_name() { + for source in [ + b"class DynEvalBadConstName { const class = 1; }" as &[u8], + b"interface DynEvalBadIfaceConstName { const class = 1; }", + b"trait DynEvalBadTraitConstName { const class = 1; }", + b"enum DynEvalBadEnumConstName { const class = 1; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval rejects PHP-reserved class-like declaration names. +#[test] +fn parse_fragment_rejects_reserved_class_like_declaration_names() { + for source in [ + b"class match {}" as &[u8], + b"class string {}", + b"class CLASS {}", + b"interface interface {}", + b"trait readonly {}", + b"enum bool { case Ready; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts PHP semi-reserved class-like declaration names. +#[test] +fn parse_fragment_accepts_semi_reserved_class_like_declaration_names() { + let program = parse_fragment( + br#"class enum {} +interface from {} +trait resource {} +enum integer { case Ready; }"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!(class.name(), "enum"); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!(interface.name(), "from"); + + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.name(), "resource"); + + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!(enum_decl.name(), "integer"); +} + +/// Verifies eval rejects PHP-reserved bare class-like reference names. +#[test] +fn parse_fragment_rejects_reserved_unqualified_class_like_reference_names() { + for source in [ + b"class DynEvalBadExtends extends match {}" as &[u8], + b"class DynEvalBadImplements implements match {}", + b"interface DynEvalBadIfaceExtends extends match {}", + b"class DynEvalBadTraitUse { use match; }", + b"class DynEvalBadTraitAdapt { use SomeTrait { match::run insteadof SomeTrait; } }", + b"$box = new match();", + b"$ok = $box instanceof match;", + b"try {} catch (match $e) {}", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts semi-reserved or qualified class-like reference names PHP parses. +#[test] +fn parse_fragment_accepts_semi_reserved_and_qualified_class_like_reference_names() { + parse_fragment( + br#"class enum {} +class DynEvalExtendsSemiReserved extends enum {} +class DynEvalExtendsQualifiedReserved extends \match {} +class DynEvalNewSelf { + public function make() { return new self(); } +} +$ok = $box instanceof \match;"#, + ) + .expect("fragment should parse"); +} + +/// Verifies comma-separated class-like constants lower to individual eval constants. +#[test] +fn parse_fragment_accepts_comma_separated_class_like_constants() { + let program = parse_fragment( + br#"class DynEvalMultiConstClass { + public const A = 1, B = 2; +} +interface DynEvalMultiConstIface { + final public const C = 3, D = 4; +} +trait DynEvalMultiConstTrait { + protected const E = 5, F = 6; +} +enum DynEvalMultiConstEnum { + public const G = 7, H = 8; + case Ready; +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!( + class.constants(), + &[ + EvalClassConstant::new("A", EvalExpr::Const(EvalConst::Int(1))), + EvalClassConstant::new("B", EvalExpr::Const(EvalConst::Int(2))), + ], + ); + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!( + interface.constants(), + &[ + EvalClassConstant::with_visibility_and_final( + "C", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(3)), + ), + EvalClassConstant::with_visibility_and_final( + "D", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(4)), + ), + ], + ); + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!( + trait_decl.constants(), + &[ + EvalClassConstant::with_visibility( + "E", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(5)), + ), + EvalClassConstant::with_visibility( + "F", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(6)), + ), + ], + ); + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!( + enum_decl.constants(), + &[ + EvalClassConstant::new("G", EvalExpr::Const(EvalConst::Int(7))), + EvalClassConstant::new("H", EvalExpr::Const(EvalConst::Int(8))), + ], + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs b/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs new file mode 100644 index 0000000000..edaff35d90 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs @@ -0,0 +1,326 @@ +//! Purpose: +//! Parser tests for interfaces, property contracts, constructor promotion, and +//! public class members. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Interface and property forms retain their EvalIR metadata and diagnostics. + +use super::super::support::*; + +/// Verifies eval interface declarations lower to dynamic interface metadata. +#[test] +fn parse_fragment_accepts_interface_declaration_source() { + let program = parse_fragment( + br#"interface DynEvalIface extends ParentIface, \Root\Iface { + public function read($value); + function label(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::new( + "DynEvalIface", + vec!["ParentIface".to_string(), "Root\\Iface".to_string()], + vec![ + EvalInterfaceMethod::new("read", vec!["value".to_string()]), + EvalInterfaceMethod::new("label", Vec::new()), + ], + ))] + ); +} + +/// Verifies interface property hook contracts lower to eval interface metadata. +#[test] +fn parse_fragment_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface DynEvalHookIface { + public string $value { get; set; } + public int $id { &get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalHookIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("value", true, true).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )), + EvalInterfaceProperty::new("id", true, false).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )), + ], + Vec::new(), + ) + )] + ); +} + +/// Verifies interface property contracts retain asymmetric set visibility. +#[test] +fn parse_fragment_accepts_interface_asymmetric_property_contracts() { + let program = parse_fragment( + br#"interface DynEvalAsymmetricIface { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalAsymmetricIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("name", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + EvalInterfaceProperty::new("id", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + ], + Vec::new(), + ) + )] + ); +} + +/// Verifies public property and method class members lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_public_class_members() { + let program = parse_fragment( + b"class DynEvalSupported { public int $x = 1; public function read() { return $this->x; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalSupported", + vec![ + EvalClassProperty::new("x", Some(EvalExpr::Const(EvalConst::Int(1)))).with_type( + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )) + ) + ], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + )] + ))] + ); +} + +/// Verifies constructor-promoted properties lower to property metadata and assignments. +#[test] +fn parse_fragment_accepts_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromoted { + public function __construct(public int $id, private readonly ?string $name = null) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromoted", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted(), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Private, + false, + false, + true, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + ))) + .with_promoted(), + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string(), "name".to_string()], + vec![ + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + value: EvalExpr::LoadVar("id".to_string()), + }, + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "name".to_string(), + value: EvalExpr::LoadVar("name".to_string()), + }, + ], + ) + .with_parameter_types(vec![ + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )), + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + )), + ]) + .with_parameter_defaults(vec![None, Some(EvalExpr::Const(EvalConst::Null))])] + ))] + ); +} + +/// Verifies comma-separated simple properties lower to individual eval properties. +#[test] +fn parse_fragment_accepts_comma_separated_class_properties() { + let program = parse_fragment( + br#"class DynEvalMultiProperties { + public private(set) int $id = 1, $nextId; + public static string $first = "a", $second = "b"; + var $legacy, $legacyDefault = 3; +} +trait DynEvalMultiPropertyTrait { + protected int $left = 4, $right = 5; +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let properties = class.properties(); + assert_eq!(properties.len(), 6); + assert_eq!(properties[0].name(), "id"); + assert_eq!(properties[0].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!( + properties[0].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )), + ); + assert_eq!(properties[0].default(), Some(&EvalExpr::Const(EvalConst::Int(1)))); + assert_eq!(properties[1].name(), "nextId"); + assert_eq!(properties[1].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!(properties[1].default(), None); + assert_eq!(properties[2].name(), "first"); + assert!(properties[2].is_static()); + assert_eq!( + properties[2].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false, + )), + ); + assert_eq!(properties[3].name(), "second"); + assert!(properties[3].is_static()); + assert_eq!(properties[4].name(), "legacy"); + assert_eq!(properties[4].property_type(), None); + assert_eq!(properties[5].name(), "legacyDefault"); + assert_eq!(properties[5].default(), Some(&EvalExpr::Const(EvalConst::Int(3)))); + + let EvalStmt::TraitDecl(trait_decl) = &program.statements()[1] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.properties().len(), 2); + assert_eq!(trait_decl.properties()[0].name(), "left"); + assert_eq!(trait_decl.properties()[0].visibility(), EvalVisibility::Protected); + assert_eq!(trait_decl.properties()[1].name(), "right"); + assert_eq!(trait_decl.properties()[1].visibility(), EvalVisibility::Protected); +} + +/// Verifies by-reference promoted constructor parameters lower to property aliases. +#[test] +fn parse_fragment_accepts_by_reference_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromotedRef { + public function __construct(public int &$id) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromotedRef", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted() + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string()], + vec![EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + source: "id".to_string(), + }], + ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) + .with_parameter_by_ref_flags(vec![true])] + ))] + ); +} + +/// Verifies eval rejects promoted parameter forms that the eval runtime cannot model yet. +#[test] +fn parse_fragment_rejects_unsupported_constructor_promotion_forms() { + parse_fragment(b"class DynEvalPromotedMethod { public function run(public int $id) {} }") + .expect_err("promotion is only valid on constructors"); + parse_fragment( + b"class DynEvalPromotedVariadic { public function __construct(public ...$ids) {} }", + ) + .expect_err("promoted variadic parameters need variadic property semantics"); + parse_fragment( + b"interface DynEvalPromotedIface { public function __construct(public int $id); }", + ) + .expect_err("interface signatures cannot promote properties"); + parse_fragment(b"enum DynEvalPromotedEnum { public function __construct(public int $id) {} }") + .expect_err("enum methods cannot promote properties"); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs new file mode 100644 index 0000000000..683888cae0 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs @@ -0,0 +1,319 @@ +//! Purpose: +//! Parser tests for abstract/final methods, typed/default parameters, traits, +//! object construction, and terminal diagnostics. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parameter metadata and invalid variadic or late-bound defaults are retained. + +use super::super::support::*; + +/// Verifies abstract and final class modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_abstract_and_final_class_members() { + let program = parse_fragment( + br#"abstract class DynEvalAbstract { + abstract public function read($value); + final public $value = 42; + final public function label() { return "base"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstract", + true, + false, + None, + Vec::new(), + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "value", + EvalVisibility::Public, + false, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(42))), + ) + ], + vec![ + EvalClassMethod::with_modifiers( + "read", + true, + false, + vec!["value".to_string()], + Vec::new() + ), + EvalClassMethod::with_modifiers( + "label", + false, + true, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "base".to_string() + ))))] + ), + ], + ))] + ); +} + +/// Verifies eval method parameters retain type, default, by-reference, and variadic metadata. +#[test] +fn parse_fragment_accepts_typed_method_parameter_metadata() { + let program = parse_fragment( + br#"class DynEvalTypedParams { + public function run(#[Both("pair")] Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + assert_eq!( + method.params(), + &[ + "both".to_string(), + "id".to_string(), + "name".to_string(), + "label".to_string(), + "tail".to_string() + ] + ); + assert_eq!( + method.parameter_has_types(), + &[true, true, true, true, true] + ); + assert!(method.parameter_types().iter().all(Option::is_some)); + assert_eq!(method.parameter_attributes()[0][0].name(), "Both"); + assert!(method.parameter_attributes()[1].is_empty()); + let both_type = method.parameter_types()[0].as_ref().expect("both type"); + assert_eq!( + both_type.variants(), + &[ + EvalParameterTypeVariant::Class("Left".to_string()), + EvalParameterTypeVariant::Class("Right".to_string()) + ] + ); + assert!(!both_type.allows_null()); + assert!(both_type.is_intersection()); + let id_type = method.parameter_types()[1].as_ref().expect("id type"); + assert_eq!(id_type.variants(), &[EvalParameterTypeVariant::Int]); + assert!(!id_type.allows_null()); + assert!(!id_type.is_intersection()); + let name_type = method.parameter_types()[2].as_ref().expect("name type"); + assert_eq!( + name_type.variants(), + &[EvalParameterTypeVariant::Class("App\\Name".to_string())] + ); + assert!(name_type.allows_null()); + assert!(!name_type.is_intersection()); + let label_type = method.parameter_types()[3].as_ref().expect("label type"); + assert_eq!(label_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(label_type.allows_null()); + assert!(!label_type.is_intersection()); + assert!(matches!( + method.parameter_defaults(), + [ + None, + Some(EvalExpr::Const(EvalConst::Int(7))), + Some(EvalExpr::Const(EvalConst::Null)), + Some(EvalExpr::Const(EvalConst::String(label))), + None + ] if label == "x" + )); + assert_eq!( + method.parameter_is_by_ref(), + &[false, true, true, false, true] + ); + assert_eq!( + method.parameter_is_variadic(), + &[false, false, false, false, true] + ); +} + +/// Verifies eval method parameter defaults retain supported constant-expression metadata. +#[test] +fn parse_fragment_accepts_method_parameter_constant_defaults() { + let program = parse_fragment( + br#"class DynEvalDefaultConstants { + const LABEL = "box"; + public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "x"], $method = __METHOD__, $dep = new DynEvalDefaultDep(label: "dep")) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + + assert!(matches!( + method.parameter_defaults(), + [ + Some(EvalExpr::ConstFetch(global)), + Some(EvalExpr::ClassConstantFetch { class_name: self_name, constant: self_constant }), + Some(EvalExpr::ClassConstantFetch { class_name: parent_name, constant: parent_constant }), + Some(EvalExpr::ClassNameFetch { class_name }), + Some(EvalExpr::Array(_)), + Some(EvalExpr::Magic(EvalMagicConst::Method)), + Some(EvalExpr::NewObject { class_name: dep_name, args }) + ] if global == "DYN_EVAL_DEFAULT_GLOBAL" + && self_name == "self" + && self_constant == "LABEL" + && parent_name == "parent" + && parent_constant == "LABEL" + && class_name == "self" + && dep_name == "DynEvalDefaultDep" + && args.len() == 1 + && args[0].name() == Some("label") + )); +} + +/// Verifies eval rejects late-bound `static::` defaults like PHP compile-time constants do. +#[test] +fn parse_fragment_rejects_late_bound_static_method_parameter_defaults() { + parse_fragment( + b"class DynEvalStaticDefault { public function read($label = static::LABEL) {} }", + ) + .expect_err("static class constant defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticClassDefault { public function read($class = static::class) {} }", + ) + .expect_err("static class-name defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticNewDefault { public function read($dep = new static()) {} }", + ) + .expect_err("static object defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalSpreadNewDefault { public function read($dep = new DynEvalDep(...[\"x\"])) {} }", + ) + .expect_err("argument unpacking is not supported in PHP constant expressions"); +} + +/// Verifies eval rejects invalid variadic method parameter forms. +#[test] +fn parse_fragment_rejects_invalid_variadic_method_parameters() { + parse_fragment(b"class DynEvalVariadicDefault { public function run(...$tail = null) {} }") + .expect_err("variadic method parameters cannot have defaults"); + parse_fragment(b"class DynEvalVariadicNotLast { public function run(...$tail, $next) {} }") + .expect_err("variadic method parameters must be last"); + parse_fragment(b"class DynEvalVariadicRefOrder { public function run(...&$tail) {} }") + .expect_err("by-reference marker must precede variadic marker"); +} + +/// Verifies trait declarations and class trait uses lower into dynamic metadata. +#[test] +fn parse_fragment_accepts_trait_declaration_and_class_use() { + let program = parse_fragment( + br#"trait DynEvalTrait { + public int $seed = 2; + public function read($value) { return $this->seed + $value; } +} +class DynEvalUsesTrait { + use DynEvalTrait, \Root\SharedTrait; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalTrait", + vec![ + EvalClassProperty::new("seed", Some(EvalExpr::Const(EvalConst::Int(2)))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + ], + vec![EvalClassMethod::new( + "read", + vec!["value".to_string()], + vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "seed".to_string(), + }), + right: Box::new(EvalExpr::LoadVar("value".to_string())), + }))] + )], + )), + EvalStmt::ClassDecl(EvalClass::with_modifiers_and_traits( + "DynEvalUsesTrait", + false, + false, + None, + Vec::new(), + vec!["DynEvalTrait".to_string(), "Root\\SharedTrait".to_string()], + Vec::new(), + Vec::new(), + )), + ] + ); +} + +/// Verifies trait declarations can compose other traits with adaptations. +#[test] +fn parse_fragment_accepts_trait_use_inside_trait() { + let program = parse_fragment( + br#"trait DynEvalInnerTrait { + public function read() { return "inner"; } +} +trait DynEvalOuterTrait { + use DynEvalInnerTrait { + read as private hiddenRead; + } + public function expose() { return $this->hiddenRead(); } +}"#, + ) + .expect("fragment should parse"); + + let [_, EvalStmt::TraitDecl(trait_decl)] = program.statements() else { + panic!("second statement should be a trait declaration"); + }; + assert_eq!(trait_decl.traits(), &["DynEvalInnerTrait".to_string()]); + assert_eq!( + trait_decl.trait_adaptations(), + &[EvalTraitAdaptation::Alias { + trait_name: None, + method: "read".to_string(), + alias: Some("hiddenRead".to_string()), + visibility: Some(EvalVisibility::Private), + }] + ); +} +/// Verifies malformed object construction reports an unexpected token. +#[test] +fn parse_fragment_rejects_new_without_class_name() { + assert_eq!( + parse_fragment(b"return new ();"), + Err(EvalParseError::UnexpectedToken) + ); +} +/// Verifies unsupported expression keywords report the unsupported construct status. +#[test] +fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { + for source in [b"return yield 1;" as &[u8]] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} +/// Verifies malformed statements report parse errors instead of partial IR. +#[test] +fn parse_fragment_rejects_missing_semicolon() { + assert_eq!( + parse_fragment(b"$x = 1"), + Err(EvalParseError::ExpectedSemicolon) + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs b/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs new file mode 100644 index 0000000000..6887625df6 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Organizes parser tests for class-like declarations, members, attributes, +//! hooks, traits, and diagnostics by syntax responsibility. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod attributes; +mod declarations; +mod interfaces_properties; +mod methods_traits_errors; +mod visibility_hooks; diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs b/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs new file mode 100644 index 0000000000..b7ad5c6313 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs @@ -0,0 +1,419 @@ +//! Purpose: +//! Parser tests for member visibility, readonly/asymmetric properties, property +//! hooks, and related invalid forms. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Concrete, abstract, interface, and trait hook contracts are distinguished. + +use super::super::support::*; + +/// Verifies private and protected class members lower with explicit visibility metadata. +#[test] +fn parse_fragment_accepts_private_and_protected_class_members() { + let program = parse_fragment( + b"class DynEvalVisibility { private int $secret = 3; protected function reveal() { return $this->secret; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVisibility", + vec![EvalClassProperty::with_visibility( + "secret", + EvalVisibility::Private, + Some(EvalExpr::Const(EvalConst::Int(3))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], + vec![EvalClassMethod::with_visibility_and_modifiers( + "reveal", + EvalVisibility::Protected, + false, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "secret".to_string(), + }))] + )] + ))] + ); +} + +/// Verifies readonly property modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_readonly_class_property() { + let program = parse_fragment(b"class DynEvalReadonly { public readonly int $id; }") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalReadonly", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], + Vec::new() + ))] + ); +} + +/// Verifies asymmetric property visibility lowers into eval class metadata. +#[test] +fn parse_fragment_accepts_asymmetric_property_visibility() { + let program = parse_fragment( + b"class DynEvalAsymmetric { public private(set) int $id = 1; protected(set) string $name = \"x\"; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalAsymmetric", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::Int(1))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::String("x".to_string()))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + ], + Vec::new() + ))] + ); +} + +/// Verifies eval rejects asymmetric property visibility forms that PHP rejects. +#[test] +fn parse_fragment_rejects_invalid_asymmetric_property_visibility() { + parse_fragment(b"class DynEvalAsymUntyped { public private(set) $id = 1; }") + .expect_err("asymmetric properties must be typed"); + parse_fragment(b"class DynEvalAsymStatic { public private(set) static int $id = 1; }") + .expect_err("asymmetric properties cannot be static"); + parse_fragment(b"class DynEvalAsymWeak { private public(set) int $id = 1; }") + .expect_err("set visibility cannot be weaker than read visibility"); + parse_fragment(b"class DynEvalAsymMethod { public private(set) function run() {} }") + .expect_err("asymmetric visibility is property-only"); +} + +/// Verifies readonly class modifiers lower into class and property metadata. +#[test] +fn parse_fragment_accepts_readonly_class_modifier() { + let program = parse_fragment( + b"final readonly class DynEvalReadonlyClass { public int $id; public static int $count = 0; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_class_modifiers( + "DynEvalReadonlyClass", + false, + true, + true, + None, + Vec::new(), + vec![ + EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))), + EvalClassProperty::with_visibility_static_and_readonly( + "count", + EvalVisibility::Public, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(0))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + ], + Vec::new() + ))] + ); +} + +/// Verifies concrete property hooks lower to property metadata plus accessor methods. +#[test] +fn parse_fragment_accepts_concrete_class_property_hooks() { + let program = parse_fragment( + br#"class DynEvalHooked { + public int $value { + &get => 7; + set => $value + 1; + } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalHooked", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_hooks(true, true) + .with_virtual(false)], + vec![ + EvalClassMethod::new( + "__propget_value", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(7))))] + ) + .with_source_location(EvalSourceLocation::new(3, 3)), + EvalClassMethod::new( + "__propset_value", + vec!["value".to_string()], + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "value".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("value".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))) + } + }] + ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) + .with_source_location(EvalSourceLocation::new(4, 4)) + ] + ) + .with_source_location(EvalSourceLocation::new(1, 6)))] + ); +} + +/// Verifies typed set-hook parameters are retained separately from the property type. +#[test] +fn parse_fragment_retains_property_set_hook_parameter_type() { + let program = parse_fragment( + br#"class DynEvalTypedSetHooked { + public string $value { + set(int|string $raw) => $raw; + } +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let property = &class.properties()[0]; + assert_eq!( + property.property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + )) + ); + assert_eq!( + property.set_hook_type(), + Some(&EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + )) + ); + assert_eq!( + class.methods()[0].parameter_types(), + &[Some(EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + ))] + ); +} + +/// Verifies eval rejects explicit untyped set-hook parameters for typed properties. +#[test] +fn parse_fragment_rejects_untyped_explicit_property_set_hook_parameter_for_typed_property() { + assert_eq!( + parse_fragment( + br#"class DynEvalUntypedSetParamHooked { + public string $value { + set($raw) => $raw; + } +}"# + ), + Err(EvalParseError::UnsupportedConstruct) + ); + + parse_fragment( + br#"class DynEvalUntypedSetParamUntypedProperty { + public $value { + set($raw) => $raw; + } +}"#, + ) + .expect("untyped properties may use an untyped explicit set-hook parameter"); +} + +/// Verifies abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_abstract_class_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class DynEvalAbstractHooked { + abstract public int $value { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstractHooked", + true, + false, + None, + Vec::new(), + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_abstract_hook_contract(true, true)], + Vec::new(), + ))] + ); +} + +/// Verifies trait abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait DynEvalAbstractHookTrait { + abstract protected string $name { get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalAbstractHookTrait", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "name", + EvalVisibility::Protected, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_abstract_hook_contract(true, false)], + Vec::new(), + ))] + ); +} + +/// Verifies eval rejects readonly property forms that PHP does not allow. +#[test] +fn parse_fragment_rejects_invalid_readonly_class_properties() { + parse_fragment(b"class DynEvalReadonlyDefault { public readonly int $id = 1; }") + .expect_err("readonly properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalReadonlyStatic { public static readonly int $id; }") + .expect_err("static properties cannot be readonly in eval"); + parse_fragment(b"readonly class DynEvalReadonlyClassDefault { public int $id = 1; }") + .expect_err("readonly class instance properties cannot have defaults in eval"); +} + +/// Verifies eval rejects property hook forms that need broader class contracts. +#[test] +fn parse_fragment_rejects_invalid_property_hooks() { + parse_fragment(b"class DynEvalHookDefault { public int $id = 1 { get => $this->id; } }") + .expect_err("hooked properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") + .expect_err("static properties cannot have hooks in eval"); + parse_fragment(b"class DynEvalHookByRefSet { public int $id { &set => 1; } }") + .expect_err("set property hooks cannot return by reference"); + parse_fragment( + b"abstract class DynEvalHookAbstractDefault { abstract public int $id = 1 { get; } }", + ) + .expect_err("abstract properties cannot have defaults in eval"); + parse_fragment( + b"abstract class DynEvalHookAbstractStatic { abstract public static int $id { get; } }", + ) + .expect_err("static properties cannot have abstract hooks in eval"); +} + +/// Verifies eval rejects concrete hook bodies in interface property contracts. +#[test] +fn parse_fragment_rejects_invalid_interface_property_hooks() { + parse_fragment(b"interface DynEvalIfaceHookBody { public int $id { get => 1; } }") + .expect_err("interface property hooks cannot have concrete bodies"); + parse_fragment(b"interface DynEvalIfaceHookDuplicate { public int $id { get; get; } }") + .expect_err("interface property hooks cannot repeat contracts"); + parse_fragment(b"interface DynEvalIfaceHookEmpty { public int $id { } }") + .expect_err("interface property hooks require at least one contract"); + parse_fragment(b"interface DynEvalIfaceHookByRefSet { public int $id { &set; } }") + .expect_err("set interface property hooks cannot return by reference"); + parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") + .expect_err("interface property hook contracts cannot have defaults"); + parse_fragment( + b"interface DynEvalIfaceAsymReadOnly { public protected(set) int $id { get; } }", + ) + .expect_err("readonly virtual property cannot have asymmetric visibility"); + parse_fragment( + b"interface DynEvalIfaceAsymUntyped { public protected(set) $id { get; set; } }", + ) + .expect_err("asymmetric interface property must be typed"); +} diff --git a/crates/elephc-magician/src/parser/tests/control_statements.rs b/crates/elephc-magician/src/parser/tests/control_statements.rs new file mode 100644 index 0000000000..359d1511f4 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/control_statements.rs @@ -0,0 +1,173 @@ +//! Purpose: +//! Parser tests for branch, loop, switch, foreach, and function declaration statements. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify statement body shapes and ordered EvalIR blocks. + +use super::support::*; + +/// Verifies if/else fragments lower to branch statements with nested blocks. +#[test] +fn parse_fragment_accepts_if_else_source() { + let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("yes".to_string())), + }], + else_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("no".to_string())), + }], + }] + ); +} +/// Verifies braceless if/else bodies parse as single-statement branch bodies. +#[test] +fn parse_fragment_accepts_braceless_if_else_source() { + let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))], + else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "no".to_string() + )))], + }] + ); +} +/// Verifies elseif fragments lower to nested if statements in the else branch. +#[test] +fn parse_fragment_accepts_elseif_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("a".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("a".to_string())), + }], + else_branch: vec![EvalStmt::If { + condition: EvalExpr::LoadVar("b".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("b".to_string())), + }], + else_branch: Vec::new(), + }], + }] + ); +} +/// Verifies PHP's `else if` spelling follows the same nested branch shape. +#[test] +fn parse_fragment_accepts_else_if_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + + assert!(matches!( + program.statements(), + [EvalStmt::If { + else_branch, + .. + }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) + )); +} +/// Verifies for loops lower clauses and body statements separately. +#[test] +fn parse_fragment_accepts_for_source() { + let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::For { + init: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }], + condition: Some(EvalExpr::LoadVar("i".to_string())), + update: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }], + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], + }] + ); +} +/// Verifies switch fragments preserve ordered case and default bodies. +#[test] +fn parse_fragment_accepts_switch_source() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Switch { + expr: EvalExpr::LoadVar("x".to_string()), + cases: vec![ + EvalSwitchCase { + condition: Some(EvalExpr::Const(EvalConst::Int(1))), + body: vec![ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), + EvalStmt::Break, + ], + }, + EvalSwitchCase { + condition: None, + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))], + }, + ], + }] + ); +} +/// Verifies value-only foreach loops lower to an array expression, value target, and body. +#[test] +fn parse_fragment_accepts_foreach_source() { + let program = parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: None, + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], + }] + ); +} +/// Verifies key-value foreach loops preserve both loop target names in EvalIR. +#[test] +fn parse_fragment_accepts_foreach_key_value_source() { + let program = parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) + .expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: Some("key".to_string()), + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("key".to_string())), + right: Box::new(EvalExpr::LoadVar("item".to_string())), + })], + }] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/enums.rs b/crates/elephc-magician/src/parser/tests/enums.rs new file mode 100644 index 0000000000..de8bb0da1d --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/enums.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Parser tests for eval-declared pure and backed enum declarations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover enum cases, backing types, interfaces, constants, and methods. + +use super::support::*; + +/// Verifies pure enum cases lower to dynamic enum metadata. +#[test] +fn parse_fragment_accepts_pure_enum_declaration_source() { + let program = + parse_fragment(b"enum EvalSuit { case Hearts; case Clubs; }").expect("parse eval enum"); + assert_eq!( + program.statements(), + &[EvalStmt::EnumDecl(EvalEnum::new( + "EvalSuit", + None, + vec![ + EvalEnumCase::new("Hearts", None), + EvalEnumCase::new("Clubs", None), + ], + ))] + ); +} + +/// Verifies backed enum metadata preserves interfaces, case values, constants, and methods. +#[test] +fn parse_fragment_accepts_backed_enum_members() { + let program = parse_fragment( + br#"enum EvalColor: string implements EvalLabel { + case Red = "r"; + final public const PREFIX = "color"; + public function label() { return self::PREFIX . ":" . $this->name; } +}"#, + ) + .expect("parse backed eval enum"); + assert_eq!( + program.statements(), + &[EvalStmt::EnumDecl(EvalEnum::with_members( + "EvalColor", + Some(EvalEnumBackingType::String), + vec!["EvalLabel".to_string()], + vec![EvalEnumCase::new( + "Red", + Some(EvalExpr::Const(EvalConst::String("r".to_string()))), + )], + vec![EvalClassConstant::with_visibility_and_final( + "PREFIX", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::String("color".to_string())), + )], + vec![EvalClassMethod::new( + "label", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "PREFIX".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::String(":".to_string()))), + }), + right: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "name".to_string(), + }), + }))], + )], + ))] + ); +} + +/// Verifies enum declarations retain trait uses and adaptations. +#[test] +fn parse_fragment_accepts_enum_trait_use() { + let program = parse_fragment( + br#"enum EvalTraitEnum { + use EvalEnumTrait { + label as private hiddenLabel; + } + case Ready; +}"#, + ) + .expect("parse eval enum with trait use"); + + let [EvalStmt::EnumDecl(enum_decl)] = program.statements() else { + panic!("fragment should contain one enum declaration"); + }; + assert_eq!(enum_decl.traits(), &["EvalEnumTrait".to_string()]); + assert_eq!( + enum_decl.trait_adaptations(), + &[EvalTraitAdaptation::Alias { + trait_name: None, + method: "label".to_string(), + alias: Some("hiddenLabel".to_string()), + visibility: Some(EvalVisibility::Private), + }] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/exceptions_control.rs b/crates/elephc-magician/src/parser/tests/exceptions_control.rs new file mode 100644 index 0000000000..b2edbffc3e --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/exceptions_control.rs @@ -0,0 +1,286 @@ +//! Purpose: +//! Parser tests for while, do-while, break, continue, return, throw, try/catch/finally, and unset. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover control-transfer and exception statement parsing. + +use super::support::*; + +/// Verifies while fragments lower to loop statements with a nested block. +#[test] +fn parse_fragment_accepts_while_source() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + }] + ); +} +/// Verifies do/while fragments lower to body-first loop statements. +#[test] +fn parse_fragment_accepts_do_while_source() { + let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DoWhile { + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + condition: EvalExpr::LoadVar("flag".to_string()), + }] + ); +} +/// Verifies loop control statements parse inside while blocks. +#[test] +fn parse_fragment_accepts_break_and_continue_source() { + let program = + parse_fragment(br#"while ($flag) { continue; break; }"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![EvalStmt::Continue, EvalStmt::Break], + }] + ); +} +/// Verifies return fragments parse optional return expressions. +#[test] +fn parse_fragment_accepts_return_source() { + let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }))] + ); +} +/// Verifies throw statements lower to a Throwable expression carried by EvalIR. +#[test] +fn parse_fragment_accepts_throw_source() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })] + ); +} +/// Verifies try/catch statements lower supported Throwable clauses into EvalIR. +#[test] +fn parse_fragment_accepts_try_catch_throwable_source() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies class imports can alias the supported Throwable catch type. +#[test] +fn parse_fragment_accepts_try_catch_imported_throwable_alias() { + let program = parse_fragment( + br#"use Throwable as T; +try { + throw $e; +} catch (T $caught) { + echo "caught"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "caught".to_string() + )))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies Throwable catch clauses can omit the catch variable like PHP. +#[test] +fn parse_fragment_accepts_try_catch_without_variable() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: None, + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies single catch type narrowing lowers into EvalIR. +#[test] +fn parse_fragment_accepts_specific_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies union catch type narrowing lowers all source-order types into one clause. +#[test] +fn parse_fragment_accepts_union_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable|Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string(), "Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies try/finally statements lower the finalizer block into EvalIR. +#[test] +fn parse_fragment_accepts_eval_finally_source() { + let program = parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + catches: Vec::new(), + finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "finally".to_string() + )))], + }] + ); +} +/// Verifies unset fragments expand variable, array-access, and object-property operands. +#[test] +fn parse_fragment_accepts_unset_source() { + let program = parse_fragment(br#"unset($x, $this->name, $box["k"], $y);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetVar { + name: "x".to_string() + }, + EvalStmt::UnsetProperty { + object: EvalExpr::LoadVar("this".to_string()), + property: "name".to_string(), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::LoadVar("box".to_string()), + index: EvalExpr::Const(EvalConst::String("k".to_string())), + }, + EvalStmt::UnsetVar { + name: "y".to_string() + }, + ] + ); +} +/// Verifies eval fragments reject PHP opening tags. +#[test] +fn parse_fragment_rejects_opening_tag() { + assert_eq!( + parse_fragment(b" 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Spaceship, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies loose equality operators parse as binary EvalIR expressions. +#[test] +fn parse_fragment_accepts_loose_equality_source() { + let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), + }))] + ); +} +/// Verifies strict equality operators parse as distinct EvalIR comparisons. +#[test] +fn parse_fragment_accepts_strict_equality_source() { + let program = + parse_fragment(br#"return "10" === "10" && "10" !== 10;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::Int(10))), + }), + }))] + ); +} +/// Verifies static `instanceof` parses as a high-precedence EvalIR expression. +#[test] +fn parse_fragment_accepts_static_instanceof_source() { + let program = + parse_fragment(br#"return !$object instanceof App\Box;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::ClassName("App\\Box".to_string()), + }), + }))] + ); +} + +/// Verifies dynamic `instanceof` targets parse from variables, properties, arrays, and parens. +#[test] +fn parse_fragment_accepts_dynamic_instanceof_targets() { + let program = parse_fragment( + br#"return $object instanceof $names[0] . ":" . ($object instanceof ($prefix . $suffix));"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::Expr(Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("names".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + })), + }), + right: Box::new(EvalExpr::Const(EvalConst::String(":".to_string()))), + }), + right: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::Expr(Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("prefix".to_string())), + right: Box::new(EvalExpr::LoadVar("suffix".to_string())), + })), + }), + }))] + ); +} + +/// Verifies scalar cast syntax parses with PHP cast precedence across concatenation. +#[test] +fn parse_fragment_accepts_scalar_cast_source() { + let program = + parse_fragment(br#"return (string)$value . "!";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Cast { + target: EvalCastType::String, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("value".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("!".to_string()))), + }), + }))] + ); +} + +/// Verifies logical operators parse with `&&` binding tighter than `||`. +#[test] +fn parse_fragment_accepts_short_circuit_logical_source() { + let program = parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies PHP logical keywords parse case-insensitively with their own precedence. +#[test] +fn parse_fragment_accepts_keyword_logical_source() { + let program = + parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies PHP `xor` binds between `or` and `and` in eval expressions. +#[test] +fn parse_fragment_accepts_keyword_xor_source() { + let program = + parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies ternary expressions parse below logical OR and preserve both branches. +#[test] +fn parse_fragment_accepts_ternary_source() { + let program = + parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} +/// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. +#[test] +fn parse_fragment_accepts_short_ternary_source() { + let program = parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::LoadVar("name".to_string())), + then_branch: None, + else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }))] + ); +} +/// Verifies null coalescing parses as a right-associative expression. +#[test] +fn parse_fragment_accepts_null_coalesce_source() { + let program = + parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("b".to_string())), + default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }), + }))] + ); +} +/// Verifies match expressions preserve subject, patterns, and default expression. +#[test] +fn parse_fragment_accepts_match_source() { + let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Match { + subject: Box::new(EvalExpr::LoadVar("x".to_string())), + arms: vec![EvalMatchArm { + patterns: vec![ + EvalExpr::Const(EvalConst::Int(1)), + EvalExpr::Const(EvalConst::Int(2)), + ], + value: EvalExpr::Const(EvalConst::String("small".to_string())), + }], + default: Some(Box::new(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))), + }))] + ); +} +/// Verifies null coalescing binds tighter than PHP ternary expressions. +#[test] +fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { + let program = + parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} +/// Verifies logical negation parses as a unary expression before comparisons. +#[test] +fn parse_fragment_accepts_logical_not_source() { + let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseEq, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::LoadVar("flag".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }))] + ); +} +/// Verifies unary numeric operators bind tighter than multiplication. +#[test] +fn parse_fragment_accepts_unary_numeric_source() { + let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::LoadVar("x".to_string())), + }), + right: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs b/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs new file mode 100644 index 0000000000..21eb840ec6 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs @@ -0,0 +1,301 @@ +//! Purpose: +//! Parser tests for static declarations, calls, first-class callables, dynamic +//! receivers, and expression receivers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `::` receivers lower to EvalIR static-member nodes. + +use super::super::support::*; + +/// Verifies static class members lower with explicit static metadata. +#[test] +fn parse_fragment_accepts_static_class_members() { + let program = parse_fragment( + br#"class EvalStaticBox { + public static int $count = 1; + public static function read() { return self::$count; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "EvalStaticBox", + vec![EvalClassProperty::with_visibility_and_static( + "count", + EvalVisibility::Public, + true, + Some(EvalExpr::Const(EvalConst::Int(1))), + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )))], + vec![EvalClassMethod::with_visibility_and_modifiers( + "read", + EvalVisibility::Public, + true, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::StaticPropertyGet { + class_name: "self".to_string(), + property: "count".to_string(), + }))], + )], + ))] + ); +} + +/// Verifies static method calls lower to EvalIR call expressions. +#[test] +fn parse_fragment_accepts_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "Read".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies first-class static method syntax retains static callable metadata. +#[test] +fn parse_fragment_accepts_first_class_static_method_callable_source() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCallable { + class_name: "EvalStaticBox".to_string(), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + }))] + ); +} + +/// Verifies static method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "Read".to_string(), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + +/// Verifies runtime-valued static receivers lower to dynamic static method calls. +#[test] +fn parse_fragment_accepts_dynamic_static_method_receiver() { + let program = + parse_fragment(br#"return $class::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + +/// Verifies runtime-valued static receivers support variable method names. +#[test] +fn parse_fragment_accepts_dynamic_static_method_name() { + let program = + parse_fragment(br#"return $class::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies named static receivers support variable method names. +#[test] +fn parse_fragment_accepts_named_receiver_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies braced dynamic static method names preserve their method expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$method}(2) . $class::{"Read"}(3);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + right: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(3)))], + }), + }))] + ); +} + +/// Verifies braced dynamic class constant names preserve their constant-name expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_class_constant_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$constant} . $class::{"VALUE"};"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + constant: Box::new(EvalExpr::LoadVar("constant".to_string())), + }), + right: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + }))] + ); +} + +/// Verifies parenthesized static receivers parse through the dynamic receiver path. +#[test] +fn parse_fragment_accepts_expression_static_receiver_members() { + let program = parse_fragment( + br#"return [($class)::read(2), (factory())::VALUE, (factory())::{"VALUE"}, (factory())::$count];"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(factory_call()), + constant: "VALUE".to_string(), + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(factory_call()), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + ])))] + ); +} + +/// Verifies expression static receivers support property write statement lowering. +#[test] +fn parse_fragment_accepts_expression_static_receiver_property_writes() { + let program = parse_fragment( + br#"(factory())::$count = 2; +(factory())::$count += 3; +(factory())::$items[] = 4; +(factory())::$items[0] = 5; +(factory())::$count++; +++ (factory())::$count; +-- (factory())::$count;"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: factory_call(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + EvalStmt::DynamicStaticPropertyArraySet { + class_name: factory_call(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::Int(5)), + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: false, + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs b/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs new file mode 100644 index 0000000000..e0618d5a23 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Parser tests for dynamic static-property names and runtime-valued metadata +//! receivers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Braced runtime names lower to explicit EvalIR name expressions. + +use super::super::support::*; + +/// Verifies `${...}` static property names parse as runtime-name expressions. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_expressions() { + let program = parse_fragment( + br#"return [EvalStaticBox::${$property}, $class::${name_expr()}, (factory())::${"items"}];"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + property: Box::new(EvalExpr::LoadVar("property".to_string())), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: Box::new(name_expr_call()), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(factory_call()), + property: Box::new(EvalExpr::Const(EvalConst::String("items".to_string()))), + }), + ])))] + ); +} + +/// Verifies `${...}` static property names lower write-like statements. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_writes() { + let program = parse_fragment( + br#"EvalStaticBox::${$property} = 2; +++$class::${name_expr()}; +(factory())::${"items"}[] = 4;"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyNameSet { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("property".to_string()), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: name_expr_call(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: factory_call(), + property: EvalExpr::Const(EvalConst::String("items".to_string())), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + ] + ); +} + +/// Verifies runtime-valued static receivers support properties, constants, and `::class`. +#[test] +fn parse_fragment_accepts_dynamic_static_metadata_receiver() { + let program = parse_fragment(br#"return $class::$count . $class::VALUE . $object::class;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: "VALUE".to_string(), + }), + }), + right: Box::new(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(EvalExpr::LoadVar("object".to_string())), + }), + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members/mod.rs b/crates/elephc-magician/src/parser/tests/static_members/mod.rs new file mode 100644 index 0000000000..c49122625c --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/mod.rs @@ -0,0 +1,13 @@ +//! Purpose: +//! Organizes parser tests for static calls, receivers, dynamic member names, and +//! static-property mutations. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod calls_receivers; +mod dynamic_names; +mod property_mutations; diff --git a/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs b/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs new file mode 100644 index 0000000000..48d04cae13 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs @@ -0,0 +1,279 @@ +//! Purpose: +//! Parser tests for static-property assignment, compound operations, unset, +//! increment/decrement, reference binding, and array writes. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Named and dynamic class/property receivers retain dedicated EvalIR forms. + +use super::super::support::*; + +/// Verifies runtime-valued static receivers support property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_assignment() { + let program = parse_fragment(br#"$class::$count = 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + +/// Verifies dynamic static property compound assignments lower to read-modify-write EvalIR. +#[test] +fn parse_fragment_accepts_dynamic_static_property_compound_assignment() { + let program = parse_fragment(br#"$class::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} + +/// Verifies static property unsets parse as explicit static-unset statements. +#[test] +fn parse_fragment_accepts_static_property_unset() { + let program = + parse_fragment(br#"unset(EvalStaticBox::$count);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetStaticProperty { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }] + ); +} + +/// Verifies runtime-valued static property unsets preserve the class receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_unset() { + let program = parse_fragment(br#"unset($class::$count, $class::${$name});"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetDynamicStaticProperty { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + }, + EvalStmt::UnsetDynamicStaticPropertyName { + class_name: EvalExpr::LoadVar("class".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }, + ] + ); +} + +/// Verifies static-property array element unset preserves the static member expression. +#[test] +fn parse_fragment_accepts_static_property_array_unset() { + let program = parse_fragment(br#"unset(EvalStaticBox::$items[0], $class::$items[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} + +/// Verifies static property increment/decrement parse as dedicated member updates. +#[test] +fn parse_fragment_accepts_static_property_inc_dec() { + let program = parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyIncDec { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::StaticPropertyIncDec { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic static property increment/decrement preserves the receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_inc_dec() { + let program = + parse_fragment(br#"$class::$count++; --$class::$count;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies static property compound assignments lower to one read-modify-write statement. +#[test] +fn parse_fragment_accepts_static_property_compound_assignment() { + let program = parse_fragment(br#"EvalStaticBox::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertySet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} + +/// Verifies static property reference bindings parse for named and dynamic targets. +#[test] +fn parse_fragment_accepts_static_property_reference_bind_source() { + let program = parse_fragment( + br#"EvalStaticBox::$count =& $source; +$class::$count =& $dynamic; +EvalStaticBox::${$name} =& $other; +(factory())::${$name} =& $third;"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyReferenceBind { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + source: "dynamic".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "third".to_string(), + }, + ] + ); +} + +/// Verifies indexed static-property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_static_property_array_write_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] = "x"; EvalStaticBox::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::StaticPropertyArrayAppend { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies indexed static-property compound assignment retains the static target. +#[test] +fn parse_fragment_accepts_static_property_array_compound_assignment_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] .= "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Concat), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} + +/// Verifies runtime-valued static receivers support indexed static-property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_array_write_source() { + let program = parse_fragment(br#"$class::$items[0] = "x"; $class::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyArraySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/support.rs b/crates/elephc-magician/src/parser/tests/support.rs new file mode 100644 index 0000000000..05aff54f2d --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/support.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Shared imports for parser unit tests. +//! The focused test modules compare parser output against EvalIR structures +//! without repeating the parser and IR imports in each file. +//! +//! Called from: +//! - `crate::parser::tests::*` focused parser test modules. +//! +//! Key details: +//! - Re-exports are limited to parser tests through `pub(super)`. + +pub(super) use super::super::cursor::inc_dec_store; +pub(super) use super::super::parse_fragment; +pub(super) use crate::errors::EvalParseError; +pub(super) use crate::eval_ir::*; diff --git a/crates/elephc-magician/src/parser/tests/trait_adaptations.rs b/crates/elephc-magician/src/parser/tests/trait_adaptations.rs new file mode 100644 index 0000000000..f68a2b03f1 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/trait_adaptations.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Parser tests for eval class trait adaptation syntax. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `insteadof` and `as` clauses lower into class metadata. + +use super::support::*; + +/// Verifies trait `insteadof`, alias, and visibility adaptations are parsed. +#[test] +fn parse_fragment_accepts_trait_adaptations() { + let program = parse_fragment( + br#"class EvalAdaptBox { + use EvalAdaptA, EvalAdaptB { + EvalAdaptA::talk insteadof EvalAdaptB; + EvalAdaptB::talk as talkB; + EvalAdaptA::hidden as private; + talk as public talkPublic; + } +}"#, + ) + .expect("fragment should parse"); + + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_adaptations_and_constants( + "EvalAdaptBox", + false, + false, + None, + Vec::new(), + vec!["EvalAdaptA".to_string(), "EvalAdaptB".to_string()], + vec![ + EvalTraitAdaptation::InsteadOf { + trait_name: Some("EvalAdaptA".to_string()), + method: "talk".to_string(), + instead_of: vec!["EvalAdaptB".to_string()], + }, + EvalTraitAdaptation::Alias { + trait_name: Some("EvalAdaptB".to_string()), + method: "talk".to_string(), + alias: Some("talkB".to_string()), + visibility: None, + }, + EvalTraitAdaptation::Alias { + trait_name: Some("EvalAdaptA".to_string()), + method: "hidden".to_string(), + alias: None, + visibility: Some(EvalVisibility::Private), + }, + EvalTraitAdaptation::Alias { + trait_name: None, + method: "talk".to_string(), + alias: Some("talkPublic".to_string()), + visibility: Some(EvalVisibility::Public), + }, + ], + Vec::new(), + Vec::new(), + Vec::new(), + ) + )] + ); +} diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs new file mode 100644 index 0000000000..80b7e88881 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -0,0 +1,385 @@ +//! Purpose: +//! Declares generated C-ABI runtime wrapper symbols consumed by eval hooks. +//! These declarations are grouped separately so operation code can stay focused +//! on RuntimeValueOps behavior rather than linkage inventory. +//! +//! Called from: +//! - `crate::runtime_hooks::ops` runtime adapter methods. +//! - `crate::runtime_hooks::ElephcRuntimeOps` shared argument packing helpers. +//! +//! Key details: +//! - Symbols are provided by the main elephc runtime object when eval is enabled. +//! - Null return pointers are translated to `EvalStatus::RuntimeFatal` by callers. + +use std::ffi::c_void; + +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +#[cfg(not(test))] +unsafe extern "C" { + pub(super) fn __elephc_eval_value_array_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string_array_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string_array_push( + array: *mut RuntimeCell, + value_ptr: *const u8, + value_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_assoc_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_get( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_key_exists( + key: *mut RuntimeCell, + array: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_iter_key( + array: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_set( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + value: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_property_get( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_property_is_initialized( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_property_set( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + value: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_static_property_get( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_static_property_is_initialized( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_static_property_set( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + value: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_class_constant_get( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> *mut RuntimeCell; + /// Returns a boxed shallow clone for stdClass/eval object storage. + pub(super) fn __elephc_eval_value_object_clone_shallow( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; + /// Returns a boxed Mixed object cell for a borrowed raw object payload. + pub(super) fn __elephc_eval_value_object_from_raw( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_object_property_iter_key( + object: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_method_call( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, + context: *const c_void, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_static_method_call( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, + context: *const c_void, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_attribute_new( + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + target: u64, + repeated: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_owner_new( + owner_kind: u64, + name_ptr: *const u8, + name_len: u64, + attrs: *mut RuntimeCell, + interface_names: *mut RuntimeCell, + trait_names: *mut RuntimeCell, + method_names: *mut RuntimeCell, + property_names: *mut RuntimeCell, + method_objects: *mut RuntimeCell, + property_objects: *mut RuntimeCell, + parent_class: *mut RuntimeCell, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: *mut RuntimeCell, + backing_value: *mut RuntimeCell, + constructor: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_method_flags( + class_ptr: *const u8, + class_len: u64, + method_ptr: *const u8, + method_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_reflection_method_declaring_class( + class_ptr: *const u8, + class_len: u64, + method_ptr: *const u8, + method_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_method_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_source_file() -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_flags( + class_ptr: *const u8, + class_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_reflection_property_flags( + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_reflection_property_declaring_class( + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_property_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_value( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_flags( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_reflection_constant_declaring_class( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_interface_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_alias_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_alias_sources( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_new_object( + name_ptr: *const u8, + name_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_construct_object( + object: *mut RuntimeCell, + args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, + context: *const c_void, + ) -> u64; + pub(super) fn __elephc_eval_value_take_pending_throwable() -> *mut RuntimeCell; + pub(super) fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_value_is_a( + object_or_class: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_object_class_name( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_parent_class_name( + object_or_class: *mut RuntimeCell, + ) -> *mut RuntimeCell; + /// Returns whether generated trait metadata contains the requested PHP name. + pub(super) fn __elephc_eval_trait_exists(name_ptr: *const u8, name_len: u64) -> u64; + /// Returns whether generated enum metadata contains the requested PHP name. + pub(super) fn __elephc_eval_enum_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_invoker_ref_cell( + slot: *mut RuntimeCellHandle, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_invoker_raw_ref_cell( + slot: *mut c_void, + source_tag: u64, + ) -> *mut RuntimeCell; + /// Extracts the low raw payload word from a boxed runtime value. + pub(super) fn __elephc_eval_value_raw_word(value: *mut RuntimeCell) -> u64; + /// Extracts the high raw payload word from a boxed runtime value. + pub(super) fn __elephc_eval_value_raw_high_word(value: *mut RuntimeCell) -> u64; + /// Duplicates raw string storage for a staged native by-reference slot. + pub(super) fn __elephc_eval_value_retain_raw_string( + ptr: u64, + len: u64, + out_len: *mut u64, + ) -> u64; + /// Boxes raw string storage back into a runtime value for eval writeback. + pub(super) fn __elephc_eval_value_from_raw_string(ptr: u64, len: u64) -> *mut RuntimeCell; + /// Releases raw string storage owned by a staged native by-reference slot. + pub(super) fn __elephc_eval_value_release_raw_string(ptr: u64, len: u64); + /// Retains one raw heap payload word for a staged native by-reference slot. + pub(super) fn __elephc_eval_value_retain_raw_heap_word(word: u64) -> u64; + /// Boxes one one-word raw payload back into a runtime value using a known tag. + pub(super) fn __elephc_eval_value_from_raw_word( + source_tag: u64, + word: u64, + ) -> *mut RuntimeCell; + /// Boxes one raw heap payload word back into a runtime value. + pub(super) fn __elephc_eval_value_from_raw_heap_word(word: u64) -> *mut RuntimeCell; + /// Releases one raw heap payload word owned by a staged by-reference slot. + pub(super) fn __elephc_eval_value_release_raw_heap_word(word: u64); + /// Returns the unboxed object payload pointer for object-tagged eval values. + pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); + pub(super) fn __elephc_eval_value_null() -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_resource(value: i64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_int(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_float(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_strrev(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_fdiv( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_fmod( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_add( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_sub( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_mul( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_div( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_mod( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_pow( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_round( + value: *mut RuntimeCell, + precision: *mut RuntimeCell, + has_precision: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bitwise( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bit_not(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_concat( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_compare( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_spaceship( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_echo(value: *mut RuntimeCell); + pub(super) fn __elephc_eval_value_string_bytes( + value: *mut RuntimeCell, + out_ptr: *mut *const u8, + out_len: *mut u64, + ) -> u64; + pub(super) fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_final_object_identity(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_release(value: *mut RuntimeCell); + pub(super) fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; + /// Installs the optional eval dynamic object destructor callback. + pub(super) fn __elephc_eval_install_dynamic_object_destructor_hook(callback: usize); +} diff --git a/crates/elephc-magician/src/runtime_hooks/mod.rs b/crates/elephc-magician/src/runtime_hooks/mod.rs new file mode 100644 index 0000000000..f6b2447326 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/mod.rs @@ -0,0 +1,100 @@ +//! Purpose: +//! Bridges EvalIR value operations to elephc runtime values. +//! The module wires the stateless adapter type while focused submodules own +//! generated C-ABI symbols, trait operations, and opcode mappings. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` in non-test builds. +//! +//! Key details: +//! - The wrapper symbols adapt to elephc's target-specific internal helper ABI. +//! - Unit tests do not link the generated runtime object, so real hooks compile +//! only outside `cfg(test)`. + +#[cfg(not(test))] +mod externs; +#[cfg(not(test))] +mod ops; +#[cfg(not(test))] +mod tags; + +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::abi::ElephcEvalContext; +#[cfg(not(test))] +use crate::value::{RuntimeCell, RuntimeCellHandle}; +#[cfg(not(test))] +use externs::{ + __elephc_eval_install_dynamic_object_destructor_hook, __elephc_eval_value_array_new, + __elephc_eval_value_array_set, __elephc_eval_value_int, __elephc_eval_value_object_from_raw, +}; + +/// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. +#[cfg(not(test))] +pub struct ElephcRuntimeOps { + context: *const ElephcEvalContext, +} + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Creates a runtime hook adapter without caller-sensitive eval context. + pub const fn new() -> Self { + Self { + context: std::ptr::null(), + } + } + + /// Creates a runtime hook adapter that can expose the active class scope to generated helpers. + pub const fn with_context(context: *const ElephcEvalContext) -> Self { + Self { context } + } + + /// Converts a runtime wrapper result into an interpreter handle. + pub(crate) fn handle(ptr: *mut RuntimeCell) -> Result { + if ptr.is_null() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(RuntimeCellHandle::from_raw(ptr)) + } + } + + /// Boxes one borrowed raw object payload for eval `$this` dispatch. + pub(crate) fn object_from_raw( + object: *mut RuntimeCell, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_from_raw(object) }) + } + + /// Packs source-order argument cells into the boxed eval array ABI. + fn arg_array(args: Vec) -> Result { + let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; + let mut arg_array = Self::handle(arg_array)?; + for (index, value) in args.into_iter().enumerate() { + let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; + arg_array = Self::handle(unsafe { + __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) + })?; + } + Ok(arg_array) + } + + /// Returns the active eval class-scope bytes in the generated helper ABI shape. + fn current_class_scope_abi(&self) -> (*const u8, u64) { + let Some(context) = (unsafe { self.context.as_ref() }) else { + return (std::ptr::null(), 0); + }; + let Some(class_scope) = context.current_class_scope() else { + return (std::ptr::null(), 0); + }; + (class_scope.as_ptr(), class_scope.len() as u64) + } +} + +/// Installs the eval dynamic object destructor callback into runtime data. +#[cfg(not(test))] +pub(crate) unsafe fn install_dynamic_object_destructor_hook(callback: usize) { + unsafe { + __elephc_eval_install_dynamic_object_destructor_hook(callback); + } +} diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs new file mode 100644 index 0000000000..76d0b678ab --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Implements RuntimeValueOps by delegating each eval value operation to the +//! generated elephc runtime wrapper symbols. +//! +//! Called from: +//! - `crate::interpreter` when executing EvalIR in non-test builds. +//! +//! Key details: +//! - Every returned runtime pointer is checked before becoming a handle. +//! - Temporary argument arrays are released after object and method bridge calls. + +use super::externs::*; +use super::tags::{bitwise_op_tag, compare_op_tag}; +use super::ElephcRuntimeOps; +use crate::errors::EvalStatus; +use crate::eval_ir::EvalBinOp; +use crate::interpreter::RuntimeValueOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +mod collection_calls; +mod construction_raw; +mod lifecycle_scalars; +mod native_results; +mod numeric_string; +mod reflection; + +use collection_calls::impl_collection_call_ops; +use construction_raw::impl_construction_raw_ops; +use lifecycle_scalars::impl_lifecycle_scalar_ops; +use numeric_string::impl_numeric_string_ops; +use reflection::impl_reflection_ops; + +#[cfg(not(test))] +impl RuntimeValueOps for ElephcRuntimeOps { + impl_collection_call_ops!(); + impl_reflection_ops!(); + impl_construction_raw_ops!(); + impl_lifecycle_scalar_ops!(); + impl_numeric_string_ops!(); +} diff --git a/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs b/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs new file mode 100644 index 0000000000..e5e4cf4ea9 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs @@ -0,0 +1,323 @@ +//! Purpose: +//! Defines the array, property, object-call, static-call, and native-result +//! methods of the generated-runtime `RuntimeValueOps` adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Temporary argument arrays are released after bridge calls. + +macro_rules! impl_collection_call_ops { + () => { + /// Creates a boxed Mixed indexed array through the generated runtime wrapper. + fn array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_new(capacity as u64) }) + } + + /// Creates a boxed Mixed indexed array whose payload uses direct string slots. + fn string_array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_string_array_new(capacity as u64) }) + } + + /// Appends one string to a boxed direct-string indexed array. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_string_array_push( + array.as_ptr(), + value.as_ptr(), + value.len() as u64, + ) + }) + } + + /// Creates a boxed Mixed associative array through the generated runtime wrapper. + fn assoc_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_assoc_new(capacity as u64) }) + } + + /// Reads one element from a boxed Mixed array through the generated runtime wrapper. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) + } + + /// Checks whether a boxed Mixed array contains a normalized PHP key. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_key_exists(key.as_ptr(), array.as_ptr()) }) + } + + /// Returns one foreach-visible key from a boxed Mixed array by iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_iter_key(array.as_ptr(), position as u64) }) + } + + /// Writes one element to a boxed Mixed array through the generated runtime wrapper. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_array_set(array.as_ptr(), index.as_ptr(), value.as_ptr()) + }) + } + + /// Reads a boxed Mixed object property through the generated user helper. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + Self::handle(unsafe { + __elephc_eval_value_property_get( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }) + } + + /// Checks an AOT instance property initialization marker through the generated helper. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_property_is_initialized( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + + /// Writes a boxed Mixed object property through the generated user helper. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ok = unsafe { + __elephc_eval_value_property_set( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + scope_ptr, + scope_len, + ) + }; + if ok == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } + } + + /// Reads an AOT static property through the generated user helper. + fn static_property_get( + &mut self, + class_name: &str, + property: &str, + ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ptr = unsafe { + __elephc_eval_value_static_property_get( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Checks an AOT static property initialization marker through the generated helper. + fn static_property_is_initialized( + &mut self, + class_name: &str, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_static_property_is_initialized( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + + /// Writes an AOT static property through the generated user helper. + fn static_property_set( + &mut self, + class_name: &str, + property: &str, + value: RuntimeCellHandle, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ok = unsafe { + __elephc_eval_value_static_property_set( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + scope_ptr, + scope_len, + ) + }; + Ok(ok != 0) + } + + /// Reads an AOT class-like constant through the generated user helper. + fn class_constant_get( + &mut self, + class_name: &str, + constant: &str, + ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ptr = unsafe { + __elephc_eval_value_class_constant_get( + class_name.as_ptr(), + class_name.len() as u64, + constant.as_ptr(), + constant.len() as u64, + scope_ptr, + scope_len, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_clone_shallow(object.as_ptr()) }) + } + + /// Returns the JSON-visible public property count for a boxed Mixed object. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns one JSON-visible public property key for a boxed Mixed object. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_object_property_iter_key(object.as_ptr(), position as u64) + }) + } + + /// Calls a boxed Mixed object method through the generated user helper. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let result = unsafe { + __elephc_eval_value_method_call( + object.as_ptr(), + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + self.handle_native_call_result(result) + } + + /// Calls an AOT static method through the generated user helper. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let result = unsafe { + __elephc_eval_value_static_method_call( + class_name.as_ptr(), + class_name.len() as u64, + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + self.handle_native_call_result(result) + } + + /// Converts a native free-function result into eval status, preserving pending throwables. + fn native_call_result( + &mut self, + result: RuntimeCellHandle, + ) -> Result { + self.handle_native_call_result(result.as_ptr()) + } + + }; +} + +pub(super) use impl_collection_call_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs b/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs new file mode 100644 index 0000000000..14ef19e2aa --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs @@ -0,0 +1,214 @@ +//! Purpose: +//! Defines object construction, class-like queries, raw value conversion, and +//! raw heap/string ownership methods for the runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Raw retain/release pairs preserve the generated runtime ownership contract. + +macro_rules! impl_construction_raw_ops { + () => { + + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. + fn new_object(&mut self, class_name: &str) -> Result { + let object = Self::handle(unsafe { + __elephc_eval_value_new_object(class_name.as_ptr(), class_name.len() as u64) + })?; + match self.is_null(object) { + Ok(false) => Ok(object), + Ok(true) => { + self.release(object)?; + Err(EvalStatus::RuntimeFatal) + } + Err(err) => { + let _ = self.release(object); + Err(err) + } + } + } + + /// Calls an AOT constructor through the generated user bridge when one exists. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let ok = unsafe { + __elephc_eval_value_construct_object( + object.as_ptr(), + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + if ok == 0 { + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) + } else { + Ok(()) + } + } + + /// Returns whether the generated AOT class-name table contains the requested class. + fn class_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT interface-name table contains the requested interface. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT trait-name table contains the requested trait. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_trait_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT enum-name table contains the requested enum. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_enum_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Tests a boxed Mixed object against generated class/interface metadata. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + Ok(unsafe { + __elephc_eval_value_is_a( + object_or_class.as_ptr(), + target_class.as_ptr(), + target_class.len() as u64, + u64::from(exclude_self), + ) != 0 + }) + } + + /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_class_name(object.as_ptr()) }) + } + + /// Returns a boxed Mixed string naming a boxed Mixed object's or class string's parent class. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_parent_class_name(object_or_class.as_ptr()) }) + } + + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns whether a boxed Mixed cell has an array-like runtime tag. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) + } + + /// Returns whether a boxed Mixed cell unwraps to PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_null(value.as_ptr()) != 0 }) + } + + /// Returns the unboxed Mixed runtime tag for PHP type-predicate builtins. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) + } + + /// Creates an invoker-only by-reference marker for a staged Mixed slot. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_ref_cell(slot) }) + } + + /// Creates an invoker-only by-reference marker for a staged raw one-word slot. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + source_tag: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_raw_ref_cell(slot, source_tag) }) + } + + /// Extracts the low raw payload word from a boxed Mixed cell. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) + } + + /// Extracts the high raw payload word from a boxed Mixed cell. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_high_word(value.as_ptr()) }) + } + + /// Duplicates one raw string payload for owned native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + let mut out_len = 0; + let out_ptr = unsafe { __elephc_eval_value_retain_raw_string(ptr, len, &mut out_len) }; + Ok((out_ptr, out_len)) + } + + /// Boxes one raw string payload as a Mixed string cell. + fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_string(ptr, len) }) + } + + /// Releases one raw string payload owned by native by-reference staging. + fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_string(ptr, len); + } + Ok(()) + } + + /// Retains one raw heap payload word for owned native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) + } + + /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_word(source_tag, word) }) + } + + /// Boxes one raw heap payload word as a Mixed cell using its runtime heap kind. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_heap_word(word) }) + } + + /// Releases one raw heap payload word owned by native by-reference staging. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_heap_word(word); + } + Ok(()) + } + + }; +} + +pub(super) use impl_construction_raw_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs b/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs new file mode 100644 index 0000000000..8c29450b92 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! Defines object identity, retain/release, warning, scalar construction, and +//! scalar-cast methods for the generated runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Every runtime pointer is validated before it becomes a handle. + +macro_rules! impl_lifecycle_scalar_ops { + () => { + + /// Returns the unboxed object payload pointer for SPL object identity builtins. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; + if identity == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(identity) + } + } + + /// Returns the object payload that the next release would destroy, when known. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + let identity = unsafe { __elephc_eval_value_final_object_identity(value.as_ptr()) }; + Ok((identity != 0).then_some(identity)) + } + + /// Releases one boxed Mixed cell through the generated runtime wrapper. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release(value.as_ptr()); + } + Ok(()) + } + + /// Retains one boxed Mixed cell through the generated runtime wrapper. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(RuntimeCellHandle::from_raw(unsafe { + __elephc_eval_value_retain(value.as_ptr()) + })) + } + + /// Emits one PHP warning through the generated runtime diagnostic helper. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_warning(message.as_ptr(), message.len() as u64); + } + Ok(()) + } + + /// Creates a boxed null Mixed cell through the generated runtime wrapper. + fn null(&mut self) -> Result { + Self::handle(unsafe { __elephc_eval_value_null() }) + } + + /// Creates a boxed bool Mixed cell through the generated runtime wrapper. + fn bool_value(&mut self, value: bool) -> Result { + Self::handle(unsafe { __elephc_eval_value_bool(u64::from(value)) }) + } + + /// Creates a boxed int Mixed cell through the generated runtime wrapper. + fn int(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_int(value) }) + } + + /// Creates a boxed resource Mixed cell through the generated runtime wrapper. + fn resource(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_resource(value) }) + } + + /// Creates a boxed float Mixed cell through the generated runtime wrapper. + fn float(&mut self, value: f64) -> Result { + Self::handle(unsafe { __elephc_eval_value_float(value) }) + } + + /// Creates a boxed string Mixed cell through the generated runtime wrapper. + fn string(&mut self, value: &str) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + + /// Creates a boxed string Mixed cell from raw PHP bytes through the generated runtime wrapper. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + + /// Casts a boxed Mixed cell to a boxed integer Mixed cell through the generated runtime wrapper. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_int(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed float Mixed cell through the generated runtime wrapper. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_float(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed string Mixed cell through the generated runtime wrapper. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_string(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed boolean Mixed cell through the generated runtime wrapper. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_bool(value.as_ptr()) }) + } + + }; +} + +pub(super) use impl_lifecycle_scalar_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs b/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs new file mode 100644 index 0000000000..4424a05325 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Converts null native bridge results into pending Throwable state and schedules +//! escaped native exceptions for eval catch handling. +//! +//! Called from: +//! - Runtime method and constructor bridge operations. +//! +//! Key details: +//! - Null without a pending Throwable remains a runtime fatal error. + +use super::*; + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Converts a generated native method-call result into an eval result status. + pub(super) fn handle_native_call_result( + &self, + result: *mut RuntimeCell, + ) -> Result { + if !result.is_null() { + return Ok(RuntimeCellHandle::from_raw(result)); + } + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) + } + + /// Takes a native Throwable that escaped through the generated constructor bridge. + pub(super) fn take_pending_native_throwable(&self) -> Option { + let thrown = unsafe { __elephc_eval_value_take_pending_throwable() }; + if thrown.is_null() { + None + } else { + Self::object_from_raw(thrown).ok() + } + } + + /// Schedules a native Throwable so eval's ordinary catch machinery can handle it. + pub(super) fn schedule_pending_throw( + &self, + thrown: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let Some(context) = + (unsafe { (self.context as *mut crate::abi::ElephcEvalContext).as_mut() }) + else { + return Err(EvalStatus::RuntimeFatal); + }; + context.set_pending_throw(thrown); + Ok(()) + } +} diff --git a/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs b/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs new file mode 100644 index 0000000000..226d66d03b --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs @@ -0,0 +1,205 @@ +//! Purpose: +//! Defines numeric, bitwise, comparison, concatenation, output, byte, and +//! truthiness operations for the generated runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Binary and comparison op tags continue to use the shared target mappings. + +macro_rules! impl_numeric_string_ops { + () => { + + /// Computes PHP `abs()` for a boxed Mixed cell through the generated runtime wrapper. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) + } + + /// Computes PHP `ceil()` for a boxed Mixed cell through the generated runtime wrapper. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_ceil(value.as_ptr()) }) + } + + /// Computes PHP `floor()` for a boxed Mixed cell through the generated runtime wrapper. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_floor(value.as_ptr()) }) + } + + /// Computes PHP `sqrt()` for a boxed Mixed cell through the generated runtime wrapper. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) + } + + /// Computes PHP `strrev()` for a boxed Mixed cell through the generated runtime wrapper. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_strrev(value.as_ptr()) }) + } + + /// Computes PHP `fdiv()` for boxed Mixed cells through the generated runtime wrapper. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fdiv(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes PHP `fmod()` for boxed Mixed cells through the generated runtime wrapper. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fmod(left.as_ptr(), right.as_ptr()) }) + } + + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_add(left.as_ptr(), right.as_ptr()) }) + } + + /// Subtracts two boxed Mixed cells using elephc runtime numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_sub(left.as_ptr(), right.as_ptr()) }) + } + + /// Multiplies two boxed Mixed cells using elephc runtime numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mul(left.as_ptr(), right.as_ptr()) }) + } + + /// Divides two boxed Mixed cells using elephc runtime numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_div(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes modulo for two boxed Mixed cells using elephc runtime integer semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) + } + + /// Raises two boxed Mixed cells using elephc runtime numeric exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_pow(left.as_ptr(), right.as_ptr()) }) + } + + /// Rounds a boxed Mixed cell through the generated runtime wrapper. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let (precision, has_precision) = if let Some(precision) = precision { + (precision.as_ptr(), 1) + } else { + (core::ptr::null_mut(), 0) + }; + Self::handle(unsafe { __elephc_eval_value_round(value.as_ptr(), precision, has_precision) }) + } + + /// Applies an integer bitwise or shift operation through the generated runtime wrapper. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_bitwise(left.as_ptr(), right.as_ptr(), bitwise_op_tag(op)) + }) + } + + /// Applies integer bitwise NOT through the generated runtime wrapper. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_bit_not(value.as_ptr()) }) + } + + /// Concatenates two boxed Mixed cells using elephc runtime string semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_concat(left.as_ptr(), right.as_ptr()) }) + } + + /// Compares two boxed Mixed cells through the generated runtime wrapper. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_compare(left.as_ptr(), right.as_ptr(), compare_op_tag(op)) + }) + } + + /// Computes a PHP numeric spaceship result through the generated runtime wrapper. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_spaceship(left.as_ptr(), right.as_ptr()) }) + } + + /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_echo(value.as_ptr()); + } + Ok(()) + } + + /// Casts one boxed Mixed cell to a PHP string and copies the bytes into Rust memory. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + let mut ptr = std::ptr::null(); + let mut len = 0; + let ok = unsafe { __elephc_eval_value_string_bytes(value.as_ptr(), &mut ptr, &mut len) }; + if ok == 0 || (len > 0 && ptr.is_null()) { + return Err(EvalStatus::RuntimeFatal); + } + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(ptr, len) } + }; + Ok(bytes.to_vec()) + } + + /// Converts one boxed Mixed cell to PHP truthiness through the generated runtime wrapper. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) + } + + }; +} + +pub(super) use impl_numeric_string_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs b/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs new file mode 100644 index 0000000000..eea14a9858 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs @@ -0,0 +1,330 @@ +//! Purpose: +//! Defines reflection object materialization and metadata-query methods for the +//! generated-runtime `RuntimeValueOps` adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Nullable wrapper results are mapped to `Option` without manufacturing handles. + +macro_rules! impl_reflection_ops { + () => { + + /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_attribute_new( + name.as_ptr(), + name.len() as u64, + args.as_ptr(), + target, + if repeated { 1 } else { 0 }, + ) + }) + } + + /// Materializes a populated synthetic Reflection owner object for eval metadata. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_owner_new( + owner_kind, + reflected_name.as_ptr(), + reflected_name.len() as u64, + attrs.as_ptr(), + interface_names.as_ptr(), + trait_names.as_ptr(), + method_names.as_ptr(), + property_names.as_ptr(), + method_objects.as_ptr(), + property_objects.as_ptr(), + parent_class.as_ptr(), + flags, + modifiers, + method_modifiers, + constant_value.as_ptr(), + backing_value.as_ptr(), + constructor.as_ptr(), + ) + }) + } + + /// Returns generated AOT ReflectionMethod flags, or `None` when no row matches. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_method_flags( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionMethod declaring class metadata. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_method_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionMethod names visible for one class. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_method_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT source-file metadata for reflection source-location calls. + fn reflection_source_file(&mut self) -> Result, EvalStatus> { + let ptr = unsafe { __elephc_eval_reflection_source_file() }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionClass modifier flags, or `None` when no row matches. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_class_flags(class_name.as_ptr(), class_name.len() as u64) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_property_flags( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionProperty declaring class metadata. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_property_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionProperty names visible for one class. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_property_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT ReflectionClassConstant values without visibility checks. + fn reflection_constant_value( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_value( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Returns generated AOT ReflectionClassConstant flags for one constant. + fn reflection_constant_flags( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_constant_flags( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionClassConstant declaring class metadata. + fn reflection_constant_declaring_class( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionClassConstant names visible for one class. + fn reflection_constant_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_constant_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT interface names visible for one reflected class-like symbol. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_interface_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait names visible for one reflected class-like symbol. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait alias names visible for one reflected class-like symbol. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait alias sources visible for one reflected class-like symbol. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_sources( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + }; +} + +pub(super) use impl_reflection_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/tags.rs b/crates/elephc-magician/src/runtime_hooks/tags.rs new file mode 100644 index 0000000000..f37246bce1 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/tags.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Maps EvalIR binary operators onto generated runtime wrapper opcode tables. +//! The runtime wrappers expect compact numeric tags rather than Rust enum +//! discriminants. +//! +//! Called from: +//! - `crate::runtime_hooks::ops` comparison and bitwise operations. +//! +//! Key details: +//! - Non-matching operators map to zero because callers only pass matching groups. + +use crate::eval_ir::EvalBinOp; + +/// Maps an EvalIR comparison operator to the bridge ABI opcode. +#[cfg(not(test))] +pub(super) fn compare_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::LooseEq => 0, + EvalBinOp::LooseNotEq => 1, + EvalBinOp::Lt => 2, + EvalBinOp::LtEq => 3, + EvalBinOp::Gt => 4, + EvalBinOp::GtEq => 5, + EvalBinOp::StrictEq => 6, + EvalBinOp::StrictNotEq => 7, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => 0, + } +} + +/// Maps bitwise EvalIR operators onto the generated runtime wrapper opcode table. +#[cfg(not(test))] +pub(super) fn bitwise_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::BitAnd => 0, + EvalBinOp::BitOr => 1, + EvalBinOp::BitXor => 2, + EvalBinOp::ShiftLeft => 3, + EvalBinOp::ShiftRight => 4, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::Concat + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor + | EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq + | EvalBinOp::Spaceship => 0, + } +} diff --git a/crates/elephc-magician/src/scope.rs b/crates/elephc-magician/src/scope.rs new file mode 100644 index 0000000000..2f363349e9 --- /dev/null +++ b/crates/elephc-magician/src/scope.rs @@ -0,0 +1,439 @@ +//! Purpose: +//! Owns the materialized activation scope used by runtime eval. +//! The scope maps PHP variable names to opaque elephc runtime cells and tracks +//! unset/dirty/generation metadata for native reloads after an eval barrier. +//! +//! Called from: +//! - `crate::abi` +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - Scope entries store runtime-cell handles only; the eval bridge does not +//! introduce a second PHP value representation. + +use std::collections::HashMap; + +use crate::context::EvalReferenceTarget; +use crate::value::RuntimeCellHandle; + +/// Records whether a scope entry owns or borrows its runtime cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScopeCellOwnership { + Borrowed, + Owned, +} + +/// Tracks the observable PHP state associated with one named scope cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScopeEntryFlags { + pub present: bool, + pub unset: bool, + pub dirty: bool, + pub by_ref: bool, + pub ownership: ScopeCellOwnership, +} + +impl ScopeEntryFlags { + /// Builds flags for a present runtime cell written by native code or eval. + pub const fn present(ownership: ScopeCellOwnership) -> Self { + Self { + present: true, + unset: false, + dirty: true, + by_ref: false, + ownership, + } + } + + /// Builds flags for a present runtime cell shared by PHP references. + pub const fn reference(ownership: ScopeCellOwnership) -> Self { + Self { + present: true, + unset: false, + dirty: true, + by_ref: true, + ownership, + } + } + + /// Builds flags for a PHP variable that has been unset from the dynamic scope. + pub const fn unset() -> Self { + Self { + present: false, + unset: true, + dirty: true, + by_ref: false, + ownership: ScopeCellOwnership::Borrowed, + } + } + + /// Returns true when this entry names a PHP variable visible to reads. + pub const fn is_visible(self) -> bool { + self.present && !self.unset + } +} + +/// Stores one named variable's runtime cell plus sync metadata. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScopeEntry { + cell: RuntimeCellHandle, + flags: ScopeEntryFlags, + generation: u64, +} + +impl ScopeEntry { + /// Creates an entry for a present runtime cell at the given scope generation. + pub const fn present( + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + generation: u64, + ) -> Self { + Self { + cell, + flags: ScopeEntryFlags::present(ownership), + generation, + } + } + + /// Creates a present entry that participates in a PHP reference alias set. + pub const fn reference( + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + generation: u64, + ) -> Self { + Self { + cell, + flags: ScopeEntryFlags::reference(ownership), + generation, + } + } + + /// Creates an unset marker at the given scope generation. + pub const fn unset(generation: u64) -> Self { + Self { + cell: RuntimeCellHandle::from_raw(std::ptr::null_mut()), + flags: ScopeEntryFlags::unset(), + generation, + } + } + + /// Returns the runtime cell handle stored for this variable. + pub const fn cell(self) -> RuntimeCellHandle { + self.cell + } + + /// Returns the PHP visibility and ownership flags for this entry. + pub const fn flags(self) -> ScopeEntryFlags { + self.flags + } + + /// Returns the scope generation when this entry was last updated. + pub const fn generation(self) -> u64 { + self.generation + } + + /// Clears the dirty flag after native code has synchronized the entry. + pub fn mark_clean(&mut self) { + self.flags.dirty = false; + } +} + +/// Materialized activation scope passed opaquely across the eval ABI. +pub struct ElephcEvalScope { + entries: HashMap, + global_aliases: HashMap, + reference_targets: HashMap, + generation: u64, +} + +impl ElephcEvalScope { + /// Creates an empty materialized activation scope. + pub fn new() -> Self { + Self { + entries: HashMap::new(), + global_aliases: HashMap::new(), + reference_targets: HashMap::new(), + generation: 0, + } + } + + /// Returns the current scope generation counter. + pub const fn generation(&self) -> u64 { + self.generation + } + + /// Stores or replaces a named variable with a runtime cell handle. + pub fn set( + &mut self, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + ) -> Option { + self.bump_generation(); + let previous = self.entries.insert( + name.into(), + ScopeEntry::present(cell, ownership, self.generation), + ); + owned_cell_except(previous, cell) + } + + /// Stores a variable while preserving existing PHP reference aliases. + pub fn set_respecting_references( + &mut self, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + ) -> Vec { + let name = name.into(); + let Some(entry) = self.entries.get(&name).copied() else { + return self.set(name, cell, ownership).into_iter().collect(); + }; + if !entry.flags().is_visible() || !entry.flags().by_ref { + return self.set(name, cell, ownership).into_iter().collect(); + } + + self.bump_generation(); + let old_cell = entry.cell(); + let mut replaced = Vec::new(); + for (entry_name, entry) in &mut self.entries { + let flags = entry.flags(); + if !flags.is_visible() || !flags.by_ref || entry.cell() != old_cell { + continue; + } + if flags.ownership == ScopeCellOwnership::Owned + && old_cell != cell + && !replaced.contains(&old_cell) + { + replaced.push(old_cell); + } + let next_ownership = if entry_name == &name { + ownership + } else { + ScopeCellOwnership::Borrowed + }; + *entry = ScopeEntry::reference(cell, next_ownership, self.generation); + } + replaced + } + + /// Binds a target variable name as a PHP reference to a source variable name. + pub fn set_reference( + &mut self, + target: impl Into, + source: impl Into, + default_cell: RuntimeCellHandle, + default_ownership: ScopeCellOwnership, + ) -> Vec { + let target = target.into(); + let source = source.into(); + self.bump_generation(); + let source_entry = self + .entries + .get(&source) + .copied() + .filter(|entry| entry.flags().is_visible()); + let (cell, source_ownership) = source_entry + .map_or((default_cell, default_ownership), |entry| { + (entry.cell(), entry.flags().ownership) + }); + if target == source { + let previous = self.entries.insert( + source, + ScopeEntry::reference(cell, source_ownership, self.generation), + ); + return owned_cell_except(previous, cell).into_iter().collect(); + } + + let previous_source = self.entries.insert( + source, + ScopeEntry::reference(cell, source_ownership, self.generation), + ); + let previous_target = self.entries.insert( + target, + ScopeEntry::reference(cell, ScopeCellOwnership::Borrowed, self.generation), + ); + owned_cells_except([previous_source, previous_target], cell) + } + + /// Records the caller-side storage target for one by-reference local variable. + pub fn set_reference_target(&mut self, name: impl Into, target: EvalReferenceTarget) { + self.reference_targets.insert(name.into(), target); + } + + /// Returns the caller-side storage target associated with one by-reference local. + pub fn reference_target(&self, name: &str) -> Option<&EvalReferenceTarget> { + self.reference_targets.get(name) + } + + /// Marks a named variable as unset while preserving the fact that eval touched it. + pub fn unset(&mut self, name: impl Into) -> Option { + self.bump_generation(); + let name = name.into(); + self.reference_targets.remove(&name); + let previous = self + .entries + .insert(name, ScopeEntry::unset(self.generation)); + owned_cell(previous) + } + + /// Marks a variable as unset while preserving ownership for remaining references. + pub fn unset_respecting_references( + &mut self, + name: impl Into, + ) -> Option { + let name = name.into(); + let Some(entry) = self.entries.get(&name).copied() else { + return self.unset(name); + }; + if !entry.flags().is_visible() || !entry.flags().by_ref { + return self.unset(name); + } + let old_cell = entry.cell(); + let should_transfer_ownership = entry.flags().ownership == ScopeCellOwnership::Owned; + self.bump_generation(); + let mut transferred = false; + if should_transfer_ownership { + for (entry_name, entry) in &mut self.entries { + let flags = entry.flags(); + if entry_name == &name + || !flags.is_visible() + || !flags.by_ref + || entry.cell() != old_cell + { + continue; + } + *entry = + ScopeEntry::reference(old_cell, ScopeCellOwnership::Owned, self.generation); + transferred = true; + break; + } + } + self.reference_targets.remove(&name); + let previous = self + .entries + .insert(name, ScopeEntry::unset(self.generation)); + if transferred { + None + } else { + owned_cell(previous) + } + } + + /// Returns the entry for a named variable, including unset markers. + pub fn entry(&self, name: &str) -> Option { + self.entries.get(name).copied() + } + + /// Returns the visible runtime cell for a named variable, excluding unset markers. + pub fn visible_cell(&self, name: &str) -> Option { + self.entry(name) + .filter(|entry| entry.flags().is_visible()) + .map(ScopeEntry::cell) + } + + /// Returns true when the scope contains a visible value for the named variable. + pub fn contains_visible(&self, name: &str) -> bool { + self.visible_cell(name).is_some() + } + + /// Marks a variable name as an alias to the eval context's global scope. + pub fn mark_global_alias(&mut self, name: impl Into) { + let name = name.into(); + self.mark_global_alias_to(name.clone(), name); + } + + /// Marks a variable name as an alias to a differently named global variable. + pub fn mark_global_alias_to( + &mut self, + name: impl Into, + global_name: impl Into, + ) { + self.global_aliases.insert(name.into(), global_name.into()); + } + + /// Removes a variable's global alias marker after local `unset()`. + pub fn clear_global_alias(&mut self, name: &str) { + self.global_aliases.remove(name); + } + + /// Returns true when the variable should resolve through the global scope. + pub fn is_global_alias(&self, name: &str) -> bool { + self.global_aliases.contains_key(name) + } + + /// Returns the target global name for a local alias. + pub fn global_alias_target(&self, name: &str) -> Option<&str> { + self.global_aliases.get(name).map(String::as_str) + } + + /// Returns the names of entries dirtied since the last synchronization. + pub fn dirty_names(&self) -> Vec<&str> { + self.entries + .iter() + .filter_map(|(name, entry)| entry.flags().dirty.then_some(name.as_str())) + .collect() + } + + /// Clears dirty flags after native code reloads or invalidates affected locals. + pub fn mark_all_clean(&mut self) { + for entry in self.entries.values_mut() { + entry.mark_clean(); + } + } + + /// Removes every entry and returns runtime cells owned by the scope. + pub fn drain_owned_cells(&mut self) -> Vec { + self.reference_targets.clear(); + self.entries + .drain() + .filter_map(|(_, entry)| owned_cell(Some(entry))) + .collect() + } + + /// Advances the generation counter for a scope mutation. + fn bump_generation(&mut self) { + self.generation = self.generation.saturating_add(1); + } +} + +/// Returns the owned cell for a visible owned entry, if one exists. +fn owned_cell(entry: Option) -> Option { + let entry = entry?; + let flags = entry.flags(); + (flags.is_visible() && flags.ownership == ScopeCellOwnership::Owned).then_some(entry.cell()) +} + +/// Returns an owned cell unless it is the same handle as the newly stored cell. +fn owned_cell_except( + entry: Option, + replacement: RuntimeCellHandle, +) -> Option { + owned_cell(entry).filter(|cell| *cell != replacement) +} + +/// Returns owned cells from replaced entries unless they match the replacement. +fn owned_cells_except( + entries: impl IntoIterator>, + replacement: RuntimeCellHandle, +) -> Vec { + let mut cells = Vec::new(); + for entry in entries { + let Some(cell) = owned_cell_except(entry, replacement) else { + continue; + }; + if !cells.contains(&cell) { + cells.push(cell); + } + } + cells +} + +impl Default for ElephcEvalScope { + /// Creates the default empty materialized activation scope. + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/elephc-magician/src/scope/tests.rs b/crates/elephc-magician/src/scope/tests.rs new file mode 100644 index 0000000000..6a742b6051 --- /dev/null +++ b/crates/elephc-magician/src/scope/tests.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Unit tests for scope visibility, ownership, references, aliases, generations, +//! dirty markers, unset behavior, and draining. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Fake cell pointers are compared as opaque handles and never dereferenced. + +use super::*; + +/// Verifies setting a variable records a visible dirty runtime-cell handle. +#[test] +fn set_records_visible_dirty_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let old = scope.set("x", cell, ScopeCellOwnership::Borrowed); + + let entry = scope.entry("x").expect("entry must exist"); + assert_eq!(old, None); + assert_eq!(entry.cell(), cell); + assert!(entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(entry.generation(), 1); + assert_eq!(scope.visible_cell("x"), Some(cell)); +} + +/// Verifies unsetting a variable creates a dirty marker that is not visible. +#[test] +fn unset_records_missing_dirty_marker() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("x", cell, ScopeCellOwnership::Borrowed); + scope.mark_all_clean(); + let old = scope.unset("x"); + + let entry = scope.entry("x").expect("unset marker must exist"); + assert_eq!(old, None); + assert!(entry.flags().unset); + assert!(!entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(scope.visible_cell("x"), None); + assert_eq!(scope.dirty_names(), vec!["x"]); +} + +/// Verifies replacing an owned entry returns the old cell for release. +#[test] +fn set_returns_replaced_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + + scope.set("x", old_cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, Some(old_cell)); + assert_eq!(scope.visible_cell("x"), Some(new_cell)); +} + +/// Verifies replacing an owned entry with the same cell does not release it. +#[test] +fn set_does_not_return_same_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("x", cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, None); +} + +/// Verifies reference binding points two variable names at one runtime cell. +#[test] +fn set_reference_binds_names_to_source_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("source", cell, ScopeCellOwnership::Owned); + let replaced = scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let source = scope.entry("source").expect("source entry should exist"); + let alias = scope.entry("alias").expect("alias entry should exist"); + assert!(replaced.is_empty()); + assert_eq!(source.cell(), cell); + assert_eq!(alias.cell(), cell); + assert!(source.flags().by_ref); + assert!(alias.flags().by_ref); + assert_eq!(source.flags().ownership, ScopeCellOwnership::Owned); + assert_eq!(alias.flags().ownership, ScopeCellOwnership::Borrowed); +} + +/// Verifies writing through one reference alias updates every alias in the group. +#[test] +fn set_respecting_references_updates_alias_group() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("source", old_cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let replaced = + scope.set_respecting_references("alias", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, vec![old_cell]); + assert_eq!(scope.visible_cell("source"), Some(new_cell)); + assert_eq!(scope.visible_cell("alias"), Some(new_cell)); + assert_eq!( + scope.entry("alias").expect("alias").flags().ownership, + ScopeCellOwnership::Owned + ); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Borrowed + ); +} + +/// Verifies unsetting an owned alias transfers ownership to a remaining reference. +#[test] +fn unset_respecting_references_transfers_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("source", cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + scope.set_respecting_references("alias", cell, ScopeCellOwnership::Owned); + + let replaced = scope.unset_respecting_references("alias"); + + assert_eq!(replaced, None); + assert!(scope.entry("alias").expect("alias").flags().unset); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Owned + ); +} + +/// Verifies local aliases can point at differently named globals. +#[test] +fn global_alias_to_records_target_name() { + let mut scope = ElephcEvalScope::new(); + + scope.mark_global_alias_to("alias", "source"); + + assert!(scope.is_global_alias("alias")); + assert_eq!(scope.global_alias_target("alias"), Some("source")); + assert_eq!(scope.global_alias_target("source"), None); +} + +/// Verifies draining a scope returns only visible owned cells. +#[test] +fn drain_owned_cells_returns_visible_owned_entries() { + let mut scope = ElephcEvalScope::new(); + let owned = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let borrowed = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("owned", owned, ScopeCellOwnership::Owned); + scope.set("borrowed", borrowed, ScopeCellOwnership::Borrowed); + scope.unset("borrowed"); + + let drained = scope.drain_owned_cells(); + + assert_eq!(drained, vec![owned]); + assert!(scope.entry("owned").is_none()); + assert!(scope.entry("borrowed").is_none()); +} diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs new file mode 100644 index 0000000000..6525dfaddd --- /dev/null +++ b/crates/elephc-magician/src/stream_resources.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Owns eval-local resource storage backed by host file handles, directory +//! snapshots, stream contexts, and hash contexts. Runtime Mixed cells only carry +//! a numeric resource id. +//! +//! Called from: +//! - `crate::context::ElephcEvalContext` stream-resource accessors. +//! - `crate::interpreter::builtins::filesystem` stream builtin helpers. +//! +//! Key details: +//! - Resource ids are zero-based runtime payloads; PHP display ids are payload + 1. +//! - Resource handles are process-local to eval and are not visible across the C ABI. + +use std::collections::{HashMap, HashSet}; +use std::ffi::c_void; +use std::fs::{File, Metadata, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::net::{Shutdown, TcpListener, TcpStream}; +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; +#[cfg(unix)] +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; + +use crate::stream_wrappers; +use crate::value::RuntimeCellHandle; + +mod file_process_opening; +mod operations; +mod resource_registration; +mod sockets; +mod storage; +mod types; + +use types::*; + +/// Eval-owned table of local file streams keyed by runtime resource payload. +#[derive(Default)] +pub(crate) struct EvalStreamResources { + chunk_sizes: HashMap, + default_stream_context: Option, + disabled_builtin_stream_wrappers: HashSet, + next_id: i64, + directories: HashMap, + filter_resources: HashSet, + hash_contexts: HashMap, + process_children: HashMap, + socket_listeners: HashMap, + socket_names: HashMap, + stream_contexts: HashMap, + streams: HashMap, + user_stream_wrapper_classes: HashMap, + user_stream_wrappers: Vec, + user_wrapper_directories: HashMap, + user_wrapper_streams: HashMap, +} diff --git a/crates/elephc-magician/src/stream_resources/file_process_opening.rs b/crates/elephc-magician/src/stream_resources/file_process_opening.rs new file mode 100644 index 0000000000..7c9d8fa5b1 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/file_process_opening.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Opens local, temporary, process-pipe, memory, and Phar-backed eval streams. +//! +//! Called from: +//! - Filesystem and process stream builtins through `EvalStreamResources`. +//! +//! Key details: +//! - PHP mode parsing and write-back targets are delegated to shared storage types. + +use super::*; + +impl EvalStreamResources { + /// Opens a local path using PHP's common `fopen()` mode strings. + pub(crate) fn open_path(&mut self, path: &str, mode: &str) -> Option { + let mode = EvalOpenMode::parse(mode)?; + if stream_wrappers::is_php_memory_stream(path) { + return self.open_ephemeral_stream(path, &mode, &[], None, false); + } + if stream_wrappers::is_data_stream(path) { + let bytes = stream_wrappers::decode_data_uri(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } + if stream_wrappers::is_phar_stream(path) { + return self.open_phar_stream(path, &mode); + } + if stream_wrappers::is_http_stream(path) && mode.read && !mode.write { + let bytes = stream_wrappers::read_http_url(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } + let path = stream_wrappers::local_filesystem_path(path)?; + let file = mode.open(&path).ok()?; + Some(self.insert(EvalFileStream::new(file, path, mode.label))) + } + + /// Opens an anonymous temporary file and returns its resource id. + pub(crate) fn open_tmpfile(&mut self) -> Option { + let path = eval_tmpfile_path(); + let file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + Some(self.insert(EvalFileStream::new( + file, + path.to_string_lossy().into_owned(), + "w+".to_string(), + ))) + } + + /// Opens a shell process pipe and returns its stream resource id. + pub(crate) fn open_process_pipe(&mut self, command: &str, mode: &str) -> Option { + let read_mode = match mode.chars().next()? { + 'r' => true, + 'w' => false, + _ => return None, + }; + let mut child = Command::new("/bin/sh") + .arg("-c") + .arg(command) + .stdin(if read_mode { + Stdio::null() + } else { + Stdio::piped() + }) + .stdout(if read_mode { + Stdio::piped() + } else { + Stdio::null() + }) + .spawn() + .ok()?; + let file = if read_mode { + let stdout = child.stdout.take()?; + unsafe { + // The ChildStdout pipe is converted into the File that backs + // this eval stream; no second owner keeps the fd alive. + File::from_raw_fd(stdout.into_raw_fd()) + } + } else { + let stdin = child.stdin.take()?; + unsafe { + // The ChildStdin pipe is converted into the File that backs + // this eval stream; dropping it before wait sends EOF. + File::from_raw_fd(stdin.into_raw_fd()) + } + }; + let id = self.insert(EvalFileStream::new( + file, + command.to_string(), + if read_mode { "r" } else { "w" }.to_string(), + )); + self.process_children.insert(id, child); + Some(id) + } + +} diff --git a/crates/elephc-magician/src/stream_resources/operations.rs b/crates/elephc-magician/src/stream_resources/operations.rs new file mode 100644 index 0000000000..e5700c7d41 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/operations.rs @@ -0,0 +1,397 @@ +//! Purpose: +//! Performs close, read/write, positioning, metadata, synchronization, hash, +//! socket, directory, context, filter, and copy operations on resource ids. +//! +//! Called from: +//! - Stream builtins after a resource id has been resolved. +//! +//! Key details: +//! - Each operation checks the concrete resource table and preserves existing +//! false/none behavior for incompatible ids. + +use super::*; + +impl EvalStreamResources { + + /// Removes a stream resource from the table, closing its file handle. + pub(crate) fn close(&mut self, id: i64) -> bool { + let mut closed = false; + let mut ok = true; + if let Some(stream) = self.streams.remove(&id) { + closed = true; + ok = stream.finalize_on_close(); + } + closed = closed + || self.user_wrapper_streams.remove(&id).is_some() + || self.filter_resources.remove(&id) + || self.socket_listeners.remove(&id).is_some(); + self.socket_names.remove(&id); + if let Some(mut child) = self.process_children.remove(&id) { + let _ = child.wait(); + } + closed && ok + } + + /// Returns whether a file-like stream resource exists. + pub(crate) fn has_stream(&self, id: i64) -> bool { + self.streams.contains_key(&id) || self.user_wrapper_streams.contains_key(&id) + } + + /// Returns a local or remote socket name for a socket resource. + pub(crate) fn socket_name(&self, id: i64, remote: bool) -> Option { + let names = self.socket_names.get(&id)?; + if remote { + names.peer.clone() + } else { + Some(names.local.clone()) + } + } + + /// Applies a TCP/Unix stream shutdown operation. + pub(crate) fn socket_shutdown(&self, id: i64, mode: i64) -> Option { + let stream = self.streams.get(&id)?; + let shutdown = match mode { + 0 => Shutdown::Read, + 1 => Shutdown::Write, + 2 => Shutdown::Both, + _ => return Some(false), + }; + let result = unsafe { + // libc shutdown only observes the borrowed descriptor and mode. + libc::shutdown(stream.file.as_raw_fd(), eval_shutdown_how(shutdown)) + }; + Some(result == 0) + } + + /// Allocates an eval-local stream filter resource handle. + pub(crate) fn open_filter_resource(&mut self) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.filter_resources.insert(id); + id + } + + /// Removes an eval-local stream filter resource handle. + pub(crate) fn close_filter_resource(&mut self, id: i64) -> bool { + self.filter_resources.remove(&id) + } + + /// Closes a process pipe stream and returns the child exit status. + pub(crate) fn pclose(&mut self, id: i64) -> Option { + let mut child = self.process_children.remove(&id)?; + self.streams.remove(&id)?; + let status = child.wait().ok()?; + Some(status.code().unwrap_or(0) as i64) + } + + /// Removes a directory resource from the table. + pub(crate) fn close_directory(&mut self, id: i64) -> bool { + self.directories.remove(&id).is_some() + } + + /// Reads up to `length` bytes from a stream resource. + pub(crate) fn read(&mut self, id: i64, length: usize) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut buffer = vec![0_u8; length]; + let read = stream.file.read(&mut buffer).ok()?; + buffer.truncate(read); + stream.eof = read == 0 || read < length; + Some(buffer) + } + + /// Reads the next entry name from a directory resource. + pub(crate) fn read_directory(&mut self, id: i64) -> Option { + self.directories.get_mut(&id)?.read() + } + + /// Feeds bytes into an incremental hash context. + pub(crate) fn update_hash_context(&mut self, id: i64, data: &[u8]) -> bool { + let Some(context) = self.hash_contexts.get_mut(&id) else { + return false; + }; + unsafe { + // The table owns the opaque handle and this mutable borrow gives the + // crypto call exclusive access for the duration of the update. + elephc_crypto::elephc_crypto_update(context.handle, data.as_ptr(), data.len()); + } + true + } + + /// Returns the persisted options for a stream context resource. + pub(crate) fn stream_context_options(&self, id: i64) -> Option { + self.stream_contexts.get(&id).and_then(|context| context.options) + } + + /// Replaces persisted options for a stream context resource. + pub(crate) fn set_stream_context_options( + &mut self, + id: i64, + options: Option, + ) -> bool { + let Some(context) = self.stream_contexts.get_mut(&id) else { + return false; + }; + context.options = options; + true + } + + /// Reads one stream line up to a limit, newline, or custom delimiter. + pub(crate) fn read_line( + &mut self, + id: i64, + length: usize, + ending: Option<&[u8]>, + include_ending: bool, + stop_at_newline: bool, + ) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut output = Vec::new(); + let mut byte = [0_u8; 1]; + while output.len() < length { + let read = stream.file.read(&mut byte).ok()?; + if read == 0 { + stream.eof = true; + break; + } + output.push(byte[0]); + if let Some(ending) = ending { + if !ending.is_empty() && output.ends_with(ending) { + if !include_ending { + output.truncate(output.len().saturating_sub(ending.len())); + } + break; + } + } else if stop_at_newline && byte[0] == b'\n' { + break; + } + } + Some(output) + } + + /// Writes all provided bytes to a stream resource and returns the written byte count. + pub(crate) fn write(&mut self, id: i64, data: &[u8]) -> Option { + let stream = self.streams.get_mut(&id)?; + let written = stream.file.write(data).ok()?; + stream.eof = false; + Some(written) + } + + /// Flushes buffered stream data to the host file handle. + pub(crate) fn flush(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.flush().is_ok()) + } + + /// Returns whether a stream's file descriptor is attached to a terminal. + pub(crate) fn isatty(&self, id: i64) -> Option { + let stream = self.streams.get(&id)?; + let result = unsafe { + // libc only reads the descriptor value during the terminal probe. + libc::isatty(stream.file.as_raw_fd()) + }; + Some(result == 1) + } + + /// Toggles blocking mode on a stream's file descriptor. + pub(crate) fn set_blocking(&self, id: i64, enable: bool) -> Option { + let stream = self.streams.get(&id)?; + let fd = stream.file.as_raw_fd(); + let flags = unsafe { + // fcntl reads the current descriptor flags without taking ownership. + libc::fcntl(fd, libc::F_GETFL) + }; + if flags < 0 { + return Some(false); + } + let flags = if enable { + flags & !libc::O_NONBLOCK + } else { + flags | libc::O_NONBLOCK + }; + let result = unsafe { + // fcntl updates the descriptor flags in place. + libc::fcntl(fd, libc::F_SETFL, flags) + }; + Some(result == 0) + } + + /// Reports timeout-setting support for local file streams. + pub(crate) fn set_timeout(&self, id: i64, _seconds: i64, _microseconds: i64) -> Option { + self.streams.get(&id).map(|_| false) + } + + /// Stores a per-stream chunk size and returns the previous size. + pub(crate) fn set_chunk_size(&mut self, id: i64, size: i64) -> Option { + if !self.streams.contains_key(&id) || size <= 0 { + return None; + } + Some(self.chunk_sizes.insert(id, size).unwrap_or(8192)) + } + + /// Accepts read/write buffer settings for local file streams. + pub(crate) fn set_buffer(&self, id: i64, _size: i64) -> Option { + self.streams.get(&id).map(|_| 0) + } + + /// Applies an advisory lock operation to a stream's backing file descriptor. + pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { + let stream = self.streams.get(&id)?; + let operation = eval_flock_operation(operation)?; + let result = unsafe { + // libc only observes the borrowed raw fd during this call. + libc::flock(stream.file.as_raw_fd(), operation) + }; + if result == 0 { + Some((true, false)) + } else { + Some((false, eval_flock_would_block())) + } + } + + /// Synchronizes stream data and metadata to storage. + pub(crate) fn sync_all(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_all().is_ok()) + } + + /// Synchronizes stream data to storage where the host platform supports it. + pub(crate) fn sync_data(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_data().is_ok()) + } + + /// Returns whether the stream has reached EOF after the last read attempt. + pub(crate) fn eof(&self, id: i64) -> Option { + self.streams + .get(&id) + .map(|stream| stream.eof) + .or_else(|| self.user_wrapper_streams.get(&id).map(|stream| stream.eof)) + } + + /// Returns the current stream cursor offset. + pub(crate) fn tell(&mut self, id: i64) -> Option { + self.streams.get_mut(&id)?.file.stream_position().ok() + } + + /// Moves the stream cursor according to PHP `fseek()` whence values. + pub(crate) fn seek(&mut self, id: i64, offset: i64, whence: i64) -> bool { + let Some(stream) = self.streams.get_mut(&id) else { + return false; + }; + let position = match whence { + 0 => SeekFrom::Start(u64::try_from(offset).unwrap_or(u64::MAX)), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => return false, + }; + stream.eof = false; + stream.file.seek(position).is_ok() + } + + /// Rewinds a stream to the beginning. + pub(crate) fn rewind(&mut self, id: i64) -> bool { + self.seek(id, 0, 0) + } + + /// Rewinds a directory resource to its first entry. + pub(crate) fn rewind_directory(&mut self, id: i64) -> bool { + self.directories + .get_mut(&id) + .is_some_and(EvalDirectoryStream::rewind) + } + + /// Finalizes and removes an incremental hash context, returning raw digest bytes. + pub(crate) fn finalize_hash_context(&mut self, id: i64) -> Option> { + let context = self.hash_contexts.remove(&id)?; + let mut output = [0_u8; 64]; + let len = unsafe { + // elephc-crypto consumes and frees the owned context handle here. + elephc_crypto::elephc_crypto_final(context.handle, output.as_mut_ptr()) + }; + eval_hash_digest_bytes(len, &output) + } + + /// Clones an incremental hash context into a new resource id. + pub(crate) fn copy_hash_context(&mut self, id: i64) -> Option { + let context = self.hash_contexts.get(&id)?; + let handle = unsafe { + // elephc-crypto returns a deep clone with independent ownership. + elephc_crypto::elephc_crypto_clone(context.handle) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + + /// Truncates a stream to the requested byte length. + pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.set_len(size).is_ok()) + } + + /// Returns host metadata for a stream's backing file handle. + pub(crate) fn metadata(&self, id: i64) -> Option { + self.streams + .get(&id) + .and_then(|stream| stream.file.metadata().ok()) + } + + /// Reads a full or bounded byte sequence from the stream, with optional offset seek. + pub(crate) fn get_contents( + &mut self, + id: i64, + length: Option, + offset: Option, + ) -> Option> { + if let Some(offset) = offset { + if !self.seek(id, offset, 0) { + return None; + } + } + match length { + Some(length) => self.read(id, length), + None => { + let stream = self.streams.get_mut(&id)?; + let mut bytes = Vec::new(); + stream.file.read_to_end(&mut bytes).ok()?; + stream.eof = true; + Some(bytes) + } + } + } + + /// Copies bytes between two streams and returns the copied byte count. + pub(crate) fn copy_to_stream( + &mut self, + from: i64, + to: i64, + length: Option, + offset: Option, + ) -> Option { + let bytes = self.get_contents(from, length, offset)?; + self.write(to, &bytes) + } + + /// Returns metadata fields used by PHP `stream_get_meta_data()`. + pub(crate) fn meta_data(&self, id: i64) -> Option { + if let Some(stream) = self.streams.get(&id) { + return Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }); + } + let stream = self.user_wrapper_streams.get(&id)?; + Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }) + } + +} diff --git a/crates/elephc-magician/src/stream_resources/resource_registration.rs b/crates/elephc-magician/src/stream_resources/resource_registration.rs new file mode 100644 index 0000000000..a9e7cadf6d --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/resource_registration.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! Registers directories, incremental hashes, stream contexts, filters, and +//! user-defined stream wrappers in the eval resource table. +//! +//! Called from: +//! - Directory, hash, context, and userspace-wrapper builtins. +//! +//! Key details: +//! - Wrapper protocol names are normalized before registry mutation. + +use super::*; + +impl EvalStreamResources { + + /// Opens a local directory and returns its resource id. + pub(crate) fn open_directory(&mut self, path: &str) -> Option { + let directory = EvalDirectoryStream::open(path)?; + Some(self.insert_directory(directory)) + } + + /// Opens an incremental hash context and returns its resource id. + pub(crate) fn open_hash_context(&mut self, algo: &[u8]) -> Option { + let handle = unsafe { + // elephc-crypto reads the algorithm name during this call and returns + // an owned opaque context handle on success. + elephc_crypto::elephc_crypto_init(algo.as_ptr(), algo.len()) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + + /// Opens a stream context resource with optional persisted options. + pub(crate) fn open_stream_context(&mut self, options: Option) -> i64 { + self.insert_stream_context(EvalStreamContext { options }) + } + + /// Registers a user stream wrapper protocol and class in eval-local state. + pub(crate) fn register_stream_wrapper( + &mut self, + protocol: &str, + class_name: &str, + builtins: &[&str], + ) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if self + .user_stream_wrappers + .iter() + .any(|current| current.eq_ignore_ascii_case(&protocol)) + { + return false; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) + && !self.disabled_builtin_stream_wrappers.contains(&protocol) + { + return false; + } + self.user_stream_wrapper_classes + .insert(protocol.clone(), class_name.to_string()); + self.user_stream_wrappers.push(protocol); + true + } + + /// Unregisters a user or built-in stream wrapper protocol. + pub(crate) fn unregister_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if let Some(index) = self + .user_stream_wrappers + .iter() + .position(|current| current.eq_ignore_ascii_case(&protocol)) + { + let protocol = self.user_stream_wrappers.remove(index); + self.user_stream_wrapper_classes.remove(&protocol); + return true; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + return self.disabled_builtin_stream_wrappers.insert(protocol); + } + false + } + + /// Restores a built-in stream wrapper protocol or accepts no-op user restores. + pub(crate) fn restore_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + self.disabled_builtin_stream_wrappers.remove(&protocol); + } + true + } + + /// Returns the currently visible stream wrapper protocol list. + pub(crate) fn stream_wrappers(&self, builtins: &[&str]) -> Vec { + let mut wrappers = Vec::with_capacity(builtins.len() + self.user_stream_wrappers.len()); + for builtin in builtins { + if !self.disabled_builtin_stream_wrappers.contains(*builtin) { + wrappers.push((*builtin).to_string()); + } + } + wrappers.extend(self.user_stream_wrappers.iter().cloned()); + wrappers + } + + /// Returns the registered userspace wrapper class for a URL scheme. + pub(crate) fn user_stream_wrapper_class_for_path(&self, path: &str) -> Option { + let scheme = stream_wrappers::stream_scheme(path)?; + self.user_stream_wrapper_classes.get(&scheme).cloned() + } + + /// Opens a userspace wrapper stream around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_stream( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + uri: &str, + mode: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_streams.insert( + id, + EvalUserWrapperStream { + object, + class_name: class_name.to_string(), + uri: uri.to_string(), + mode: mode.to_string(), + eof: false, + }, + ); + id + } + + /// Opens a userspace wrapper directory around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_directory( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_directories.insert( + id, + EvalUserWrapperDirectory { + object, + class_name: class_name.to_string(), + }, + ); + id + } + + /// Returns a copied userspace-wrapper stream descriptor for dispatch. + pub(crate) fn user_wrapper_stream_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_streams + .get(&id) + .map(EvalUserWrapperStream::info) + } + + /// Returns a copied userspace-wrapper directory descriptor for dispatch. + pub(crate) fn user_wrapper_directory_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .get(&id) + .map(EvalUserWrapperDirectory::info) + } + + /// Removes a userspace-wrapper directory and returns its descriptor. + pub(crate) fn close_user_wrapper_directory( + &mut self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .remove(&id) + .map(|directory| directory.info()) + } + + /// Updates the cached EOF state for a userspace-wrapper stream. + pub(crate) fn set_user_wrapper_eof(&mut self, id: i64, eof: bool) -> bool { + let Some(stream) = self.user_wrapper_streams.get_mut(&id) else { + return false; + }; + stream.eof = eof; + true + } + + /// Returns the default stream context resource id, creating it if needed. + pub(crate) fn default_stream_context(&mut self) -> i64 { + if let Some(id) = self.default_stream_context { + return id; + } + let id = self.open_stream_context(None); + self.default_stream_context = Some(id); + id + } + +} diff --git a/crates/elephc-magician/src/stream_resources/sockets.rs b/crates/elephc-magician/src/stream_resources/sockets.rs new file mode 100644 index 0000000000..f98928c356 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/sockets.rs @@ -0,0 +1,116 @@ +//! Purpose: +//! Opens TCP listeners/streams, accepted connections, and Unix socket pairs for +//! eval stream builtins. +//! +//! Called from: +//! - Socket-related filesystem and network builtins through `EvalStreamResources`. +//! +//! Key details: +//! - Socket names are captured when handles enter the shared resource table. + +use super::*; + +impl EvalStreamResources { + + /// Opens a TCP listener resource for `stream_socket_server()`. + pub(crate) fn open_tcp_listener(&mut self, address: &str) -> Option { + let listener = TcpListener::bind(eval_tcp_address(address)).ok()?; + let local = listener.local_addr().ok()?.to_string(); + let id = self.next_id; + self.next_id += 1; + self.socket_names.insert( + id, + EvalSocketNames { + local, + peer: None, + }, + ); + self.socket_listeners.insert(id, listener); + Some(id) + } + + /// Opens a connected TCP stream resource. + pub(crate) fn open_tcp_stream(&mut self, address: &str) -> Option { + self.open_tcp_stream_result(address).ok() + } + + /// Opens a connected TCP stream resource and preserves the host I/O error on failure. + pub(crate) fn open_tcp_stream_result(&mut self, address: &str) -> io::Result { + let stream = TcpStream::connect(eval_tcp_address(address))?; + self.insert_tcp_stream(stream).ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "failed to track eval TCP stream") + }) + } + + /// Opens a connected TCP stream from separate host and port arguments. + pub(crate) fn open_tcp_stream_host_port(&mut self, host: &str, port: i64) -> Option { + self.open_tcp_stream_host_port_result(host, port).ok() + } + + /// Opens a connected TCP stream from host and port while preserving I/O errors. + pub(crate) fn open_tcp_stream_host_port_result( + &mut self, + host: &str, + port: i64, + ) -> io::Result { + let host = host + .strip_prefix("tcp://") + .or_else(|| host.strip_prefix("ssl://")) + .or_else(|| host.strip_prefix("tls://")) + .unwrap_or(host); + self.open_tcp_stream_result(&format!("{host}:{port}")) + } + + /// Accepts one TCP connection from a listener resource. + pub(crate) fn accept_tcp(&mut self, id: i64) -> Option { + let listener = self.socket_listeners.get(&id)?; + let (stream, _) = listener.accept().ok()?; + self.insert_tcp_stream(stream) + } + + /// Opens a pair of connected local stream resources. + pub(crate) fn open_socket_pair(&mut self) -> Option<(i64, i64)> { + #[cfg(unix)] + { + let (left, right) = UnixStream::pair().ok()?; + let left = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(left.into_raw_fd()) + }; + let right = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(right.into_raw_fd()) + }; + let left_id = self.insert(EvalFileStream::new( + left, + "socketpair".to_string(), + "r+".to_string(), + )); + let right_id = self.insert(EvalFileStream::new( + right, + "socketpair".to_string(), + "r+".to_string(), + )); + self.socket_names.insert( + left_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + self.socket_names.insert( + right_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + Some((left_id, right_id)) + } + #[cfg(not(unix))] + { + None + } + } + +} diff --git a/crates/elephc-magician/src/stream_resources/storage.rs b/crates/elephc-magician/src/stream_resources/storage.rs new file mode 100644 index 0000000000..9d8794020d --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/storage.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Allocates resource ids and inserts file, ephemeral, Phar, TCP, directory, +//! hash, and context storage entries. +//! +//! Called from: +//! - Resource-opening and registration modules in this tree. +//! +//! Key details: +//! - `next_id` remains the single monotonically increasing id source. + +use super::*; + +impl EvalStreamResources { + /// Inserts a file stream and returns the assigned zero-based resource payload. + pub(super) fn insert(&mut self, stream: EvalFileStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.streams.insert(id, stream); + id + } + + /// Opens one unlinked temporary file as the backing storage for wrapper streams. + pub(super) fn open_ephemeral_stream( + &mut self, + uri: &str, + mode: &EvalOpenMode, + initial: &[u8], + flush_target: Option, + append: bool, + ) -> Option { + let path = eval_tmpfile_path(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + file.write_all(initial).ok()?; + if append { + file.seek(SeekFrom::End(0)).ok()?; + } else { + file.seek(SeekFrom::Start(0)).ok()?; + } + Some(self.insert(EvalFileStream::new_with_flush_target( + file, + uri.to_string(), + mode.label.clone(), + flush_target, + ))) + } + + /// Opens a `phar://` entry for reading or buffered write-back on close. + pub(super) fn open_phar_stream( + &mut self, + path: &str, + mode: &EvalOpenMode, + ) -> Option { + let url = path.as_bytes(); + if mode.write { + let initial = if mode.truncate { + Vec::new() + } else { + match elephc_phar::extract_url_bytes(url) { + Some(bytes) => bytes, + None if mode.create => Vec::new(), + None => return None, + } + }; + return self.open_ephemeral_stream( + path, + mode, + &initial, + Some(EvalStreamFlushTarget::PharUrl(url.to_vec())), + mode.append, + ); + } + let bytes = elephc_phar::extract_url_bytes(url)?; + self.open_ephemeral_stream(path, mode, &bytes, None, false) + } + + /// Inserts a TCP stream as a File-backed eval stream and records endpoint names. + pub(super) fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { + let local = stream.local_addr().ok()?.to_string(); + let peer = stream.peer_addr().ok().map(|addr| addr.to_string()); + let file = unsafe { + // The TcpStream is moved into the File-backed eval stream. + File::from_raw_fd(stream.into_raw_fd()) + }; + let id = self.insert(EvalFileStream::new(file, local.clone(), "r+".to_string())); + self.socket_names + .insert(id, EvalSocketNames { local, peer }); + Some(id) + } + + /// Inserts a directory stream and returns the assigned zero-based resource payload. + pub(super) fn insert_directory(&mut self, directory: EvalDirectoryStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.directories.insert(id, directory); + id + } + + /// Inserts a hash context and returns the assigned zero-based resource payload. + pub(super) fn insert_hash_context(&mut self, context: EvalHashContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.hash_contexts.insert(id, context); + id + } + + /// Inserts a stream context and returns the assigned zero-based resource payload. + pub(super) fn insert_stream_context(&mut self, context: EvalStreamContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.stream_contexts.insert(id, context); + id + } +} diff --git a/crates/elephc-magician/src/stream_resources/types.rs b/crates/elephc-magician/src/stream_resources/types.rs new file mode 100644 index 0000000000..3b2d943117 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/types.rs @@ -0,0 +1,360 @@ +//! Purpose: +//! Defines concrete file, socket, wrapper, directory, hash, context, and fopen +//! mode storage used by `EvalStreamResources`. +//! +//! Called from: +//! - Resource opening, registration, operations, storage, and cleanup modules. +//! +//! Key details: +//! - File streams may carry a close-time write-back target for virtual wrappers. + +use super::*; + +impl Drop for EvalStreamResources { + /// Frees any incremental hash contexts that were never finalized. + fn drop(&mut self) { + for context in self.hash_contexts.drain().map(|(_, context)| context) { + unsafe { + // The resource table owns these handles; draining prevents reuse + // after the crypto free call. + elephc_crypto::elephc_crypto_free(context.handle); + } + } + } +} + +/// PHP-visible metadata for one eval stream resource. +pub(crate) struct EvalStreamMetaData { + pub(crate) eof: bool, + pub(crate) mode: String, + pub(crate) uri: String, +} + +/// Local and peer names tracked for socket-backed eval streams. +pub(super) struct EvalSocketNames { + pub(super) local: String, + pub(super) peer: Option, +} + +/// Normalizes supported TCP-style stream socket addresses. +pub(super) fn eval_tcp_address(address: &str) -> &str { + address + .strip_prefix("tcp://") + .or_else(|| address.strip_prefix("ssl://")) + .or_else(|| address.strip_prefix("tls://")) + .unwrap_or(address) +} + +/// Converts Rust's socket shutdown enum into libc constants. +pub(super) fn eval_shutdown_how(shutdown: Shutdown) -> libc::c_int { + match shutdown { + Shutdown::Read => libc::SHUT_RD, + Shutdown::Write => libc::SHUT_WR, + Shutdown::Both => libc::SHUT_RDWR, + } +} + +/// Converts PHP `LOCK_*` bit flags into host `flock()` flags. +pub(super) fn eval_flock_operation(operation: i64) -> Option { + let non_blocking = operation & 4 != 0; + let base = match operation & !4 { + 1 => libc::LOCK_SH, + 2 => libc::LOCK_EX, + 3 => libc::LOCK_UN, + _ => return None, + }; + Some(base | if non_blocking { libc::LOCK_NB } else { 0 }) +} + +/// Returns whether the last host `flock()` failure was a non-blocking lock miss. +pub(super) fn eval_flock_would_block() -> bool { + let errno = std::io::Error::last_os_error().raw_os_error(); + errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) +} + +/// Converts an elephc-crypto digest length into owned raw bytes. +pub(super) fn eval_hash_digest_bytes(len: isize, output: &[u8; 64]) -> Option> { + let len = usize::try_from(len).ok()?; + if len > output.len() { + return None; + } + Some(output[..len].to_vec()) +} + +/// Normalizes a PHP stream wrapper protocol name for eval registry storage. +pub(super) fn eval_normalize_stream_wrapper_protocol(protocol: &str) -> Option { + let protocol = protocol.trim().trim_end_matches("://"); + if protocol.is_empty() { + return None; + } + Some(protocol.to_ascii_lowercase()) +} + +/// Returns whether the protocol is one of elephc's built-in stream wrappers. +pub(super) fn eval_builtin_stream_wrapper_exists(builtins: &[&str], protocol: &str) -> bool { + builtins + .iter() + .any(|builtin| builtin.eq_ignore_ascii_case(protocol)) +} + +/// File stream stored behind one eval resource id. +pub(super) struct EvalFileStream { + pub(super) file: File, + pub(super) uri: String, + pub(super) mode: String, + pub(super) eof: bool, + pub(super) flush_target: Option, +} + +impl EvalFileStream { + /// Creates a tracked stream around a host file handle. + pub(super) fn new(file: File, uri: String, mode: String) -> Self { + Self::new_with_flush_target(file, uri, mode, None) + } + + /// Creates a tracked stream that may write back to a wrapper target on close. + pub(super) fn new_with_flush_target( + file: File, + uri: String, + mode: String, + flush_target: Option, + ) -> Self { + Self { + file, + uri, + mode, + eof: false, + flush_target, + } + } + + /// Flushes any buffered wrapper target before the stream resource disappears. + pub(super) fn finalize_on_close(mut self) -> bool { + let Some(flush_target) = self.flush_target.take() else { + return true; + }; + let mut bytes = Vec::new(); + if self.file.flush().is_err() || self.file.seek(SeekFrom::Start(0)).is_err() { + return false; + } + if self.file.read_to_end(&mut bytes).is_err() { + return false; + } + flush_target.write_back(&bytes) + } +} + +/// Userspace wrapper stream stored behind one eval resource id. +pub(super) struct EvalUserWrapperStream { + pub(super) object: RuntimeCellHandle, + pub(super) class_name: String, + pub(super) uri: String, + pub(super) mode: String, + pub(super) eof: bool, +} + +impl EvalUserWrapperStream { + /// Copies the dispatch-relevant wrapper fields out of the resource table. + pub(super) fn info(&self) -> EvalUserWrapperStreamInfo { + EvalUserWrapperStreamInfo { + object: self.object, + class_name: self.class_name.clone(), + eof: self.eof, + } + } +} + +/// Copied userspace-wrapper stream fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperStreamInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, + pub(crate) eof: bool, +} + +/// Userspace-wrapper directory stored behind one eval resource id. +pub(super) struct EvalUserWrapperDirectory { + pub(super) object: RuntimeCellHandle, + pub(super) class_name: String, +} + +impl EvalUserWrapperDirectory { + /// Copies the dispatch fields needed while invoking wrapper directory methods. + pub(super) fn info(&self) -> EvalUserWrapperDirectoryInfo { + EvalUserWrapperDirectoryInfo { + object: self.object, + class_name: self.class_name.clone(), + } + } +} + +/// Copied userspace-wrapper directory fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperDirectoryInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, +} + +/// Wrapper targets that need a write-back step when their stream closes. +pub(super) enum EvalStreamFlushTarget { + PharUrl(Vec), +} + +impl EvalStreamFlushTarget { + /// Writes buffered stream bytes back to the target URL. + pub(super) fn write_back(&self, bytes: &[u8]) -> bool { + match self { + Self::PharUrl(url) => elephc_phar::put_url_bytes(url, bytes).is_some(), + } + } +} + +/// Directory stream stored behind one eval resource id. +pub(super) struct EvalDirectoryStream { + pub(super) entries: Vec, + pub(super) index: usize, +} + +impl EvalDirectoryStream { + /// Opens a local directory and snapshots its entry names. + pub(super) fn open(path: &str) -> Option { + let entries = std::fs::read_dir(path).ok()?; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.ok()?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Some(Self { + entries: names, + index: 0, + }) + } + + /// Returns the next directory entry name. + pub(super) fn read(&mut self) -> Option { + let name = self.entries.get(self.index)?.clone(); + self.index += 1; + Some(name) + } + + /// Moves the directory cursor back to its first entry. + pub(super) fn rewind(&mut self) -> bool { + self.index = 0; + true + } +} + +/// Opaque elephc-crypto incremental hash context resource. +pub(super) struct EvalHashContext { + pub(super) handle: *mut c_void, +} + +/// Stream context metadata tracked by eval. +pub(super) struct EvalStreamContext { + pub(super) options: Option, +} + +/// Parsed PHP fopen mode used to configure `OpenOptions`. +pub(super) struct EvalOpenMode { + pub(super) read: bool, + pub(super) write: bool, + pub(super) append: bool, + pub(super) truncate: bool, + pub(super) create: bool, + pub(super) create_new: bool, + pub(super) label: String, +} + +impl EvalOpenMode { + /// Parses PHP's common fopen mode grammar, ignoring binary/text markers. + pub(super) fn parse(mode: &str) -> Option { + let mut chars = mode.chars(); + let first = chars.next()?; + let plus = mode.contains('+'); + if !mode + .chars() + .all(|ch| matches!(ch, 'r' | 'w' | 'a' | 'x' | 'c' | '+' | 'b' | 't' | 'e')) + { + return None; + } + let mut mode = match first { + 'r' => Self { + read: true, + write: plus, + append: false, + truncate: false, + create: false, + create_new: false, + label: if plus { "r+" } else { "r" }.to_string(), + }, + 'w' => Self { + read: plus, + write: true, + append: false, + truncate: true, + create: true, + create_new: false, + label: if plus { "w+" } else { "w" }.to_string(), + }, + 'a' => Self { + read: plus, + write: true, + append: true, + truncate: false, + create: true, + create_new: false, + label: if plus { "a+" } else { "a" }.to_string(), + }, + 'x' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: false, + create_new: true, + label: if plus { "x+" } else { "x" }.to_string(), + }, + 'c' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: true, + create_new: false, + label: if plus { "c+" } else { "c" }.to_string(), + }, + _ => return None, + }; + mode.write = mode.write || plus; + Some(mode) + } + + /// Opens a path with the parsed stream mode. + pub(super) fn open(&self, path: &str) -> std::io::Result { + OpenOptions::new() + .read(self.read) + .write(self.write) + .append(self.append) + .truncate(self.truncate) + .create(self.create) + .create_new(self.create_new) + .open(path) + } +} + +/// Builds a unique temporary path for eval `tmpfile()`. +pub(super) fn eval_tmpfile_path() -> PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!( + "elephc-magician-tmpfile-{}-{}", + std::process::id(), + eval_tmpfile_nonce() + )); + path +} + +/// Returns a monotonic-ish nonce for temporary file names. +pub(super) fn eval_tmpfile_nonce() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0) +} diff --git a/crates/elephc-magician/src/stream_wrappers.rs b/crates/elephc-magician/src/stream_wrappers.rs new file mode 100644 index 0000000000..b86073a715 --- /dev/null +++ b/crates/elephc-magician/src/stream_wrappers.rs @@ -0,0 +1,280 @@ +//! Purpose: +//! Parses eval-supported PHP stream wrapper URLs that can be handled entirely +//! inside the magician process. +//! +//! Called from: +//! - `crate::stream_resources::EvalStreamResources` for `fopen()` wrapper routing. +//! - Filesystem builtins that need direct `file_get_contents()` / `file_put_contents()` handling. +//! +//! Key details: +//! - Plain `http://` uses a small blocking HTTP/1.0 client. TLS-backed +//! `https://` remains outside magician's implemented wrapper paths for now. + +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::time::Duration; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(5); + +/// Returns true when the path names a `php://memory` or `php://temp` stream. +pub(crate) fn is_php_memory_stream(path: &str) -> bool { + path == "php://memory" || path == "php://temp" || path.starts_with("php://temp/") +} + +/// Returns true when the path names a `data://` stream. +pub(crate) fn is_data_stream(path: &str) -> bool { + path.starts_with("data://") +} + +/// Returns true when the path names a `phar://` stream. +pub(crate) fn is_phar_stream(path: &str) -> bool { + path.starts_with("phar://") +} + +/// Returns true when the path names a plain `http://` stream. +pub(crate) fn is_http_stream(path: &str) -> bool { + path.starts_with("http://") +} + +/// Extracts and normalizes the PHP stream-wrapper scheme from a URL-like path. +pub(crate) fn stream_scheme(path: &str) -> Option { + let separator = path.find("://")?; + if separator == 0 || !path_has_scheme(path) { + return None; + } + Some(path[..separator].to_ascii_lowercase()) +} + +/// Maps plain local paths and `file://` URLs onto host filesystem paths. +pub(crate) fn local_filesystem_path(path: &str) -> Option { + if let Some(rest) = path.strip_prefix("file://") { + return Some(file_url_local_path(rest)); + } + if path_has_scheme(path) { + None + } else { + Some(path.to_string()) + } +} + +/// Decodes a `data://[][;base64],` URI into raw bytes. +pub(crate) fn decode_data_uri(path: &str) -> Option> { + let rest = path.strip_prefix("data://")?; + let comma = rest.find(',')?; + let meta = &rest[..comma]; + let payload = &rest[comma + 1..]; + if meta.to_ascii_lowercase().ends_with(";base64") { + base64_decode(payload) + } else { + Some(percent_decode(payload)) + } +} + +/// Reads a plain HTTP URL into response-body bytes. +pub(crate) fn read_http_url(path: &str) -> Option> { + let request = parse_http_url(path)?; + let mut stream = TcpStream::connect((request.host.as_str(), request.port)).ok()?; + let _ = stream.set_read_timeout(Some(HTTP_TIMEOUT)); + let _ = stream.set_write_timeout(Some(HTTP_TIMEOUT)); + let wire_request = format!( + "GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n", + request.path_and_query, + request.host_header() + ); + stream.write_all(wire_request.as_bytes()).ok()?; + let mut response = Vec::new(); + stream.read_to_end(&mut response).ok()?; + parse_http_response_body(&response) +} + +/// Parsed `http://` URL pieces needed for a minimal HTTP request. +struct EvalHttpRequest { + host: String, + port: u16, + path_and_query: String, +} + +impl EvalHttpRequest { + /// Returns the Host header value, preserving non-default ports. + fn host_header(&self) -> String { + if self.port == 80 { + self.host.clone() + } else { + format!("{}:{}", self.host, self.port) + } + } +} + +/// Parses a plain `http://host[:port][/path][?query]` URL. +fn parse_http_url(path: &str) -> Option { + let rest = path.strip_prefix("http://")?; + let (authority, suffix) = match rest.find(['/', '?', '#']) { + Some(index) => (&rest[..index], &rest[index..]), + None => (rest, "/"), + }; + if authority.is_empty() || authority.contains('@') { + return None; + } + let (host, port) = parse_http_authority(authority)?; + let mut path_and_query = match suffix.chars().next() { + Some('/') => suffix.to_string(), + Some('?') => format!("/{suffix}"), + Some('#') | None => "/".to_string(), + _ => return None, + }; + if let Some(fragment) = path_and_query.find('#') { + path_and_query.truncate(fragment); + } + if path_and_query.is_empty() { + path_and_query.push('/'); + } + Some(EvalHttpRequest { + host, + port, + path_and_query, + }) +} + +/// Parses the authority portion of an HTTP URL. +fn parse_http_authority(authority: &str) -> Option<(String, u16)> { + if let Some(rest) = authority.strip_prefix('[') { + let end = rest.find(']')?; + let host = rest[..end].to_string(); + let after = &rest[end + 1..]; + let port = if let Some(port) = after.strip_prefix(':') { + port.parse::().ok()? + } else if after.is_empty() { + 80 + } else { + return None; + }; + return Some((host, port)); + } + let (host, port) = match authority.rsplit_once(':') { + Some((host, port)) if !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()) => { + (host, port.parse::().ok()?) + } + _ => (authority, 80), + }; + if host.is_empty() { + None + } else { + Some((host.to_string(), port)) + } +} + +/// Extracts the body from an HTTP response with a successful status code. +fn parse_http_response_body(response: &[u8]) -> Option> { + let header_end = response + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|index| index + 4) + .or_else(|| { + response + .windows(2) + .position(|window| window == b"\n\n") + .map(|index| index + 2) + })?; + let status_line_end = response + .windows(2) + .position(|window| window == b"\r\n") + .or_else(|| response.iter().position(|byte| *byte == b'\n'))?; + let status_line = std::str::from_utf8(&response[..status_line_end]).ok()?; + let mut pieces = status_line.split_whitespace(); + let _version = pieces.next()?; + let status = pieces.next()?.parse::().ok()?; + if !(200..300).contains(&status) { + return None; + } + Some(response[header_end..].to_vec()) +} + +/// Converts the part after `file://` into a path understood by the host OS. +fn file_url_local_path(rest: &str) -> String { + if let Some(localhost_path) = rest.strip_prefix("localhost/") { + format!("/{localhost_path}") + } else if rest == "localhost" { + "/".to_string() + } else { + rest.to_string() + } +} + +/// Reports whether a string starts with a PHP-style `scheme://` prefix. +fn path_has_scheme(path: &str) -> bool { + let Some(separator) = path.find("://") else { + return false; + }; + separator > 0 + && path[..separator] + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.')) +} + +/// Decodes a base64 payload into raw bytes, rejecting invalid alphabet bytes. +fn base64_decode(input: &str) -> Option> { + /// Converts one base64 byte into its six-bit value. + fn sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(u32::from(byte - b'A')), + b'a'..=b'z' => Some(u32::from(byte - b'a') + 26), + b'0'..=b'9' => Some(u32::from(byte - b'0') + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } + } + + let mut output = Vec::new(); + let mut accumulator = 0_u32; + let mut bits = 0_u32; + for byte in input.bytes() { + if byte == b'=' { + break; + } + if byte.is_ascii_whitespace() { + continue; + } + accumulator = (accumulator << 6) | sextet(byte)?; + bits += 6; + if bits >= 8 { + bits -= 8; + output.push((accumulator >> bits) as u8); + } + } + Some(output) +} + +/// Percent-decodes the non-base64 data URI payload form. +fn percent_decode(input: &str) -> Vec { + let bytes = input.as_bytes(); + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'%' if index + 2 < bytes.len() => { + let high = (bytes[index + 1] as char).to_digit(16); + let low = (bytes[index + 2] as char).to_digit(16); + match (high, low) { + (Some(high), Some(low)) => { + output.push((high * 16 + low) as u8); + index += 3; + } + _ => { + output.push(b'%'); + index += 1; + } + } + } + b'+' => { + output.push(b' '); + index += 1; + } + byte => { + output.push(byte); + index += 1; + } + } + } + output +} diff --git a/crates/elephc-magician/src/value.rs b/crates/elephc-magician/src/value.rs new file mode 100644 index 0000000000..8d3e736c2a --- /dev/null +++ b/crates/elephc-magician/src/value.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Names the opaque runtime cell/value handles used by eval internals. +//! Prevents the eval bridge from introducing a second PHP value system. +//! +//! Called from: +//! - Future `crate::scope` and `crate::interpreter` implementations. +//! +//! Key details: +//! - Handles point at elephc runtime cells whose tag/payload/refcount contract +//! is owned by the main runtime. + +use std::ffi::c_void; + +/// Opaque pointer to an elephc runtime cell. +pub type RuntimeCell = c_void; + +/// Wraps an opaque runtime cell pointer without taking ownership by itself. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RuntimeCellHandle { + ptr: *mut RuntimeCell, +} + +impl RuntimeCellHandle { + /// Creates a runtime-cell handle from a raw pointer supplied by elephc. + pub const fn from_raw(ptr: *mut RuntimeCell) -> Self { + Self { ptr } + } + + /// Returns the raw runtime-cell pointer for ABI calls back into elephc. + pub const fn as_ptr(self) -> *mut RuntimeCell { + self.ptr + } + + /// Returns true when this handle does not reference a runtime cell. + pub const fn is_null(self) -> bool { + self.ptr.is_null() + } +} diff --git a/docs/README.md b/docs/README.md index b96808d2f2..59a1b46cb5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ sidebar: order: 0 --- -elephc compiles PHP to native binaries for the supported targets — currently macOS ARM64, Linux ARM64, and Linux x86_64. No interpreter, no VM, no runtime dependencies. This documentation covers everything from PHP syntax support to compiler-specific extensions and internal architecture. +elephc compiles PHP to standalone native binaries for the supported targets — currently macOS ARM64, Linux ARM64, and Linux x86_64 — without PHP, the Zend Engine, or an external VM. Ordinary source is AOT-compiled; experimental `eval()` may embed an optional interpreter bridge when a fragment requires runtime parsing. This documentation covers everything from PHP syntax support to compiler-specific extensions and internal architecture. ## Getting Started @@ -40,6 +40,7 @@ Standard PHP features supported by elephc. Implemented PHP syntax is intended to - [Operators](php/operators.md) — arithmetic, comparison, `instanceof`, logical, bitwise, string, assignment, ternary, null coalescing, error control - [Control Structures](php/control-structures.md) — if/else, while, for, foreach, switch, match, multi-level break/continue, try/catch/finally - [Functions](php/functions.md) — declarations, closures, arrow functions, named arguments, variadic, spread, pass-by-reference, first-class callables, static variables +- [Eval](php/eval.md) — experimental literal-AOT and runtime PHP fragment evaluation, scope synchronization, dynamic declarations, safety, supported builtins, and limitations - [Strings](php/strings.md) — escape sequences, interpolation, heredoc/nowdoc, 70+ built-in string functions - [Regex](php/regex.md) — PCRE2-backed `preg_*` functions, SPL regex iterators, and native PCRE2 build requirements - [Arrays](php/arrays.md) — indexed, associative, copy-on-write, 50+ built-in array functions @@ -83,6 +84,7 @@ How elephc works under the hood — from lexing to code generation and runtime s - [The Code Generator](internals/the-codegen.md) — checked AST to EIR, then target assembly - [The EIR Design](internals/the-ir.md) — PHP-shaped intermediate representation used by codegen and `--emit-ir` - [The Runtime](internals/the-runtime.md) — hand-written assembly routines +- [Eval Runtime Architecture](internals/eval-runtime.md) — literal AOT planning, scope synchronization, Magician fallback, and bridge ABI - [Memory Model](internals/memory-model.md) — stack frames, heap, reference counting - [Architecture](internals/architecture.md) — module map, calling conventions - [ARM64 Assembly](internals/arm64-assembly.md) — introduction to ARM64 diff --git a/docs/compiling/cli-reference.md b/docs/compiling/cli-reference.md index 83da7854ba..5a0a454d42 100644 --- a/docs/compiling/cli-reference.md +++ b/docs/compiling/cli-reference.md @@ -92,7 +92,7 @@ See [Optimization and codegen controls](optimization.md). | `--link LIB` / `-l LIB` / `-lLIB` | library name | — | Link an extra native library (repeatable). | | `--link-path DIR` / `-L DIR` / `-LDIR` | directory | — | Add a library search path (repeatable). | | `--framework NAME` | framework name | — | Link a macOS framework (repeatable). | -| `--with-CRATE` | `pdo`, `tls`, `crypto`, `phar`, `tz`, `image`, `web` | — | Force-enable a bridge crate regardless of feature auto-detection (repeatable). Force-links the staticlib (whole-archived, so it is not dead-stripped) and, for crates with a PHP-surface prelude (`pdo`, `tz`, `image`), force-injects that prelude so the API is available. `--with-web` is an alias for `--web`. An unknown crate name is an error. | +| `--with-CRATE` | `pdo`, `tls`, `crypto`, `phar`, `tz`, `image`, `eval`, `web` | — | Force-enable a bridge crate regardless of feature auto-detection (repeatable). Force-links the staticlib (whole-archived, so it is not dead-stripped) and, for crates with a PHP-surface prelude (`pdo`, `tz`, `image`), force-injects that prelude so the API is available. `--with-eval` force-links Magician but is not required for normal `eval()` use; eligible literal eval can remain bridge-free. `--with-web` is an alias for `--web`. An unknown crate name is an error. | See [Linking, heap, and conditional compilation](linking-and-conditional-compilation.md). diff --git a/docs/compiling/linking-and-conditional-compilation.md b/docs/compiling/linking-and-conditional-compilation.md index 8656dc82c0..90ba15a0db 100644 --- a/docs/compiling/linking-and-conditional-compilation.md +++ b/docs/compiling/linking-and-conditional-compilation.md @@ -51,12 +51,15 @@ Some optional features are implemented as Rust *bridge crates* (`staticlib` archives) that elephc links into the program: `pdo` (database access), `tls` (`https://`/`ftps://` streams), `crypto` (the `hash()`/`md5()`/`sha1()` family), `phar` (Phar archives), `tz` (timezone introspection), `image` (GD/Imagick image -processing), and `web` (the `--web` server). +processing), `eval` (the Magician interpreter fallback for dynamic `eval()`), +and `web` (the `--web` server). By default a bridge is linked **only when the program uses it** — using a hash function pulls in `crypto`, opening an `https://` stream pulls in `tls`, -referencing `PDO` pulls in `pdo`, and so on. Programs that do not use a feature -never link its crate, so binaries stay small. +referencing `PDO` pulls in `pdo`, and so on. An `eval()` call pulls in Magician +only when it needs runtime parsing: eligible literal fragments can be parsed at +compile time and lowered to native EIR without the interpreter bridge. Programs +that do not need a feature never link its crate, so binaries stay small. `--with-CRATE` force-enables a bridge regardless of that auto-detection. It force-links the staticlib (whole-archived, so it is retained even if no symbol @@ -68,8 +71,15 @@ that detection cannot see. The flag is repeatable: ```bash elephc app.php --with-pdo elephc app.php --with-crypto --with-tls +elephc app.php --with-eval ``` +`--with-eval` force-links `elephc_magician`; it does not enable new syntax or +change which fragments are eligible for AOT lowering. Normal eval usage is +detected automatically. See [Eval](../php/eval.md) for language semantics and +[Eval Runtime Architecture](../internals/eval-runtime.md) for the AOT/fallback +decision and scope ABI. + `--with-web` is an alias for [`--web`](../beyond-php/web.md) (the full server mode, which owns the program entry point). An unknown crate name is rejected with the list of valid crates. Forcing a crate increases binary size, since the whole diff --git a/docs/compiling/overview.md b/docs/compiling/overview.md index fe6433014c..22bbfda304 100644 --- a/docs/compiling/overview.md +++ b/docs/compiling/overview.md @@ -6,9 +6,11 @@ sidebar: --- elephc is an ahead-of-time compiler: it reads a `.php` file and produces a -standalone native binary with no interpreter, no virtual machine, and no runtime -dependencies. The generated program is plain machine code linked against a small -hand-written runtime that is baked into the executable. +standalone native binary with no PHP, Zend Engine, or external virtual-machine +dependency. Ordinary source is machine code linked against a small hand-written +runtime baked into the executable. Experimental dynamic `eval()` is the explicit +exception: fragments that cannot be AOT-lowered embed the optional Magician +interpreter bridge in that same standalone binary. ## Basic invocation diff --git a/docs/getting-started/your-first-program.md b/docs/getting-started/your-first-program.md index 8ca8c70e07..258d786093 100644 --- a/docs/getting-started/your-first-program.md +++ b/docs/getting-started/your-first-program.md @@ -30,7 +30,7 @@ This produces a native binary called `hello` in the same directory. Run it: Hello, World! ``` -That's it — a standalone native binary, no PHP interpreter needed. +That's it — a standalone native binary, with no external PHP interpreter needed. ## A slightly bigger example diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index b60a5c2018..036e0f010a 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -2,7 +2,7 @@ title: "Architecture" description: "Module map, calling conventions, and pipeline diagram." sidebar: - order: 10 + order: 11 --- ## Compilation pipeline @@ -155,6 +155,13 @@ PHP source (.php) └─────────┘ ``` +Experimental `eval()` follows the same front-end pipeline. A literal fragment +is classified inside EIR lowering by `src/eval_aot.rs`: eligible fragments +become native EIR, scope-backed fragments use only core runtime helpers, and +dynamic fragments enable the optional `elephc-magician` interpreter staticlib +at final linking. This is a lowering/linking decision, not a separate timed +compiler phase. See [Eval Runtime Architecture](eval-runtime.md). + ## Target Model The compiler now distinguishes the operating-system side of a target from the instruction set: @@ -184,6 +191,7 @@ src/ ├── conditional/ Build-time `ifdef` pass ├── autoload/ Composer/SPL AOT autoload indexing, rule interpretation, and file insertion ├── resolver/ Include/require resolution, declaration discovery, once guards +├── eval_aot.rs Target-independent literal eval planning and fallback classification ├── optimize.rs Public optimizer entry points and effect context ├── optimize/ Constant folding, constant propagation, control-flow pruning, normalization, dead-code elimination ├── ir/ EIR types, builder, validator, printer, effects, and tests @@ -312,6 +320,8 @@ src/ │ ├── mod.rs Runtime module boundary; re-exports the emission entry points │ ├── data/ Fixed, user-program, and instanceof runtime data tables (4 files) │ ├── diagnostics.rs Suppressible runtime-warning channel used by `@` +│ ├── eval_bridge.rs C-ABI value, callable, class, and runtime hooks used by Magician +│ ├── eval_scope.rs Core materialized-scope helpers usable without the interpreter │ ├── emitters.rs `emit_runtime()` orchestration — emits every runtime category in a fixed order │ ├── strings/ itoa, concat, resource display, ftoa, sprintf, md5, sha1, str_persist, ... (71 files) │ ├── arrays/ heap_alloc, heap_free, array_free_deep, array_grow, hash_grow, hash_*, mixed boxing/freeing, mixed instanceof, sort, usort, refcount, gc/decref dispatch, ... (148 files) @@ -332,6 +342,9 @@ src/ └── errors/ ├── mod.rs CompileError, error trait └── report.rs Error formatting + +crates/ +└── elephc-magician/ Optional EvalIR parser/interpreter staticlib for dynamic eval ``` ## ARM64 calling conventions diff --git a/docs/internals/arm64-assembly.md b/docs/internals/arm64-assembly.md index 53eae56385..03f24c1bf2 100644 --- a/docs/internals/arm64-assembly.md +++ b/docs/internals/arm64-assembly.md @@ -2,7 +2,7 @@ title: "ARM64 Assembly" description: "Introduction to ARM64 assembly for compiler output." sidebar: - order: 11 + order: 12 --- ## What is assembly language? diff --git a/docs/internals/arm64-instructions.md b/docs/internals/arm64-instructions.md index 29ac5a0fd6..394790e3cb 100644 --- a/docs/internals/arm64-instructions.md +++ b/docs/internals/arm64-instructions.md @@ -2,7 +2,7 @@ title: "ARM64 Instructions" description: "ARM64 instruction reference used by elephc." sidebar: - order: 12 + order: 13 --- This is a reference for the ARM64 instructions elephc uses most often, organized by category. Each entry shows the instruction, what it does, and where elephc uses it. diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index 4b38d39715..fc63d10226 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_gmmktime_raw() — internals" description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 433 + order: 437 --- ## `__elephc_gmmktime_raw()` — internals @@ -36,6 +36,10 @@ function __elephc_gmmktime_raw(int $hour, int $minute, int $second, int $month, - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_mktime_raw.md b/docs/internals/builtins/_internal/__elephc_mktime_raw.md index 34c78e3e42..d4594ceb36 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_mktime_raw() — internals" description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 434 + order: 438 --- ## `__elephc_mktime_raw()` — internals @@ -36,6 +36,10 @@ function __elephc_mktime_raw(int $hour, int $minute, int $second, int $month, in - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md index 43cb83bd8d..0df6b9c8c4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_bzip2_archive() — internals" description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 435 + order: 439 --- ## `__elephc_phar_bzip2_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_bzip2_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md index f360468a33..cbbf97f852 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_decompress_archive() — internals" description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 436 + order: 440 --- ## `__elephc_phar_decompress_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_decompress_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md index 8530c2e649..f1cf109141 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_file_metadata() — internals" description: "Compiler internals for __elephc_phar_get_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 437 + order: 441 --- ## `__elephc_phar_get_file_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_file_metadata(string $url): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md index 3a582db45d..69095125a6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_metadata() — internals" description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 438 + order: 442 --- ## `__elephc_phar_get_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_metadata(string $filename): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md index 9d36ac4f1d..ab785b1d3d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_hash() — internals" description: "Compiler internals for __elephc_phar_get_signature_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 439 + order: 443 --- ## `__elephc_phar_get_signature_hash()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_signature_hash(string $path): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md index cd75ce2a80..22a982fca5 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_type() — internals" description: "Compiler internals for __elephc_phar_get_signature_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 440 + order: 444 --- ## `__elephc_phar_get_signature_type()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_signature_type(string $path): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md index b5058c2c39..1a811a61f6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_stub() — internals" description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 441 + order: 445 --- ## `__elephc_phar_get_stub()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_stub(string $filename): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md index 43d82a812d..47f661eb6e 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_gzip_archive() — internals" description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 442 + order: 446 --- ## `__elephc_phar_gzip_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_gzip_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md index 8df039804d..9c8543dbd4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,7 +2,7 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 443 + order: 447 --- ## `__elephc_phar_list_entries()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_list_entries(string $filename): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index 2071a79440..246ed5d392 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 444 + order: 448 --- ## `__elephc_phar_set_compression()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_set_compression(string $filename, int $compression): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md index d6c62eaa72..d0a4d1354f 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_file_metadata() — internals" description: "Compiler internals for __elephc_phar_set_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 445 + order: 449 --- ## `__elephc_phar_set_file_metadata()` — internals @@ -34,6 +34,10 @@ function __elephc_phar_set_file_metadata(string $url, string $metadata): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md index c4d71d4840..b9af908d49 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_metadata() — internals" description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 446 + order: 450 --- ## `__elephc_phar_set_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_set_metadata(string $filename, string $metadata): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md index b6fbd1d0ed..8e64bae7a0 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_stub() — internals" description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 447 + order: 451 --- ## `__elephc_phar_set_stub()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_set_stub(string $filename, string $stub): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md index 8ae1acc485..f2dc51ce58 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_zip_password() — internals" description: "Compiler internals for __elephc_phar_set_zip_password(): lowering path, type checks, and runtime helpers." sidebar: - order: 448 + order: 452 --- ## `__elephc_phar_set_zip_password()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_set_zip_password(string $password): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md index 08dc961d7f..817eb9f247 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_hash() — internals" description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 449 + order: 453 --- ## `__elephc_phar_sign_hash()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_sign_hash(string $path, string $algo): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md index eeff1344cb..ea1f5610c9 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_openssl() — internals" description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." sidebar: - order: 450 + order: 454 --- ## `__elephc_phar_sign_openssl()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_sign_openssl(string $path, string $key): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index 12ea06148a..852c031755 100644 --- a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_strtotime_raw() — internals" description: "Compiler internals for __elephc_strtotime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 451 + order: 455 --- ## `__elephc_strtotime_raw()` — internals @@ -34,6 +34,10 @@ function __elephc_strtotime_raw(string $datetime, int $baseTimestamp = null): in - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/array/array_all.md b/docs/internals/builtins/array/array_all.md index 25d33ca7c2..e64aa09826 100644 --- a/docs/internals/builtins/array/array_all.md +++ b/docs/internals/builtins/array/array_all.md @@ -34,6 +34,10 @@ function array_all(mixed $array, mixed $callback): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_all()`](../../../php/builtins/array/array_all.md) diff --git a/docs/internals/builtins/array/array_any.md b/docs/internals/builtins/array/array_any.md index 3ade04e927..d64910b86f 100644 --- a/docs/internals/builtins/array/array_any.md +++ b/docs/internals/builtins/array/array_any.md @@ -33,6 +33,10 @@ function array_any(mixed $array, mixed $callback): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_any()`](../../../php/builtins/array/array_any.md) diff --git a/docs/internals/builtins/array/array_chunk.md b/docs/internals/builtins/array/array_chunk.md index 46bd53333e..9ec2e4beb4 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -32,6 +32,11 @@ function array_chunk(array $array, int $length): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_chunk()`](../../../php/builtins/array/array_chunk.md) diff --git a/docs/internals/builtins/array/array_column.md b/docs/internals/builtins/array/array_column.md index ee7a38c63c..0c353bc116 100644 --- a/docs/internals/builtins/array/array_column.md +++ b/docs/internals/builtins/array/array_column.md @@ -32,6 +32,11 @@ function array_column(array $array, string $column_key): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_column()`](../../../php/builtins/array/array_column.md) diff --git a/docs/internals/builtins/array/array_combine.md b/docs/internals/builtins/array/array_combine.md index 3443233226..d9c0911885 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -32,6 +32,11 @@ function array_combine(array $keys, array $values): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_combine()`](../../../php/builtins/array/array_combine.md) diff --git a/docs/internals/builtins/array/array_diff.md b/docs/internals/builtins/array/array_diff.md index 0b5cbcfb05..a0fdb30f53 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -38,6 +38,12 @@ function array_diff(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_diff()`](../../../php/builtins/array/array_diff.md) diff --git a/docs/internals/builtins/array/array_diff_assoc.md b/docs/internals/builtins/array/array_diff_assoc.md index 9d53e45ba5..b059876459 100644 --- a/docs/internals/builtins/array/array_diff_assoc.md +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -34,6 +34,10 @@ function array_diff_assoc(array $array, ...$arrays): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_diff_assoc()`](../../../php/builtins/array/array_diff_assoc.md) diff --git a/docs/internals/builtins/array/array_diff_key.md b/docs/internals/builtins/array/array_diff_key.md index d3278e96f6..4d316cfe7c 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -35,6 +35,12 @@ function array_diff_key(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_diff_key()`](../../../php/builtins/array/array_diff_key.md) diff --git a/docs/internals/builtins/array/array_fill.md b/docs/internals/builtins/array/array_fill.md index 507edba2c7..746629621b 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -32,6 +32,11 @@ function array_fill(int $start_index, int $count, mixed $value): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_fill()`](../../../php/builtins/array/array_fill.md) diff --git a/docs/internals/builtins/array/array_fill_keys.md b/docs/internals/builtins/array/array_fill_keys.md index 28c5fa95fb..e3cfab84a9 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -32,6 +32,11 @@ function array_fill_keys(array $keys, mixed $value): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_fill_keys()`](../../../php/builtins/array/array_fill_keys.md) diff --git a/docs/internals/builtins/array/array_filter.md b/docs/internals/builtins/array/array_filter.md index 55b3469842..99765841d7 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -34,6 +34,11 @@ function array_filter(array $array, callable $callback = null, int $mode = 0): a - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_filter()`](../../../php/builtins/array/array_filter.md) diff --git a/docs/internals/builtins/array/array_find.md b/docs/internals/builtins/array/array_find.md index e9df77a4fd..8463488ea6 100644 --- a/docs/internals/builtins/array/array_find.md +++ b/docs/internals/builtins/array/array_find.md @@ -33,6 +33,10 @@ function array_find(mixed $array, mixed $callback): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_find()`](../../../php/builtins/array/array_find.md) diff --git a/docs/internals/builtins/array/array_flip.md b/docs/internals/builtins/array/array_flip.md index d1b8864780..a848d576a9 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -32,6 +32,11 @@ function array_flip(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_flip()`](../../../php/builtins/array/array_flip.md) diff --git a/docs/internals/builtins/array/array_intersect.md b/docs/internals/builtins/array/array_intersect.md index e16a8026f6..7bf296e6cb 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -37,6 +37,12 @@ function array_intersect(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_intersect()`](../../../php/builtins/array/array_intersect.md) diff --git a/docs/internals/builtins/array/array_intersect_assoc.md b/docs/internals/builtins/array/array_intersect_assoc.md index eeea729525..b4c08d6146 100644 --- a/docs/internals/builtins/array/array_intersect_assoc.md +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -37,6 +37,10 @@ function array_intersect_assoc(array $array, ...$arrays): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_intersect_assoc()`](../../../php/builtins/array/array_intersect_assoc.md) diff --git a/docs/internals/builtins/array/array_intersect_key.md b/docs/internals/builtins/array/array_intersect_key.md index 5d15bd7f22..a3ad5c2f48 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -34,6 +34,12 @@ function array_intersect_key(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_intersect_key()`](../../../php/builtins/array/array_intersect_key.md) diff --git a/docs/internals/builtins/array/array_is_list.md b/docs/internals/builtins/array/array_is_list.md index 382509af6f..2428b51449 100644 --- a/docs/internals/builtins/array/array_is_list.md +++ b/docs/internals/builtins/array/array_is_list.md @@ -37,6 +37,10 @@ function array_is_list(mixed $array): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_is_list()`](../../../php/builtins/array/array_is_list.md) diff --git a/docs/internals/builtins/array/array_key_exists.md b/docs/internals/builtins/array/array_key_exists.md index d75a3f886a..8456be1378 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_hash_get` ## Signature summary @@ -32,6 +33,11 @@ function array_key_exists(string $key, array $array): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_key_exists()`](../../../php/builtins/array/array_key_exists.md) diff --git a/docs/internals/builtins/array/array_key_first.md b/docs/internals/builtins/array/array_key_first.md index 6e1607d78a..72b4203ed2 100644 --- a/docs/internals/builtins/array/array_key_first.md +++ b/docs/internals/builtins/array/array_key_first.md @@ -34,6 +34,10 @@ function array_key_first(array $array): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_key_first()`](../../../php/builtins/array/array_key_first.md) diff --git a/docs/internals/builtins/array/array_key_last.md b/docs/internals/builtins/array/array_key_last.md index dead1f9f6f..7c3c3d554a 100644 --- a/docs/internals/builtins/array/array_key_last.md +++ b/docs/internals/builtins/array/array_key_last.md @@ -34,6 +34,10 @@ function array_key_last(array $array): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_key_last()`](../../../php/builtins/array/array_key_last.md) diff --git a/docs/internals/builtins/array/array_keys.md b/docs/internals/builtins/array/array_keys.md index 4456b39e94..46a8faedfe 100644 --- a/docs/internals/builtins/array/array_keys.md +++ b/docs/internals/builtins/array/array_keys.md @@ -32,6 +32,11 @@ function array_keys(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_keys()`](../../../php/builtins/array/array_keys.md) diff --git a/docs/internals/builtins/array/array_map.md b/docs/internals/builtins/array/array_map.md index fab8abb735..b4c347c37d 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -33,6 +33,12 @@ function array_map(callable $callback, array $array, ...$arrays): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_map()`](../../../php/builtins/array/array_map.md) diff --git a/docs/internals/builtins/array/array_merge.md b/docs/internals/builtins/array/array_merge.md index afd57e2ddf..28d753b6a4 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -35,6 +35,12 @@ function array_merge(...$arrays): array - **Arity**: takes no arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_merge()`](../../../php/builtins/array/array_merge.md) diff --git a/docs/internals/builtins/array/array_merge_recursive.md b/docs/internals/builtins/array/array_merge_recursive.md index 2b9dcd6b1f..d851d593ca 100644 --- a/docs/internals/builtins/array/array_merge_recursive.md +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -36,6 +36,10 @@ function array_merge_recursive(...$arrays): array - **Arity**: takes no arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_merge_recursive()`](../../../php/builtins/array/array_merge_recursive.md) diff --git a/docs/internals/builtins/array/array_multisort.md b/docs/internals/builtins/array/array_multisort.md index cbfe1d8d7b..9bc8960212 100644 --- a/docs/internals/builtins/array/array_multisort.md +++ b/docs/internals/builtins/array/array_multisort.md @@ -36,6 +36,10 @@ function array_multisort(array $array1, int $array2): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array1`, `$array2`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_multisort()`](../../../php/builtins/array/array_multisort.md) diff --git a/docs/internals/builtins/array/array_pad.md b/docs/internals/builtins/array/array_pad.md index d24cbcc12e..c2c4958ad5 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -32,6 +32,11 @@ function array_pad(array $array, int $length, mixed $value): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_pad()`](../../../php/builtins/array/array_pad.md) diff --git a/docs/internals/builtins/array/array_pop.md b/docs/internals/builtins/array/array_pop.md index b524438a57..9f3e0fa6e7 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -35,6 +35,12 @@ function array_pop(array $array): mixed - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_pop()`](../../../php/builtins/array/array_pop.md) diff --git a/docs/internals/builtins/array/array_product.md b/docs/internals/builtins/array/array_product.md index 9823cef4cb..c049163212 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -33,6 +33,11 @@ function array_product(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_product()`](../../../php/builtins/array/array_product.md) diff --git a/docs/internals/builtins/array/array_push.md b/docs/internals/builtins/array/array_push.md index daf4d4d7b6..794b999892 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -34,6 +34,13 @@ function array_push(array $array, ...$values): void - **By-reference parameters**: `$array`. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `array_push()`](../../../php/builtins/array/array_push.md) diff --git a/docs/internals/builtins/array/array_rand.md b/docs/internals/builtins/array/array_rand.md index 8c035ab48a..867f9a04c8 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -34,6 +34,11 @@ function array_rand(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_rand()`](../../../php/builtins/array/array_rand.md) diff --git a/docs/internals/builtins/array/array_reduce.md b/docs/internals/builtins/array/array_reduce.md index 302d47051e..e01de0eda4 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -33,6 +33,11 @@ function array_reduce(array $array, callable $callback, mixed $initial = null): - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_reduce()`](../../../php/builtins/array/array_reduce.md) diff --git a/docs/internals/builtins/array/array_replace.md b/docs/internals/builtins/array/array_replace.md index 2df08cdd42..65ea065fa3 100644 --- a/docs/internals/builtins/array/array_replace.md +++ b/docs/internals/builtins/array/array_replace.md @@ -35,6 +35,10 @@ function array_replace(array $array, array $replacements): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_replace()`](../../../php/builtins/array/array_replace.md) diff --git a/docs/internals/builtins/array/array_replace_recursive.md b/docs/internals/builtins/array/array_replace_recursive.md index d85275ea3f..769ca5ef1d 100644 --- a/docs/internals/builtins/array/array_replace_recursive.md +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -34,6 +34,10 @@ function array_replace_recursive(array $array, array $replacements): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_replace_recursive()`](../../../php/builtins/array/array_replace_recursive.md) diff --git a/docs/internals/builtins/array/array_reverse.md b/docs/internals/builtins/array/array_reverse.md index fec27a94d2..f14f6ad2e9 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -32,6 +32,11 @@ function array_reverse(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_reverse()`](../../../php/builtins/array/array_reverse.md) diff --git a/docs/internals/builtins/array/array_search.md b/docs/internals/builtins/array/array_search.md index 2d0fc7acc5..d6b70200c1 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -32,6 +32,11 @@ function array_search(mixed $needle, array $haystack, bool $strict = false): mix - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_search()`](../../../php/builtins/array/array_search.md) diff --git a/docs/internals/builtins/array/array_shift.md b/docs/internals/builtins/array/array_shift.md index a724e7335d..1c3edef69b 100644 --- a/docs/internals/builtins/array/array_shift.md +++ b/docs/internals/builtins/array/array_shift.md @@ -33,6 +33,12 @@ function array_shift(array $array): mixed - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_shift()`](../../../php/builtins/array/array_shift.md) diff --git a/docs/internals/builtins/array/array_slice.md b/docs/internals/builtins/array/array_slice.md index f7c38d4b7f..5f9ef633dc 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -32,6 +32,11 @@ function array_slice(array $array, int $offset, int $length = null): array - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_slice()`](../../../php/builtins/array/array_slice.md) diff --git a/docs/internals/builtins/array/array_splice.md b/docs/internals/builtins/array/array_splice.md index 9253e05907..5ce948dbfb 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -33,6 +33,12 @@ function array_splice(array $array, int $offset, int $length = null): array - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_splice()`](../../../php/builtins/array/array_splice.md) diff --git a/docs/internals/builtins/array/array_sum.md b/docs/internals/builtins/array/array_sum.md index 5157c8d94b..80f9b86e9a 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -34,6 +34,11 @@ function array_sum(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_sum()`](../../../php/builtins/array/array_sum.md) diff --git a/docs/internals/builtins/array/array_udiff.md b/docs/internals/builtins/array/array_udiff.md index 532e8c0091..782c00f8f1 100644 --- a/docs/internals/builtins/array/array_udiff.md +++ b/docs/internals/builtins/array/array_udiff.md @@ -32,6 +32,10 @@ function array_udiff(array $array1, array $array2, callable $callback): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_udiff()`](../../../php/builtins/array/array_udiff.md) diff --git a/docs/internals/builtins/array/array_uintersect.md b/docs/internals/builtins/array/array_uintersect.md index 3e785eacad..682ebfb06b 100644 --- a/docs/internals/builtins/array/array_uintersect.md +++ b/docs/internals/builtins/array/array_uintersect.md @@ -32,6 +32,10 @@ function array_uintersect(array $array1, array $array2, callable $callback): arr - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_uintersect()`](../../../php/builtins/array/array_uintersect.md) diff --git a/docs/internals/builtins/array/array_unique.md b/docs/internals/builtins/array/array_unique.md index 25f753cfee..8c1d7c4e01 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -34,6 +34,11 @@ function array_unique(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_unique()`](../../../php/builtins/array/array_unique.md) diff --git a/docs/internals/builtins/array/array_unshift.md b/docs/internals/builtins/array/array_unshift.md index 5a84c4c60c..9abb25dde7 100644 --- a/docs/internals/builtins/array/array_unshift.md +++ b/docs/internals/builtins/array/array_unshift.md @@ -34,6 +34,13 @@ function array_unshift(array $array, ...$values): int - **By-reference parameters**: `$array`. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `array_unshift()`](../../../php/builtins/array/array_unshift.md) diff --git a/docs/internals/builtins/array/array_values.md b/docs/internals/builtins/array/array_values.md index b5b3b90a05..3ef6505258 100644 --- a/docs/internals/builtins/array/array_values.md +++ b/docs/internals/builtins/array/array_values.md @@ -32,6 +32,11 @@ function array_values(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_values()`](../../../php/builtins/array/array_values.md) diff --git a/docs/internals/builtins/array/array_walk.md b/docs/internals/builtins/array/array_walk.md index 590629ecd4..d77c9e9ed9 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -34,6 +34,12 @@ function array_walk(array $array, callable $callback): void - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_walk()`](../../../php/builtins/array/array_walk.md) diff --git a/docs/internals/builtins/array/array_walk_recursive.md b/docs/internals/builtins/array/array_walk_recursive.md index ae21641238..5abc52c9ff 100644 --- a/docs/internals/builtins/array/array_walk_recursive.md +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -36,6 +36,10 @@ function array_walk_recursive(array $array, callable $callback): void - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_walk_recursive()`](../../../php/builtins/array/array_walk_recursive.md) diff --git a/docs/internals/builtins/array/arsort.md b/docs/internals/builtins/array/arsort.md index b30e0b6ea0..b301f7e3bb 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -38,6 +38,12 @@ function arsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `arsort()`](../../../php/builtins/array/arsort.md) diff --git a/docs/internals/builtins/array/asort.md b/docs/internals/builtins/array/asort.md index d4cff7b4bd..0b1be75076 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -39,6 +39,12 @@ function asort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/asort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `asort()`](../../../php/builtins/array/asort.md) diff --git a/docs/internals/builtins/array/call_user_func.md b/docs/internals/builtins/array/call_user_func.md index 281edbe9f8..1692669873 100644 --- a/docs/internals/builtins/array/call_user_func.md +++ b/docs/internals/builtins/array/call_user_func.md @@ -35,6 +35,12 @@ function call_user_func(callable $callback, ...$args): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$args`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$args`. + ## Cross-references - [User reference for `call_user_func()`](../../../php/builtins/array/call_user_func.md) diff --git a/docs/internals/builtins/array/call_user_func_array.md b/docs/internals/builtins/array/call_user_func_array.md index ac8baf6e82..66e41e7a40 100644 --- a/docs/internals/builtins/array/call_user_func_array.md +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -34,6 +34,11 @@ function call_user_func_array(callable $callback, array $args): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `call_user_func_array()`](../../../php/builtins/array/call_user_func_array.md) diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index ff5bb8097d..d37be19c5c 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:440](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L440) (`lower_count`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1025](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1025) (`lower_count`) - **Function symbol**: `lower_count()` @@ -37,6 +37,11 @@ function count(array $value, int $mode = 0): int - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/count.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `count()`](../../../php/builtins/array/count.md) diff --git a/docs/internals/builtins/array/in_array.md b/docs/internals/builtins/array/in_array.md index eaceecf097..ab394f7e47 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -32,6 +32,11 @@ function in_array(mixed $needle, array $haystack, bool $strict = false): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `in_array()`](../../../php/builtins/array/in_array.md) diff --git a/docs/internals/builtins/array/krsort.md b/docs/internals/builtins/array/krsort.md index ccd956e871..efe453a87e 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -36,6 +36,12 @@ function krsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `krsort()`](../../../php/builtins/array/krsort.md) diff --git a/docs/internals/builtins/array/ksort.md b/docs/internals/builtins/array/ksort.md index dc4160fc4e..49e52da901 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -37,6 +37,12 @@ function ksort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `ksort()`](../../../php/builtins/array/ksort.md) diff --git a/docs/internals/builtins/array/natcasesort.md b/docs/internals/builtins/array/natcasesort.md index ab599f0366..6caa65cf61 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -34,6 +34,12 @@ function natcasesort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `natcasesort()`](../../../php/builtins/array/natcasesort.md) diff --git a/docs/internals/builtins/array/natsort.md b/docs/internals/builtins/array/natsort.md index 0321f5afde..062387d3da 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -35,6 +35,12 @@ function natsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `natsort()`](../../../php/builtins/array/natsort.md) diff --git a/docs/internals/builtins/array/range.md b/docs/internals/builtins/array/range.md index c213e1f81c..2d2f03be74 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -34,6 +34,11 @@ function range(mixed $start, mixed $end): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/range.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `range()`](../../../php/builtins/array/range.md) diff --git a/docs/internals/builtins/array/rsort.md b/docs/internals/builtins/array/rsort.md index 0f78283380..d81e68cf20 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -40,6 +40,12 @@ function rsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `rsort()`](../../../php/builtins/array/rsort.md) diff --git a/docs/internals/builtins/array/shuffle.md b/docs/internals/builtins/array/shuffle.md index 0e34663a33..a203037bf6 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -34,6 +34,12 @@ function shuffle(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `shuffle()`](../../../php/builtins/array/shuffle.md) diff --git a/docs/internals/builtins/array/sort.md b/docs/internals/builtins/array/sort.md index 9a811cf5d8..67fe1986cd 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -41,6 +41,12 @@ function sort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/sort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `sort()`](../../../php/builtins/array/sort.md) diff --git a/docs/internals/builtins/array/uasort.md b/docs/internals/builtins/array/uasort.md index 21f67fbc5a..db9a427472 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -34,6 +34,12 @@ function uasort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `uasort()`](../../../php/builtins/array/uasort.md) diff --git a/docs/internals/builtins/array/uksort.md b/docs/internals/builtins/array/uksort.md index 3357f721f8..7ada40c5ba 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -34,6 +34,12 @@ function uksort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `uksort()`](../../../php/builtins/array/uksort.md) diff --git a/docs/internals/builtins/array/usort.md b/docs/internals/builtins/array/usort.md index 66427c380f..ddbae1bcb6 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -34,6 +34,12 @@ function usort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/usort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `usort()`](../../../php/builtins/array/usort.md) diff --git a/docs/internals/builtins/buffer/buffer_free.md b/docs/internals/builtins/buffer/buffer_free.md index a49ffcb874..c54011722f 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -32,6 +32,11 @@ function buffer_free(buffer $buffer): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_free()`](../../../php/builtins/buffer/buffer_free.md) diff --git a/docs/internals/builtins/buffer/buffer_len.md b/docs/internals/builtins/buffer/buffer_len.md index ce22f1982f..a61052032f 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -32,6 +32,11 @@ function buffer_len(buffer $buffer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_len()`](../../../php/builtins/buffer/buffer_len.md) diff --git a/docs/internals/builtins/class/class_alias.md b/docs/internals/builtins/class/class_alias.md index b578f7661e..13ee959fcd 100644 --- a/docs/internals/builtins/class/class_alias.md +++ b/docs/internals/builtins/class/class_alias.md @@ -32,6 +32,11 @@ function class_alias(string $class, string $alias, bool $autoload = true): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_alias()`](../../../php/builtins/class/class_alias.md) diff --git a/docs/internals/builtins/class/class_attribute_args.md b/docs/internals/builtins/class/class_attribute_args.md index c94a5df71d..f6df5979d8 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_args.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L52) (`lower_class_attribute_args`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:62](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L62) (`lower_class_attribute_args`) - **Function symbol**: `lower_class_attribute_args()` ### Lowering notes -- Lowers `class_attribute_args(class, attr)` into an indexed Mixed array. +- Lowers `class_attribute_args(class, attr)` into a Mixed PHP argument array. ## Runtime helpers @@ -32,6 +32,11 @@ function class_attribute_args(string $class_name, string $attribute_name): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_attribute_args()`](../../../php/builtins/class/class_attribute_args.md) diff --git a/docs/internals/builtins/class/class_attribute_names.md b/docs/internals/builtins/class/class_attribute_names.md index cf38873252..82453203e7 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_names.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:36](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L36) (`lower_class_attribute_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:46](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L46) (`lower_class_attribute_names`) - **Function symbol**: `lower_class_attribute_names()` @@ -32,6 +32,11 @@ function class_attribute_names(string $class_name): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_attribute_names()`](../../../php/builtins/class/class_attribute_names.md) diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index cc3671b2f7..833594322e 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers @@ -32,6 +32,11 @@ function class_exists(string $class, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_exists()`](../../../php/builtins/class/class_exists.md) diff --git a/docs/internals/builtins/class/class_get_attributes.md b/docs/internals/builtins/class/class_get_attributes.md index 7ae201dc05..14c69bdcd7 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_get_attributes.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:68](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L68) (`lower_class_get_attributes`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:78](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L78) (`lower_class_get_attributes`) - **Function symbol**: `lower_class_get_attributes()` @@ -32,6 +32,11 @@ function class_get_attributes(string $class_name): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_get_attributes()`](../../../php/builtins/class/class_get_attributes.md) diff --git a/docs/internals/builtins/class/class_implements.md b/docs/internals/builtins/class/class_implements.md index 982dcc0290..c2dfafcc1b 100644 --- a/docs/internals/builtins/class/class_implements.md +++ b/docs/internals/builtins/class/class_implements.md @@ -32,6 +32,11 @@ function class_implements(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_implements()`](../../../php/builtins/class/class_implements.md) diff --git a/docs/internals/builtins/class/class_parents.md b/docs/internals/builtins/class/class_parents.md index 95d5f91740..9e3a631be8 100644 --- a/docs/internals/builtins/class/class_parents.md +++ b/docs/internals/builtins/class/class_parents.md @@ -32,6 +32,11 @@ function class_parents(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_parents()`](../../../php/builtins/class/class_parents.md) diff --git a/docs/internals/builtins/class/class_uses.md b/docs/internals/builtins/class/class_uses.md index ad4517f2e0..0bb5944156 100644 --- a/docs/internals/builtins/class/class_uses.md +++ b/docs/internals/builtins/class/class_uses.md @@ -32,6 +32,11 @@ function class_uses(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_uses()`](../../../php/builtins/class/class_uses.md) diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 4d97bdcb0a..4b0d6369fb 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/enum_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers @@ -32,6 +32,11 @@ function enum_exists(string $enum, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `enum_exists()`](../../../php/builtins/class/enum_exists.md) diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index e72e15dfbb..448b43c900 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:276](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L276) (`lower_function_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:566](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L566) (`lower_function_exists`) - **Function symbol**: `lower_function_exists()` @@ -37,6 +37,11 @@ function function_exists(string $function): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `function_exists()`](../../../php/builtins/class/function_exists.md) diff --git a/docs/internals/builtins/class/get_called_class.md b/docs/internals/builtins/class/get_called_class.md new file mode 100644 index 0000000000..d9890bf301 --- /dev/null +++ b/docs/internals/builtins/class/get_called_class.md @@ -0,0 +1,38 @@ +--- +title: "get_called_class() — internals" +description: "Compiler internals for get_called_class(): lowering path, type checks, and runtime helpers." +sidebar: + order: 76 +--- + +## `get_called_class()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_called_class(): mixed +``` + +## What the type checker enforces + +- **Arity**: takes no arguments. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_called_class()`](../../../php/builtins/class/get_called_class.md) diff --git a/docs/internals/builtins/class/get_class.md b/docs/internals/builtins/class/get_class.md index 323193ac61..868ca17e9a 100644 --- a/docs/internals/builtins/class/get_class.md +++ b/docs/internals/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class() — internals" description: "Compiler internals for get_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 76 + order: 77 --- ## `get_class()` — internals @@ -32,6 +32,11 @@ function get_class(object $object = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_class()`](../../../php/builtins/class/get_class.md) diff --git a/docs/internals/builtins/class/get_class_methods.md b/docs/internals/builtins/class/get_class_methods.md new file mode 100644 index 0000000000..39bdd7cbae --- /dev/null +++ b/docs/internals/builtins/class/get_class_methods.md @@ -0,0 +1,38 @@ +--- +title: "get_class_methods() — internals" +description: "Compiler internals for get_class_methods(): lowering path, type checks, and runtime helpers." +sidebar: + order: 78 +--- + +## `get_class_methods()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_class_methods(mixed $object_or_class): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_class_methods()`](../../../php/builtins/class/get_class_methods.md) diff --git a/docs/internals/builtins/class/get_class_vars.md b/docs/internals/builtins/class/get_class_vars.md new file mode 100644 index 0000000000..20e1181fb2 --- /dev/null +++ b/docs/internals/builtins/class/get_class_vars.md @@ -0,0 +1,38 @@ +--- +title: "get_class_vars() — internals" +description: "Compiler internals for get_class_vars(): lowering path, type checks, and runtime helpers." +sidebar: + order: 79 +--- + +## `get_class_vars()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_class_vars(mixed $class): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_class_vars()`](../../../php/builtins/class/get_class_vars.md) diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index aae271d112..ed778ad9fc 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes() — internals" description: "Compiler internals for get_declared_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 77 + order: 80 --- ## `get_declared_classes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_classes.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` @@ -32,6 +32,11 @@ function get_declared_classes(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_classes()`](../../../php/builtins/class/get_declared_classes.md) diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index 05df0d4739..c39bd5665f 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces() — internals" description: "Compiler internals for get_declared_interfaces(): lowering path, type checks, and runtime helpers." sidebar: - order: 78 + order: 81 --- ## `get_declared_interfaces()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_interfaces.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` @@ -32,6 +32,11 @@ function get_declared_interfaces(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_interfaces()`](../../../php/builtins/class/get_declared_interfaces.md) diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index f9a034bfc1..c57b207a5b 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits() — internals" description: "Compiler internals for get_declared_traits(): lowering path, type checks, and runtime helpers." sidebar: - order: 79 + order: 82 --- ## `get_declared_traits()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_traits.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` @@ -32,6 +32,11 @@ function get_declared_traits(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_traits()`](../../../php/builtins/class/get_declared_traits.md) diff --git a/docs/internals/builtins/class/get_object_vars.md b/docs/internals/builtins/class/get_object_vars.md new file mode 100644 index 0000000000..335d79dbba --- /dev/null +++ b/docs/internals/builtins/class/get_object_vars.md @@ -0,0 +1,38 @@ +--- +title: "get_object_vars() — internals" +description: "Compiler internals for get_object_vars(): lowering path, type checks, and runtime helpers." +sidebar: + order: 83 +--- + +## `get_object_vars()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_object_vars(mixed $object): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_object_vars()`](../../../php/builtins/class/get_object_vars.md) diff --git a/docs/internals/builtins/class/get_parent_class.md b/docs/internals/builtins/class/get_parent_class.md index a574bde029..50d0001915 100644 --- a/docs/internals/builtins/class/get_parent_class.md +++ b/docs/internals/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class() — internals" description: "Compiler internals for get_parent_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 80 + order: 84 --- ## `get_parent_class()` — internals @@ -32,6 +32,11 @@ function get_parent_class(mixed $object_or_class = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_parent_class()`](../../../php/builtins/class/get_parent_class.md) diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index 11a13350ab..848f61c5fa 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists() — internals" description: "Compiler internals for interface_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 81 + order: 85 --- ## `interface_exists()` — internals @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/interface_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers @@ -32,6 +32,11 @@ function interface_exists(string $interface, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `interface_exists()`](../../../php/builtins/class/interface_exists.md) diff --git a/docs/internals/builtins/class/is_a.md b/docs/internals/builtins/class/is_a.md index 3ca72ca135..cfd08acde5 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a() — internals" description: "Compiler internals for is_a(): lowering path, type checks, and runtime helpers." sidebar: - order: 82 + order: 86 --- ## `is_a()` — internals @@ -32,6 +32,11 @@ function is_a(object $object_or_class, string $class, bool $allow_string = false - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_a()`](../../../php/builtins/class/is_a.md) diff --git a/docs/internals/builtins/class/is_subclass_of.md b/docs/internals/builtins/class/is_subclass_of.md index 404beddeac..d18e619544 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of() — internals" description: "Compiler internals for is_subclass_of(): lowering path, type checks, and runtime helpers." sidebar: - order: 83 + order: 87 --- ## `is_subclass_of()` — internals @@ -32,6 +32,11 @@ function is_subclass_of(mixed $object_or_class, string $class, bool $allow_strin - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_subclass_of()`](../../../php/builtins/class/is_subclass_of.md) diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index 93e606d30c..4632d523a5 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists() — internals" description: "Compiler internals for trait_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 84 + order: 88 --- ## `trait_exists()` — internals @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/trait_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers @@ -32,6 +32,11 @@ function trait_exists(string $trait, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `trait_exists()`](../../../php/builtins/class/trait_exists.md) diff --git a/docs/internals/builtins/date/checkdate.md b/docs/internals/builtins/date/checkdate.md index db4899f487..c5f9ace822 100644 --- a/docs/internals/builtins/date/checkdate.md +++ b/docs/internals/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate() — internals" description: "Compiler internals for checkdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 85 + order: 89 --- ## `checkdate()` — internals @@ -37,6 +37,11 @@ function checkdate(int $month, int $day, int $year): bool - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `checkdate()`](../../../php/builtins/date/checkdate.md) diff --git a/docs/internals/builtins/date/date.md b/docs/internals/builtins/date/date.md index 284b39e58b..cc10b987d3 100644 --- a/docs/internals/builtins/date/date.md +++ b/docs/internals/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date() — internals" description: "Compiler internals for date(): lowering path, type checks, and runtime helpers." sidebar: - order: 86 + order: 90 --- ## `date()` — internals @@ -34,6 +34,11 @@ function date(string $format, int $timestamp = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date()`](../../../php/builtins/date/date.md) diff --git a/docs/internals/builtins/date/date_default_timezone_get.md b/docs/internals/builtins/date/date_default_timezone_get.md index cc1cbfbdb6..182e6b32d4 100644 --- a/docs/internals/builtins/date/date_default_timezone_get.md +++ b/docs/internals/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get() — internals" description: "Compiler internals for date_default_timezone_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 87 + order: 91 --- ## `date_default_timezone_get()` — internals @@ -36,6 +36,11 @@ function date_default_timezone_get(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date_default_timezone_get()`](../../../php/builtins/date/date_default_timezone_get.md) diff --git a/docs/internals/builtins/date/date_default_timezone_set.md b/docs/internals/builtins/date/date_default_timezone_set.md index 9cfee9f4a2..9815d9f977 100644 --- a/docs/internals/builtins/date/date_default_timezone_set.md +++ b/docs/internals/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set() — internals" description: "Compiler internals for date_default_timezone_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 88 + order: 92 --- ## `date_default_timezone_set()` — internals @@ -39,6 +39,11 @@ function date_default_timezone_set(string $timezoneId): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date_default_timezone_set()`](../../../php/builtins/date/date_default_timezone_set.md) diff --git a/docs/internals/builtins/date/getdate.md b/docs/internals/builtins/date/getdate.md index f13d4477cd..ce42d81bd9 100644 --- a/docs/internals/builtins/date/getdate.md +++ b/docs/internals/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate() — internals" description: "Compiler internals for getdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 89 + order: 93 --- ## `getdate()` — internals @@ -38,6 +38,11 @@ function getdate(int $timestamp = null): array - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getdate()`](../../../php/builtins/date/getdate.md) diff --git a/docs/internals/builtins/date/gmdate.md b/docs/internals/builtins/date/gmdate.md index a1d773144b..30a22d4975 100644 --- a/docs/internals/builtins/date/gmdate.md +++ b/docs/internals/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate() — internals" description: "Compiler internals for gmdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 90 + order: 94 --- ## `gmdate()` — internals @@ -36,6 +36,11 @@ function gmdate(string $format, int $timestamp = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gmdate()`](../../../php/builtins/date/gmdate.md) diff --git a/docs/internals/builtins/date/gmmktime.md b/docs/internals/builtins/date/gmmktime.md index e711735072..ac6c4037ef 100644 --- a/docs/internals/builtins/date/gmmktime.md +++ b/docs/internals/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime() — internals" description: "Compiler internals for gmmktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 91 + order: 95 --- ## `gmmktime()` — internals @@ -37,6 +37,11 @@ function gmmktime(int $hour, int $minute, int $second, int $month, int $day, int - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gmmktime()`](../../../php/builtins/date/gmmktime.md) diff --git a/docs/internals/builtins/date/hrtime.md b/docs/internals/builtins/date/hrtime.md index fb9cb930cc..4c8b97a9fe 100644 --- a/docs/internals/builtins/date/hrtime.md +++ b/docs/internals/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime() — internals" description: "Compiler internals for hrtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 92 + order: 96 --- ## `hrtime()` — internals @@ -38,6 +38,11 @@ function hrtime(bool $as_number = false): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hrtime()`](../../../php/builtins/date/hrtime.md) diff --git a/docs/internals/builtins/date/localtime.md b/docs/internals/builtins/date/localtime.md index 38d517a792..f966a72fca 100644 --- a/docs/internals/builtins/date/localtime.md +++ b/docs/internals/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime() — internals" description: "Compiler internals for localtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 93 + order: 97 --- ## `localtime()` — internals @@ -39,6 +39,11 @@ function localtime(int $timestamp = -1, bool $associative = false): array - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `localtime()`](../../../php/builtins/date/localtime.md) diff --git a/docs/internals/builtins/date/microtime.md b/docs/internals/builtins/date/microtime.md index cf145e0101..0d3b435b9f 100644 --- a/docs/internals/builtins/date/microtime.md +++ b/docs/internals/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime() — internals" description: "Compiler internals for microtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 94 + order: 98 --- ## `microtime()` — internals @@ -42,6 +42,11 @@ function microtime(bool $as_float = false): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `microtime()`](../../../php/builtins/date/microtime.md) diff --git a/docs/internals/builtins/date/mktime.md b/docs/internals/builtins/date/mktime.md index 58eb3e8b67..296b4cbf3e 100644 --- a/docs/internals/builtins/date/mktime.md +++ b/docs/internals/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime() — internals" description: "Compiler internals for mktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 95 + order: 99 --- ## `mktime()` — internals @@ -35,6 +35,11 @@ function mktime(int $hour, int $minute, int $second, int $month, int $day, int $ - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mktime()`](../../../php/builtins/date/mktime.md) diff --git a/docs/internals/builtins/date/strtotime.md b/docs/internals/builtins/date/strtotime.md index 8075a0c1e5..f8ec487bff 100644 --- a/docs/internals/builtins/date/strtotime.md +++ b/docs/internals/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime() — internals" description: "Compiler internals for strtotime(): lowering path, type checks, and runtime helpers." sidebar: - order: 96 + order: 100 --- ## `strtotime()` — internals @@ -39,6 +39,11 @@ function strtotime(string $datetime, int $baseTimestamp = null): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtotime()`](../../../php/builtins/date/strtotime.md) diff --git a/docs/internals/builtins/date/time.md b/docs/internals/builtins/date/time.md index 6a5cb71832..25def47f16 100644 --- a/docs/internals/builtins/date/time.md +++ b/docs/internals/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time() — internals" description: "Compiler internals for time(): lowering path, type checks, and runtime helpers." sidebar: - order: 97 + order: 101 --- ## `time()` — internals @@ -33,6 +33,11 @@ function time(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/time.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/time.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `time()`](../../../php/builtins/date/time.md) diff --git a/docs/internals/builtins/filesystem/basename.md b/docs/internals/builtins/filesystem/basename.md index 9c4395d000..777532af10 100644 --- a/docs/internals/builtins/filesystem/basename.md +++ b/docs/internals/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename() — internals" description: "Compiler internals for basename(): lowering path, type checks, and runtime helpers." sidebar: - order: 98 + order: 102 --- ## `basename()` — internals @@ -32,6 +32,11 @@ function basename(string $path, string $suffix = ''): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `basename()`](../../../php/builtins/filesystem/basename.md) diff --git a/docs/internals/builtins/filesystem/chdir.md b/docs/internals/builtins/filesystem/chdir.md index 29f810231c..d586ef347c 100644 --- a/docs/internals/builtins/filesystem/chdir.md +++ b/docs/internals/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir() — internals" description: "Compiler internals for chdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 99 + order: 103 --- ## `chdir()` — internals @@ -37,6 +37,11 @@ function chdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chdir()`](../../../php/builtins/filesystem/chdir.md) diff --git a/docs/internals/builtins/filesystem/chgrp.md b/docs/internals/builtins/filesystem/chgrp.md index 088d98a680..bfb096c9b5 100644 --- a/docs/internals/builtins/filesystem/chgrp.md +++ b/docs/internals/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp() — internals" description: "Compiler internals for chgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 100 + order: 104 --- ## `chgrp()` — internals @@ -33,6 +33,11 @@ function chgrp(string $filename, string $group): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chgrp()`](../../../php/builtins/filesystem/chgrp.md) diff --git a/docs/internals/builtins/filesystem/chmod.md b/docs/internals/builtins/filesystem/chmod.md index 3b5e76e916..8967dc2314 100644 --- a/docs/internals/builtins/filesystem/chmod.md +++ b/docs/internals/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod() — internals" description: "Compiler internals for chmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 101 + order: 105 --- ## `chmod()` — internals @@ -32,6 +32,11 @@ function chmod(string $filename, int $permissions): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chmod()`](../../../php/builtins/filesystem/chmod.md) diff --git a/docs/internals/builtins/filesystem/chown.md b/docs/internals/builtins/filesystem/chown.md index ac86b3b4e9..ff024970ad 100644 --- a/docs/internals/builtins/filesystem/chown.md +++ b/docs/internals/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown() — internals" description: "Compiler internals for chown(): lowering path, type checks, and runtime helpers." sidebar: - order: 102 + order: 106 --- ## `chown()` — internals @@ -33,6 +33,11 @@ function chown(string $filename, string $user): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chown()`](../../../php/builtins/filesystem/chown.md) diff --git a/docs/internals/builtins/filesystem/clearstatcache.md b/docs/internals/builtins/filesystem/clearstatcache.md index 2417f80455..215e90a5b2 100644 --- a/docs/internals/builtins/filesystem/clearstatcache.md +++ b/docs/internals/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache() — internals" description: "Compiler internals for clearstatcache(): lowering path, type checks, and runtime helpers." sidebar: - order: 103 + order: 107 --- ## `clearstatcache()` — internals @@ -33,6 +33,11 @@ function clearstatcache(bool $clear_realpath_cache = false, string $filename = ' - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `clearstatcache()`](../../../php/builtins/filesystem/clearstatcache.md) diff --git a/docs/internals/builtins/filesystem/copy.md b/docs/internals/builtins/filesystem/copy.md index dfc16a31fa..0ff3e31231 100644 --- a/docs/internals/builtins/filesystem/copy.md +++ b/docs/internals/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy() — internals" description: "Compiler internals for copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 104 + order: 108 --- ## `copy()` — internals @@ -36,6 +36,11 @@ function copy(string $from, string $to): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `copy()`](../../../php/builtins/filesystem/copy.md) diff --git a/docs/internals/builtins/filesystem/dirname.md b/docs/internals/builtins/filesystem/dirname.md index 6c38a29489..a6b8cee27d 100644 --- a/docs/internals/builtins/filesystem/dirname.md +++ b/docs/internals/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname() — internals" description: "Compiler internals for dirname(): lowering path, type checks, and runtime helpers." sidebar: - order: 105 + order: 109 --- ## `dirname()` — internals @@ -34,6 +34,11 @@ function dirname(string $path, int $levels = 1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `dirname()`](../../../php/builtins/filesystem/dirname.md) diff --git a/docs/internals/builtins/filesystem/disk_free_space.md b/docs/internals/builtins/filesystem/disk_free_space.md index 8243b6f2e8..f9b7096bf0 100644 --- a/docs/internals/builtins/filesystem/disk_free_space.md +++ b/docs/internals/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space() — internals" description: "Compiler internals for disk_free_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 106 + order: 110 --- ## `disk_free_space()` — internals @@ -33,6 +33,11 @@ function disk_free_space(string $directory): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `disk_free_space()`](../../../php/builtins/filesystem/disk_free_space.md) diff --git a/docs/internals/builtins/filesystem/disk_total_space.md b/docs/internals/builtins/filesystem/disk_total_space.md index 85d7f85e99..c4bfc3d60d 100644 --- a/docs/internals/builtins/filesystem/disk_total_space.md +++ b/docs/internals/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space() — internals" description: "Compiler internals for disk_total_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 107 + order: 111 --- ## `disk_total_space()` — internals @@ -33,6 +33,11 @@ function disk_total_space(string $directory): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `disk_total_space()`](../../../php/builtins/filesystem/disk_total_space.md) diff --git a/docs/internals/builtins/filesystem/file_exists.md b/docs/internals/builtins/filesystem/file_exists.md index a73bca3467..f8d6c1f7aa 100644 --- a/docs/internals/builtins/filesystem/file_exists.md +++ b/docs/internals/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists() — internals" description: "Compiler internals for file_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 108 + order: 112 --- ## `file_exists()` — internals @@ -34,6 +34,11 @@ function file_exists(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_exists()`](../../../php/builtins/filesystem/file_exists.md) diff --git a/docs/internals/builtins/filesystem/fileatime.md b/docs/internals/builtins/filesystem/fileatime.md index 7233c8305e..21e3869e78 100644 --- a/docs/internals/builtins/filesystem/fileatime.md +++ b/docs/internals/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime() — internals" description: "Compiler internals for fileatime(): lowering path, type checks, and runtime helpers." sidebar: - order: 109 + order: 113 --- ## `fileatime()` — internals @@ -36,6 +36,11 @@ function fileatime(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileatime()`](../../../php/builtins/filesystem/fileatime.md) diff --git a/docs/internals/builtins/filesystem/filectime.md b/docs/internals/builtins/filesystem/filectime.md index 7731c01108..703519fe73 100644 --- a/docs/internals/builtins/filesystem/filectime.md +++ b/docs/internals/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime() — internals" description: "Compiler internals for filectime(): lowering path, type checks, and runtime helpers." sidebar: - order: 110 + order: 114 --- ## `filectime()` — internals @@ -36,6 +36,11 @@ function filectime(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filectime()`](../../../php/builtins/filesystem/filectime.md) diff --git a/docs/internals/builtins/filesystem/filegroup.md b/docs/internals/builtins/filesystem/filegroup.md index ac29c14fc7..e0338d606f 100644 --- a/docs/internals/builtins/filesystem/filegroup.md +++ b/docs/internals/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup() — internals" description: "Compiler internals for filegroup(): lowering path, type checks, and runtime helpers." sidebar: - order: 111 + order: 115 --- ## `filegroup()` — internals @@ -36,6 +36,11 @@ function filegroup(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filegroup()`](../../../php/builtins/filesystem/filegroup.md) diff --git a/docs/internals/builtins/filesystem/fileinode.md b/docs/internals/builtins/filesystem/fileinode.md index c7a99169fb..2ff550531b 100644 --- a/docs/internals/builtins/filesystem/fileinode.md +++ b/docs/internals/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode() — internals" description: "Compiler internals for fileinode(): lowering path, type checks, and runtime helpers." sidebar: - order: 112 + order: 116 --- ## `fileinode()` — internals @@ -36,6 +36,11 @@ function fileinode(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileinode()`](../../../php/builtins/filesystem/fileinode.md) diff --git a/docs/internals/builtins/filesystem/filemtime.md b/docs/internals/builtins/filesystem/filemtime.md index 6a800f621d..4260b064c4 100644 --- a/docs/internals/builtins/filesystem/filemtime.md +++ b/docs/internals/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime() — internals" description: "Compiler internals for filemtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 113 + order: 117 --- ## `filemtime()` — internals @@ -37,6 +37,11 @@ function filemtime(string $filename): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filemtime()`](../../../php/builtins/filesystem/filemtime.md) diff --git a/docs/internals/builtins/filesystem/fileowner.md b/docs/internals/builtins/filesystem/fileowner.md index e2be28445b..3f94be073f 100644 --- a/docs/internals/builtins/filesystem/fileowner.md +++ b/docs/internals/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner() — internals" description: "Compiler internals for fileowner(): lowering path, type checks, and runtime helpers." sidebar: - order: 114 + order: 118 --- ## `fileowner()` — internals @@ -35,6 +35,11 @@ function fileowner(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileowner()`](../../../php/builtins/filesystem/fileowner.md) diff --git a/docs/internals/builtins/filesystem/fileperms.md b/docs/internals/builtins/filesystem/fileperms.md index cf6a95e897..fd3adca837 100644 --- a/docs/internals/builtins/filesystem/fileperms.md +++ b/docs/internals/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms() — internals" description: "Compiler internals for fileperms(): lowering path, type checks, and runtime helpers." sidebar: - order: 115 + order: 119 --- ## `fileperms()` — internals @@ -36,6 +36,11 @@ function fileperms(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileperms()`](../../../php/builtins/filesystem/fileperms.md) diff --git a/docs/internals/builtins/filesystem/filesize.md b/docs/internals/builtins/filesystem/filesize.md index 38060c85ff..5dd3097a50 100644 --- a/docs/internals/builtins/filesystem/filesize.md +++ b/docs/internals/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize() — internals" description: "Compiler internals for filesize(): lowering path, type checks, and runtime helpers." sidebar: - order: 116 + order: 120 --- ## `filesize()` — internals @@ -36,6 +36,11 @@ function filesize(string $filename): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filesize()`](../../../php/builtins/filesystem/filesize.md) diff --git a/docs/internals/builtins/filesystem/filetype.md b/docs/internals/builtins/filesystem/filetype.md index c22edde1f6..6993ae2462 100644 --- a/docs/internals/builtins/filesystem/filetype.md +++ b/docs/internals/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype() — internals" description: "Compiler internals for filetype(): lowering path, type checks, and runtime helpers." sidebar: - order: 117 + order: 121 --- ## `filetype()` — internals @@ -35,6 +35,11 @@ function filetype(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filetype()`](../../../php/builtins/filesystem/filetype.md) diff --git a/docs/internals/builtins/filesystem/fnmatch.md b/docs/internals/builtins/filesystem/fnmatch.md index 1d00eb2c06..211679560f 100644 --- a/docs/internals/builtins/filesystem/fnmatch.md +++ b/docs/internals/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch() — internals" description: "Compiler internals for fnmatch(): lowering path, type checks, and runtime helpers." sidebar: - order: 118 + order: 122 --- ## `fnmatch()` — internals @@ -32,6 +32,11 @@ function fnmatch(string $pattern, string $filename, int $flags = 0): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fnmatch()`](../../../php/builtins/filesystem/fnmatch.md) diff --git a/docs/internals/builtins/filesystem/getcwd.md b/docs/internals/builtins/filesystem/getcwd.md index 9243fb1ab3..0abdebaf32 100644 --- a/docs/internals/builtins/filesystem/getcwd.md +++ b/docs/internals/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd() — internals" description: "Compiler internals for getcwd(): lowering path, type checks, and runtime helpers." sidebar: - order: 119 + order: 123 --- ## `getcwd()` — internals @@ -34,6 +34,11 @@ function getcwd(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getcwd()`](../../../php/builtins/filesystem/getcwd.md) diff --git a/docs/internals/builtins/filesystem/getenv.md b/docs/internals/builtins/filesystem/getenv.md index 0fac463983..7fe7fa1a53 100644 --- a/docs/internals/builtins/filesystem/getenv.md +++ b/docs/internals/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv() — internals" description: "Compiler internals for getenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 120 + order: 124 --- ## `getenv()` — internals @@ -33,6 +33,11 @@ function getenv(string $name): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getenv()`](../../../php/builtins/filesystem/getenv.md) diff --git a/docs/internals/builtins/filesystem/glob.md b/docs/internals/builtins/filesystem/glob.md index f55d70dc80..071847ad65 100644 --- a/docs/internals/builtins/filesystem/glob.md +++ b/docs/internals/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob() — internals" description: "Compiler internals for glob(): lowering path, type checks, and runtime helpers." sidebar: - order: 121 + order: 125 --- ## `glob()` — internals @@ -33,6 +33,11 @@ function glob(string $pattern): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `glob()`](../../../php/builtins/filesystem/glob.md) diff --git a/docs/internals/builtins/filesystem/is_dir.md b/docs/internals/builtins/filesystem/is_dir.md index 8f699adcbb..db5954595b 100644 --- a/docs/internals/builtins/filesystem/is_dir.md +++ b/docs/internals/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir() — internals" description: "Compiler internals for is_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 122 + order: 126 --- ## `is_dir()` — internals @@ -35,6 +35,11 @@ function is_dir(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_dir()`](../../../php/builtins/filesystem/is_dir.md) diff --git a/docs/internals/builtins/filesystem/is_executable.md b/docs/internals/builtins/filesystem/is_executable.md index 9550fdfcb2..dfe9ab3345 100644 --- a/docs/internals/builtins/filesystem/is_executable.md +++ b/docs/internals/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable() — internals" description: "Compiler internals for is_executable(): lowering path, type checks, and runtime helpers." sidebar: - order: 123 + order: 127 --- ## `is_executable()` — internals @@ -36,6 +36,11 @@ function is_executable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_executable()`](../../../php/builtins/filesystem/is_executable.md) diff --git a/docs/internals/builtins/filesystem/is_file.md b/docs/internals/builtins/filesystem/is_file.md index 4b2a35ea45..5a5927cac0 100644 --- a/docs/internals/builtins/filesystem/is_file.md +++ b/docs/internals/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file() — internals" description: "Compiler internals for is_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 124 + order: 128 --- ## `is_file()` — internals @@ -35,6 +35,11 @@ function is_file(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_file()`](../../../php/builtins/filesystem/is_file.md) diff --git a/docs/internals/builtins/filesystem/is_link.md b/docs/internals/builtins/filesystem/is_link.md index 26df330e09..9c6b8a8f28 100644 --- a/docs/internals/builtins/filesystem/is_link.md +++ b/docs/internals/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link() — internals" description: "Compiler internals for is_link(): lowering path, type checks, and runtime helpers." sidebar: - order: 125 + order: 129 --- ## `is_link()` — internals @@ -36,6 +36,11 @@ function is_link(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_link()`](../../../php/builtins/filesystem/is_link.md) diff --git a/docs/internals/builtins/filesystem/is_readable.md b/docs/internals/builtins/filesystem/is_readable.md index 0e61b1965f..5c853dcdc6 100644 --- a/docs/internals/builtins/filesystem/is_readable.md +++ b/docs/internals/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable() — internals" description: "Compiler internals for is_readable(): lowering path, type checks, and runtime helpers." sidebar: - order: 126 + order: 130 --- ## `is_readable()` — internals @@ -35,6 +35,11 @@ function is_readable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_readable()`](../../../php/builtins/filesystem/is_readable.md) diff --git a/docs/internals/builtins/filesystem/is_writable.md b/docs/internals/builtins/filesystem/is_writable.md index f20c80490c..91d8827bea 100644 --- a/docs/internals/builtins/filesystem/is_writable.md +++ b/docs/internals/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable() — internals" description: "Compiler internals for is_writable(): lowering path, type checks, and runtime helpers." sidebar: - order: 127 + order: 131 --- ## `is_writable()` — internals @@ -35,6 +35,11 @@ function is_writable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_writable()`](../../../php/builtins/filesystem/is_writable.md) diff --git a/docs/internals/builtins/filesystem/is_writeable.md b/docs/internals/builtins/filesystem/is_writeable.md index 906ddbb7fe..ebe498e278 100644 --- a/docs/internals/builtins/filesystem/is_writeable.md +++ b/docs/internals/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable() — internals" description: "Compiler internals for is_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 128 + order: 132 --- ## `is_writeable()` — internals @@ -35,6 +35,11 @@ function is_writeable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_writeable()`](../../../php/builtins/filesystem/is_writeable.md) diff --git a/docs/internals/builtins/filesystem/lchgrp.md b/docs/internals/builtins/filesystem/lchgrp.md index 054d954188..4b2e612ac4 100644 --- a/docs/internals/builtins/filesystem/lchgrp.md +++ b/docs/internals/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp() — internals" description: "Compiler internals for lchgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 129 + order: 133 --- ## `lchgrp()` — internals @@ -33,6 +33,11 @@ function lchgrp(string $filename, string $group): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lchgrp()`](../../../php/builtins/filesystem/lchgrp.md) diff --git a/docs/internals/builtins/filesystem/lchown.md b/docs/internals/builtins/filesystem/lchown.md index d4f2cf70d6..47d3cb3ccc 100644 --- a/docs/internals/builtins/filesystem/lchown.md +++ b/docs/internals/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown() — internals" description: "Compiler internals for lchown(): lowering path, type checks, and runtime helpers." sidebar: - order: 130 + order: 134 --- ## `lchown()` — internals @@ -33,6 +33,11 @@ function lchown(string $filename, string $user): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lchown()`](../../../php/builtins/filesystem/lchown.md) diff --git a/docs/internals/builtins/filesystem/link.md b/docs/internals/builtins/filesystem/link.md index 4e2421a4cd..2f8d384ce8 100644 --- a/docs/internals/builtins/filesystem/link.md +++ b/docs/internals/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link() — internals" description: "Compiler internals for link(): lowering path, type checks, and runtime helpers." sidebar: - order: 131 + order: 135 --- ## `link()` — internals @@ -36,6 +36,11 @@ function link(string $target, string $link): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `link()`](../../../php/builtins/filesystem/link.md) diff --git a/docs/internals/builtins/filesystem/linkinfo.md b/docs/internals/builtins/filesystem/linkinfo.md index bec27ff255..10991a9f59 100644 --- a/docs/internals/builtins/filesystem/linkinfo.md +++ b/docs/internals/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo() — internals" description: "Compiler internals for linkinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 132 + order: 136 --- ## `linkinfo()` — internals @@ -36,6 +36,11 @@ function linkinfo(string $path): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `linkinfo()`](../../../php/builtins/filesystem/linkinfo.md) diff --git a/docs/internals/builtins/filesystem/lstat.md b/docs/internals/builtins/filesystem/lstat.md index 1adfae4bb3..f0de434f8e 100644 --- a/docs/internals/builtins/filesystem/lstat.md +++ b/docs/internals/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat() — internals" description: "Compiler internals for lstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 133 + order: 137 --- ## `lstat()` — internals @@ -34,6 +34,11 @@ function lstat(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lstat()`](../../../php/builtins/filesystem/lstat.md) diff --git a/docs/internals/builtins/filesystem/mkdir.md b/docs/internals/builtins/filesystem/mkdir.md index 4bc31dbde1..878c629c2d 100644 --- a/docs/internals/builtins/filesystem/mkdir.md +++ b/docs/internals/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir() — internals" description: "Compiler internals for mkdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 134 + order: 138 --- ## `mkdir()` — internals @@ -37,6 +37,11 @@ function mkdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mkdir()`](../../../php/builtins/filesystem/mkdir.md) diff --git a/docs/internals/builtins/filesystem/pathinfo.md b/docs/internals/builtins/filesystem/pathinfo.md index ebd9883a0c..474456d5fd 100644 --- a/docs/internals/builtins/filesystem/pathinfo.md +++ b/docs/internals/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo() — internals" description: "Compiler internals for pathinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 135 + order: 139 --- ## `pathinfo()` — internals @@ -33,6 +33,11 @@ function pathinfo(string $path, int $flags = 15): array - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pathinfo()`](../../../php/builtins/filesystem/pathinfo.md) diff --git a/docs/internals/builtins/filesystem/putenv.md b/docs/internals/builtins/filesystem/putenv.md index 39db571ebf..a37f2d5d38 100644 --- a/docs/internals/builtins/filesystem/putenv.md +++ b/docs/internals/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv() — internals" description: "Compiler internals for putenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 136 + order: 140 --- ## `putenv()` — internals @@ -33,6 +33,11 @@ function putenv(string $assignment): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `putenv()`](../../../php/builtins/filesystem/putenv.md) diff --git a/docs/internals/builtins/filesystem/readfile.md b/docs/internals/builtins/filesystem/readfile.md index ba9ab3f8b9..e884c81357 100644 --- a/docs/internals/builtins/filesystem/readfile.md +++ b/docs/internals/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile() — internals" description: "Compiler internals for readfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 137 + order: 141 --- ## `readfile()` — internals @@ -32,6 +32,11 @@ function readfile(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readfile()`](../../../php/builtins/filesystem/readfile.md) diff --git a/docs/internals/builtins/filesystem/readlink.md b/docs/internals/builtins/filesystem/readlink.md index 9b67cd14b3..9b6d002b55 100644 --- a/docs/internals/builtins/filesystem/readlink.md +++ b/docs/internals/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink() — internals" description: "Compiler internals for readlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 138 + order: 142 --- ## `readlink()` — internals @@ -36,6 +36,11 @@ function readlink(string $path): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readlink()`](../../../php/builtins/filesystem/readlink.md) diff --git a/docs/internals/builtins/filesystem/realpath.md b/docs/internals/builtins/filesystem/realpath.md index eaee752b33..bd76e2ce45 100644 --- a/docs/internals/builtins/filesystem/realpath.md +++ b/docs/internals/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath() — internals" description: "Compiler internals for realpath(): lowering path, type checks, and runtime helpers." sidebar: - order: 139 + order: 143 --- ## `realpath()` — internals @@ -33,6 +33,11 @@ function realpath(string $path): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath()`](../../../php/builtins/filesystem/realpath.md) diff --git a/docs/internals/builtins/filesystem/realpath_cache_get.md b/docs/internals/builtins/filesystem/realpath_cache_get.md index e32cb410b5..d3a8cac36e 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_get.md +++ b/docs/internals/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get() — internals" description: "Compiler internals for realpath_cache_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 140 + order: 144 --- ## `realpath_cache_get()` — internals @@ -32,6 +32,11 @@ function realpath_cache_get(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath_cache_get()`](../../../php/builtins/filesystem/realpath_cache_get.md) diff --git a/docs/internals/builtins/filesystem/realpath_cache_size.md b/docs/internals/builtins/filesystem/realpath_cache_size.md index 113cb67e63..f4aaca0635 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_size.md +++ b/docs/internals/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size() — internals" description: "Compiler internals for realpath_cache_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 141 + order: 145 --- ## `realpath_cache_size()` — internals @@ -32,6 +32,11 @@ function realpath_cache_size(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath_cache_size()`](../../../php/builtins/filesystem/realpath_cache_size.md) diff --git a/docs/internals/builtins/filesystem/rename.md b/docs/internals/builtins/filesystem/rename.md index 33acbcfc62..8ae0666272 100644 --- a/docs/internals/builtins/filesystem/rename.md +++ b/docs/internals/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename() — internals" description: "Compiler internals for rename(): lowering path, type checks, and runtime helpers." sidebar: - order: 142 + order: 146 --- ## `rename()` — internals @@ -35,6 +35,11 @@ function rename(string $from, string $to): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rename()`](../../../php/builtins/filesystem/rename.md) diff --git a/docs/internals/builtins/filesystem/rmdir.md b/docs/internals/builtins/filesystem/rmdir.md index 6add2d8250..4330c1bbb2 100644 --- a/docs/internals/builtins/filesystem/rmdir.md +++ b/docs/internals/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir() — internals" description: "Compiler internals for rmdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 143 + order: 147 --- ## `rmdir()` — internals @@ -37,6 +37,11 @@ function rmdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rmdir()`](../../../php/builtins/filesystem/rmdir.md) diff --git a/docs/internals/builtins/filesystem/scandir.md b/docs/internals/builtins/filesystem/scandir.md index 1225c0afbe..4b2af053d0 100644 --- a/docs/internals/builtins/filesystem/scandir.md +++ b/docs/internals/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir() — internals" description: "Compiler internals for scandir(): lowering path, type checks, and runtime helpers." sidebar: - order: 144 + order: 148 --- ## `scandir()` — internals @@ -34,6 +34,11 @@ function scandir(string $directory): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `scandir()`](../../../php/builtins/filesystem/scandir.md) diff --git a/docs/internals/builtins/filesystem/stat.md b/docs/internals/builtins/filesystem/stat.md index 0969fc70a2..95eb981237 100644 --- a/docs/internals/builtins/filesystem/stat.md +++ b/docs/internals/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat() — internals" description: "Compiler internals for stat(): lowering path, type checks, and runtime helpers." sidebar: - order: 145 + order: 149 --- ## `stat()` — internals @@ -35,6 +35,11 @@ function stat(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stat()`](../../../php/builtins/filesystem/stat.md) diff --git a/docs/internals/builtins/filesystem/symlink.md b/docs/internals/builtins/filesystem/symlink.md index 4930e27f74..111ded5b96 100644 --- a/docs/internals/builtins/filesystem/symlink.md +++ b/docs/internals/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink() — internals" description: "Compiler internals for symlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 146 + order: 150 --- ## `symlink()` — internals @@ -36,6 +36,11 @@ function symlink(string $target, string $link): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `symlink()`](../../../php/builtins/filesystem/symlink.md) diff --git a/docs/internals/builtins/filesystem/sys_get_temp_dir.md b/docs/internals/builtins/filesystem/sys_get_temp_dir.md index 7c45622094..e3529230de 100644 --- a/docs/internals/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/internals/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir() — internals" description: "Compiler internals for sys_get_temp_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 147 + order: 151 --- ## `sys_get_temp_dir()` — internals @@ -33,6 +33,11 @@ function sys_get_temp_dir(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sys_get_temp_dir()`](../../../php/builtins/filesystem/sys_get_temp_dir.md) diff --git a/docs/internals/builtins/filesystem/tempnam.md b/docs/internals/builtins/filesystem/tempnam.md index d1de170ba3..026d0887df 100644 --- a/docs/internals/builtins/filesystem/tempnam.md +++ b/docs/internals/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam() — internals" description: "Compiler internals for tempnam(): lowering path, type checks, and runtime helpers." sidebar: - order: 148 + order: 152 --- ## `tempnam()` — internals @@ -35,6 +35,11 @@ function tempnam(string $directory, string $prefix): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tempnam()`](../../../php/builtins/filesystem/tempnam.md) diff --git a/docs/internals/builtins/filesystem/tmpfile.md b/docs/internals/builtins/filesystem/tmpfile.md index 1c72e490e0..b9be0d96e1 100644 --- a/docs/internals/builtins/filesystem/tmpfile.md +++ b/docs/internals/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile() — internals" description: "Compiler internals for tmpfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 149 + order: 153 --- ## `tmpfile()` — internals @@ -35,6 +35,11 @@ function tmpfile(): mixed - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tmpfile()`](../../../php/builtins/filesystem/tmpfile.md) diff --git a/docs/internals/builtins/filesystem/touch.md b/docs/internals/builtins/filesystem/touch.md index 9a4ebb029c..8c9f885b78 100644 --- a/docs/internals/builtins/filesystem/touch.md +++ b/docs/internals/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch() — internals" description: "Compiler internals for touch(): lowering path, type checks, and runtime helpers." sidebar: - order: 150 + order: 154 --- ## `touch()` — internals @@ -32,6 +32,11 @@ function touch(string $filename, int $mtime = null, int $atime = null): bool - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `touch()`](../../../php/builtins/filesystem/touch.md) diff --git a/docs/internals/builtins/filesystem/umask.md b/docs/internals/builtins/filesystem/umask.md index 50b45bae18..0033851868 100644 --- a/docs/internals/builtins/filesystem/umask.md +++ b/docs/internals/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask() — internals" description: "Compiler internals for umask(): lowering path, type checks, and runtime helpers." sidebar: - order: 151 + order: 155 --- ## `umask()` — internals @@ -33,6 +33,11 @@ function umask(int $mask = null): int - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `umask()`](../../../php/builtins/filesystem/umask.md) diff --git a/docs/internals/builtins/filesystem/unlink.md b/docs/internals/builtins/filesystem/unlink.md index fb9df6ff4a..c00b70e08c 100644 --- a/docs/internals/builtins/filesystem/unlink.md +++ b/docs/internals/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink() — internals" description: "Compiler internals for unlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 152 + order: 156 --- ## `unlink()` — internals @@ -35,6 +35,11 @@ function unlink(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `unlink()`](../../../php/builtins/filesystem/unlink.md) diff --git a/docs/internals/builtins/io/closedir.md b/docs/internals/builtins/io/closedir.md index 4a8ad88cc8..c8c38c0a21 100644 --- a/docs/internals/builtins/io/closedir.md +++ b/docs/internals/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir() — internals" description: "Compiler internals for closedir(): lowering path, type checks, and runtime helpers." sidebar: - order: 153 + order: 157 --- ## `closedir()` — internals @@ -36,6 +36,11 @@ function closedir(resource $dir_handle): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `closedir()`](../../../php/builtins/io/closedir.md) diff --git a/docs/internals/builtins/io/fclose.md b/docs/internals/builtins/io/fclose.md index 07d61b2ec2..ffea17f3f1 100644 --- a/docs/internals/builtins/io/fclose.md +++ b/docs/internals/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose() — internals" description: "Compiler internals for fclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 154 + order: 158 --- ## `fclose()` — internals @@ -32,6 +32,11 @@ function fclose(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fclose()`](../../../php/builtins/io/fclose.md) diff --git a/docs/internals/builtins/io/fdatasync.md b/docs/internals/builtins/io/fdatasync.md index 43ad181d29..9d1b9ef3a5 100644 --- a/docs/internals/builtins/io/fdatasync.md +++ b/docs/internals/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync() — internals" description: "Compiler internals for fdatasync(): lowering path, type checks, and runtime helpers." sidebar: - order: 155 + order: 159 --- ## `fdatasync()` — internals @@ -33,6 +33,11 @@ function fdatasync(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fdatasync()`](../../../php/builtins/io/fdatasync.md) diff --git a/docs/internals/builtins/io/feof.md b/docs/internals/builtins/io/feof.md index e24c273b01..749a1bddec 100644 --- a/docs/internals/builtins/io/feof.md +++ b/docs/internals/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof() — internals" description: "Compiler internals for feof(): lowering path, type checks, and runtime helpers." sidebar: - order: 156 + order: 160 --- ## `feof()` — internals @@ -34,6 +34,11 @@ function feof(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `feof()`](../../../php/builtins/io/feof.md) diff --git a/docs/internals/builtins/io/fflush.md b/docs/internals/builtins/io/fflush.md index 2fc2d1aade..b285709dc4 100644 --- a/docs/internals/builtins/io/fflush.md +++ b/docs/internals/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush() — internals" description: "Compiler internals for fflush(): lowering path, type checks, and runtime helpers." sidebar: - order: 157 + order: 161 --- ## `fflush()` — internals @@ -33,6 +33,11 @@ function fflush(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fflush()`](../../../php/builtins/io/fflush.md) diff --git a/docs/internals/builtins/io/fgetc.md b/docs/internals/builtins/io/fgetc.md index 8a79b3f773..b7ae4fb08d 100644 --- a/docs/internals/builtins/io/fgetc.md +++ b/docs/internals/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc() — internals" description: "Compiler internals for fgetc(): lowering path, type checks, and runtime helpers." sidebar: - order: 158 + order: 162 --- ## `fgetc()` — internals @@ -34,6 +34,11 @@ function fgetc(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgetc()`](../../../php/builtins/io/fgetc.md) diff --git a/docs/internals/builtins/io/fgetcsv.md b/docs/internals/builtins/io/fgetcsv.md index 9e7afe5ddd..04f431b967 100644 --- a/docs/internals/builtins/io/fgetcsv.md +++ b/docs/internals/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv() — internals" description: "Compiler internals for fgetcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 159 + order: 163 --- ## `fgetcsv()` — internals @@ -34,6 +34,11 @@ function fgetcsv(resource $stream, int $length = null, string $separator = ','): - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgetcsv()`](../../../php/builtins/io/fgetcsv.md) diff --git a/docs/internals/builtins/io/fgets.md b/docs/internals/builtins/io/fgets.md index b87a1aeb18..4054d88afd 100644 --- a/docs/internals/builtins/io/fgets.md +++ b/docs/internals/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets() — internals" description: "Compiler internals for fgets(): lowering path, type checks, and runtime helpers." sidebar: - order: 160 + order: 164 --- ## `fgets()` — internals @@ -34,6 +34,11 @@ function fgets(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgets()`](../../../php/builtins/io/fgets.md) diff --git a/docs/internals/builtins/io/file.md b/docs/internals/builtins/io/file.md index 53664951e9..21540ef944 100644 --- a/docs/internals/builtins/io/file.md +++ b/docs/internals/builtins/io/file.md @@ -2,7 +2,7 @@ title: "file() — internals" description: "Compiler internals for file(): lowering path, type checks, and runtime helpers." sidebar: - order: 161 + order: 165 --- ## `file()` — internals @@ -34,6 +34,11 @@ function file(string $filename): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file()`](../../../php/builtins/io/file.md) diff --git a/docs/internals/builtins/io/file_get_contents.md b/docs/internals/builtins/io/file_get_contents.md index 341051a308..fdda2c790d 100644 --- a/docs/internals/builtins/io/file_get_contents.md +++ b/docs/internals/builtins/io/file_get_contents.md @@ -2,7 +2,7 @@ title: "file_get_contents() — internals" description: "Compiler internals for file_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 162 + order: 166 --- ## `file_get_contents()` — internals @@ -34,6 +34,11 @@ function file_get_contents(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_get_contents()`](../../../php/builtins/io/file_get_contents.md) diff --git a/docs/internals/builtins/io/file_put_contents.md b/docs/internals/builtins/io/file_put_contents.md index a0dd17d11b..e4b5ada072 100644 --- a/docs/internals/builtins/io/file_put_contents.md +++ b/docs/internals/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents() — internals" description: "Compiler internals for file_put_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 163 + order: 167 --- ## `file_put_contents()` — internals @@ -34,6 +34,11 @@ function file_put_contents(string $filename, string $data): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_put_contents()`](../../../php/builtins/io/file_put_contents.md) diff --git a/docs/internals/builtins/io/flock.md b/docs/internals/builtins/io/flock.md index 05e1b70504..6a2bba850d 100644 --- a/docs/internals/builtins/io/flock.md +++ b/docs/internals/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock() — internals" description: "Compiler internals for flock(): lowering path, type checks, and runtime helpers." sidebar: - order: 164 + order: 168 --- ## `flock()` — internals @@ -33,6 +33,12 @@ function flock(resource $stream, int $operation, bool $would_block = null): bool - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$would_block`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$would_block`. + ## Cross-references - [User reference for `flock()`](../../../php/builtins/io/flock.md) diff --git a/docs/internals/builtins/io/fopen.md b/docs/internals/builtins/io/fopen.md index f4af5776b2..ffd6469183 100644 --- a/docs/internals/builtins/io/fopen.md +++ b/docs/internals/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen() — internals" description: "Compiler internals for fopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 165 + order: 169 --- ## `fopen()` — internals @@ -33,6 +33,11 @@ function fopen(string $filename, string $mode, bool $use_include_path = false, m - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fopen()`](../../../php/builtins/io/fopen.md) diff --git a/docs/internals/builtins/io/fpassthru.md b/docs/internals/builtins/io/fpassthru.md index fc5dd43810..a934e5af61 100644 --- a/docs/internals/builtins/io/fpassthru.md +++ b/docs/internals/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru() — internals" description: "Compiler internals for fpassthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 166 + order: 170 --- ## `fpassthru()` — internals @@ -34,6 +34,11 @@ function fpassthru(resource $stream): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fpassthru()`](../../../php/builtins/io/fpassthru.md) diff --git a/docs/internals/builtins/io/fprintf.md b/docs/internals/builtins/io/fprintf.md index 25c68e9a11..d274ce9be5 100644 --- a/docs/internals/builtins/io/fprintf.md +++ b/docs/internals/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf() — internals" description: "Compiler internals for fprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 167 + order: 171 --- ## `fprintf()` — internals @@ -34,6 +34,12 @@ function fprintf(resource $stream, string $format, ...$values): int - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `fprintf()`](../../../php/builtins/io/fprintf.md) diff --git a/docs/internals/builtins/io/fputcsv.md b/docs/internals/builtins/io/fputcsv.md index 88071b7e26..eee759a64d 100644 --- a/docs/internals/builtins/io/fputcsv.md +++ b/docs/internals/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv() — internals" description: "Compiler internals for fputcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 168 + order: 172 --- ## `fputcsv()` — internals @@ -33,6 +33,11 @@ function fputcsv(resource $stream, array $fields, string $separator = ',', strin - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fputcsv()`](../../../php/builtins/io/fputcsv.md) diff --git a/docs/internals/builtins/io/fread.md b/docs/internals/builtins/io/fread.md index 3718fd98c9..636cc0473c 100644 --- a/docs/internals/builtins/io/fread.md +++ b/docs/internals/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread() — internals" description: "Compiler internals for fread(): lowering path, type checks, and runtime helpers." sidebar: - order: 169 + order: 173 --- ## `fread()` — internals @@ -33,6 +33,11 @@ function fread(resource $stream, int $length): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fread()`](../../../php/builtins/io/fread.md) diff --git a/docs/internals/builtins/io/fscanf.md b/docs/internals/builtins/io/fscanf.md index ac174d4b98..e007f9917e 100644 --- a/docs/internals/builtins/io/fscanf.md +++ b/docs/internals/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf() — internals" description: "Compiler internals for fscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 170 + order: 174 --- ## `fscanf()` — internals @@ -35,6 +35,12 @@ function fscanf(resource $stream, string $format, ...$vars): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `fscanf()`](../../../php/builtins/io/fscanf.md) diff --git a/docs/internals/builtins/io/fseek.md b/docs/internals/builtins/io/fseek.md index ce92f7c77f..84820d15ef 100644 --- a/docs/internals/builtins/io/fseek.md +++ b/docs/internals/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek() — internals" description: "Compiler internals for fseek(): lowering path, type checks, and runtime helpers." sidebar: - order: 171 + order: 175 --- ## `fseek()` — internals @@ -32,6 +32,11 @@ function fseek(resource $stream, int $offset, int $whence = 0): int - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fseek()`](../../../php/builtins/io/fseek.md) diff --git a/docs/internals/builtins/io/fstat.md b/docs/internals/builtins/io/fstat.md index b92d1d8b31..057c1308ea 100644 --- a/docs/internals/builtins/io/fstat.md +++ b/docs/internals/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat() — internals" description: "Compiler internals for fstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 172 + order: 176 --- ## `fstat()` — internals @@ -33,6 +33,11 @@ function fstat(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fstat()`](../../../php/builtins/io/fstat.md) diff --git a/docs/internals/builtins/io/fsync.md b/docs/internals/builtins/io/fsync.md index 5338aab7cd..198a97f9f6 100644 --- a/docs/internals/builtins/io/fsync.md +++ b/docs/internals/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync() — internals" description: "Compiler internals for fsync(): lowering path, type checks, and runtime helpers." sidebar: - order: 173 + order: 177 --- ## `fsync()` — internals @@ -34,6 +34,11 @@ function fsync(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fsync()`](../../../php/builtins/io/fsync.md) diff --git a/docs/internals/builtins/io/ftell.md b/docs/internals/builtins/io/ftell.md index fce93e4672..823c765304 100644 --- a/docs/internals/builtins/io/ftell.md +++ b/docs/internals/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell() — internals" description: "Compiler internals for ftell(): lowering path, type checks, and runtime helpers." sidebar: - order: 174 + order: 178 --- ## `ftell()` — internals @@ -33,6 +33,11 @@ function ftell(resource $stream): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ftell()`](../../../php/builtins/io/ftell.md) diff --git a/docs/internals/builtins/io/ftruncate.md b/docs/internals/builtins/io/ftruncate.md index 8a71ce2a37..d0470fe9c9 100644 --- a/docs/internals/builtins/io/ftruncate.md +++ b/docs/internals/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate() — internals" description: "Compiler internals for ftruncate(): lowering path, type checks, and runtime helpers." sidebar: - order: 175 + order: 179 --- ## `ftruncate()` — internals @@ -32,6 +32,11 @@ function ftruncate(resource $stream, int $size): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ftruncate()`](../../../php/builtins/io/ftruncate.md) diff --git a/docs/internals/builtins/io/fwrite.md b/docs/internals/builtins/io/fwrite.md index 85551c288d..15c5636223 100644 --- a/docs/internals/builtins/io/fwrite.md +++ b/docs/internals/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite() — internals" description: "Compiler internals for fwrite(): lowering path, type checks, and runtime helpers." sidebar: - order: 176 + order: 180 --- ## `fwrite()` — internals @@ -33,6 +33,11 @@ function fwrite(resource $stream, string $data): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fwrite()`](../../../php/builtins/io/fwrite.md) diff --git a/docs/internals/builtins/io/gethostbyaddr.md b/docs/internals/builtins/io/gethostbyaddr.md index 49ad6a17cc..bec60bfc7e 100644 --- a/docs/internals/builtins/io/gethostbyaddr.md +++ b/docs/internals/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr() — internals" description: "Compiler internals for gethostbyaddr(): lowering path, type checks, and runtime helpers." sidebar: - order: 177 + order: 181 --- ## `gethostbyaddr()` — internals @@ -34,6 +34,11 @@ function gethostbyaddr(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostbyaddr()`](../../../php/builtins/io/gethostbyaddr.md) diff --git a/docs/internals/builtins/io/gethostbyname.md b/docs/internals/builtins/io/gethostbyname.md index 74394b4e6b..3afafd75bd 100644 --- a/docs/internals/builtins/io/gethostbyname.md +++ b/docs/internals/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname() — internals" description: "Compiler internals for gethostbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 178 + order: 182 --- ## `gethostbyname()` — internals @@ -34,6 +34,11 @@ function gethostbyname(string $hostname): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostbyname()`](../../../php/builtins/io/gethostbyname.md) diff --git a/docs/internals/builtins/io/gethostname.md b/docs/internals/builtins/io/gethostname.md index f5dbcabf0e..c5240428d5 100644 --- a/docs/internals/builtins/io/gethostname.md +++ b/docs/internals/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname() — internals" description: "Compiler internals for gethostname(): lowering path, type checks, and runtime helpers." sidebar: - order: 179 + order: 183 --- ## `gethostname()` — internals @@ -35,6 +35,11 @@ function gethostname(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostname()`](../../../php/builtins/io/gethostname.md) diff --git a/docs/internals/builtins/io/getprotobyname.md b/docs/internals/builtins/io/getprotobyname.md index 85c99a16db..9831de7985 100644 --- a/docs/internals/builtins/io/getprotobyname.md +++ b/docs/internals/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname() — internals" description: "Compiler internals for getprotobyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 180 + order: 184 --- ## `getprotobyname()` — internals @@ -33,6 +33,11 @@ function getprotobyname(string $protocol): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getprotobyname()`](../../../php/builtins/io/getprotobyname.md) diff --git a/docs/internals/builtins/io/getprotobynumber.md b/docs/internals/builtins/io/getprotobynumber.md index f2603bff3f..7b6e21e93a 100644 --- a/docs/internals/builtins/io/getprotobynumber.md +++ b/docs/internals/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber() — internals" description: "Compiler internals for getprotobynumber(): lowering path, type checks, and runtime helpers." sidebar: - order: 181 + order: 185 --- ## `getprotobynumber()` — internals @@ -33,6 +33,11 @@ function getprotobynumber(int $protocol): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getprotobynumber()`](../../../php/builtins/io/getprotobynumber.md) diff --git a/docs/internals/builtins/io/getservbyname.md b/docs/internals/builtins/io/getservbyname.md index cf829bb720..a04bcfa61e 100644 --- a/docs/internals/builtins/io/getservbyname.md +++ b/docs/internals/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname() — internals" description: "Compiler internals for getservbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 182 + order: 186 --- ## `getservbyname()` — internals @@ -33,6 +33,11 @@ function getservbyname(string $service, string $protocol): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getservbyname()`](../../../php/builtins/io/getservbyname.md) diff --git a/docs/internals/builtins/io/getservbyport.md b/docs/internals/builtins/io/getservbyport.md index 1e37fd6dda..dd226c9dac 100644 --- a/docs/internals/builtins/io/getservbyport.md +++ b/docs/internals/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport() — internals" description: "Compiler internals for getservbyport(): lowering path, type checks, and runtime helpers." sidebar: - order: 183 + order: 187 --- ## `getservbyport()` — internals @@ -33,6 +33,11 @@ function getservbyport(int $port, string $protocol): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getservbyport()`](../../../php/builtins/io/getservbyport.md) diff --git a/docs/internals/builtins/io/hash_file.md b/docs/internals/builtins/io/hash_file.md index 7c594feab1..61dc4fcf02 100644 --- a/docs/internals/builtins/io/hash_file.md +++ b/docs/internals/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file() — internals" description: "Compiler internals for hash_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 184 + order: 188 --- ## `hash_file()` — internals @@ -32,6 +32,11 @@ function hash_file(string $algo, string $filename, bool $binary = false): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_file()`](../../../php/builtins/io/hash_file.md) diff --git a/docs/internals/builtins/io/opendir.md b/docs/internals/builtins/io/opendir.md index 26d9d4b37e..ae1f14ed3e 100644 --- a/docs/internals/builtins/io/opendir.md +++ b/docs/internals/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir() — internals" description: "Compiler internals for opendir(): lowering path, type checks, and runtime helpers." sidebar: - order: 185 + order: 189 --- ## `opendir()` — internals @@ -35,6 +35,11 @@ function opendir(string $directory): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `opendir()`](../../../php/builtins/io/opendir.md) diff --git a/docs/internals/builtins/io/readdir.md b/docs/internals/builtins/io/readdir.md index b6c1f0da7e..df501cd2bc 100644 --- a/docs/internals/builtins/io/readdir.md +++ b/docs/internals/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir() — internals" description: "Compiler internals for readdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 186 + order: 190 --- ## `readdir()` — internals @@ -36,6 +36,11 @@ function readdir(resource $dir_handle): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readdir()`](../../../php/builtins/io/readdir.md) diff --git a/docs/internals/builtins/io/rewind.md b/docs/internals/builtins/io/rewind.md index 6f63dbcf51..3a4aec0a8b 100644 --- a/docs/internals/builtins/io/rewind.md +++ b/docs/internals/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind() — internals" description: "Compiler internals for rewind(): lowering path, type checks, and runtime helpers." sidebar: - order: 187 + order: 191 --- ## `rewind()` — internals @@ -32,6 +32,11 @@ function rewind(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rewind()`](../../../php/builtins/io/rewind.md) diff --git a/docs/internals/builtins/io/rewinddir.md b/docs/internals/builtins/io/rewinddir.md index 185059fde0..bae6cdb840 100644 --- a/docs/internals/builtins/io/rewinddir.md +++ b/docs/internals/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir() — internals" description: "Compiler internals for rewinddir(): lowering path, type checks, and runtime helpers." sidebar: - order: 188 + order: 192 --- ## `rewinddir()` — internals @@ -34,6 +34,11 @@ function rewinddir(resource $dir_handle): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rewinddir()`](../../../php/builtins/io/rewinddir.md) diff --git a/docs/internals/builtins/io/stream_bucket_make_writeable.md b/docs/internals/builtins/io/stream_bucket_make_writeable.md index 6bb8d17674..f1db9bc0b6 100644 --- a/docs/internals/builtins/io/stream_bucket_make_writeable.md +++ b/docs/internals/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable() — internals" description: "Compiler internals for stream_bucket_make_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 189 + order: 193 --- ## `stream_bucket_make_writeable()` — internals @@ -33,6 +33,11 @@ function stream_bucket_make_writeable(mixed $brigade): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_make_writeable()`](../../../php/builtins/io/stream_bucket_make_writeable.md) diff --git a/docs/internals/builtins/io/stream_bucket_new.md b/docs/internals/builtins/io/stream_bucket_new.md index 568382c658..86e3fee2bf 100644 --- a/docs/internals/builtins/io/stream_bucket_new.md +++ b/docs/internals/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new() — internals" description: "Compiler internals for stream_bucket_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 190 + order: 194 --- ## `stream_bucket_new()` — internals @@ -32,6 +32,11 @@ function stream_bucket_new(resource $stream, string $buffer): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_new()`](../../../php/builtins/io/stream_bucket_new.md) diff --git a/docs/internals/builtins/io/stream_context_create.md b/docs/internals/builtins/io/stream_context_create.md index 294aed3021..0c095976bf 100644 --- a/docs/internals/builtins/io/stream_context_create.md +++ b/docs/internals/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create() — internals" description: "Compiler internals for stream_context_create(): lowering path, type checks, and runtime helpers." sidebar: - order: 191 + order: 195 --- ## `stream_context_create()` — internals @@ -32,6 +32,11 @@ function stream_context_create(array $options = null, array $params = null): mix - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_create()`](../../../php/builtins/io/stream_context_create.md) diff --git a/docs/internals/builtins/io/stream_context_get_default.md b/docs/internals/builtins/io/stream_context_get_default.md index e8fa0170ef..196df3ec9a 100644 --- a/docs/internals/builtins/io/stream_context_get_default.md +++ b/docs/internals/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default() — internals" description: "Compiler internals for stream_context_get_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 192 + order: 196 --- ## `stream_context_get_default()` — internals @@ -32,6 +32,11 @@ function stream_context_get_default(array $options = null): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_default()`](../../../php/builtins/io/stream_context_get_default.md) diff --git a/docs/internals/builtins/io/stream_context_get_options.md b/docs/internals/builtins/io/stream_context_get_options.md index 1bc88ab6fc..9451ec5a62 100644 --- a/docs/internals/builtins/io/stream_context_get_options.md +++ b/docs/internals/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options() — internals" description: "Compiler internals for stream_context_get_options(): lowering path, type checks, and runtime helpers." sidebar: - order: 193 + order: 197 --- ## `stream_context_get_options()` — internals @@ -34,6 +34,11 @@ function stream_context_get_options(resource $context): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_options()`](../../../php/builtins/io/stream_context_get_options.md) diff --git a/docs/internals/builtins/io/stream_context_get_params.md b/docs/internals/builtins/io/stream_context_get_params.md index a336ce53ac..d70a703acb 100644 --- a/docs/internals/builtins/io/stream_context_get_params.md +++ b/docs/internals/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params() — internals" description: "Compiler internals for stream_context_get_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 194 + order: 198 --- ## `stream_context_get_params()` — internals @@ -32,6 +32,11 @@ function stream_context_get_params(resource $context): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_params()`](../../../php/builtins/io/stream_context_get_params.md) diff --git a/docs/internals/builtins/io/stream_context_set_default.md b/docs/internals/builtins/io/stream_context_set_default.md index 11bc262f4a..d8fdcc977a 100644 --- a/docs/internals/builtins/io/stream_context_set_default.md +++ b/docs/internals/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default() — internals" description: "Compiler internals for stream_context_set_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 195 + order: 199 --- ## `stream_context_set_default()` — internals @@ -32,6 +32,11 @@ function stream_context_set_default(array $options): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_default()`](../../../php/builtins/io/stream_context_set_default.md) diff --git a/docs/internals/builtins/io/stream_context_set_option.md b/docs/internals/builtins/io/stream_context_set_option.md index fc81a0beaf..68c8aa94b5 100644 --- a/docs/internals/builtins/io/stream_context_set_option.md +++ b/docs/internals/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option() — internals" description: "Compiler internals for stream_context_set_option(): lowering path, type checks, and runtime helpers." sidebar: - order: 196 + order: 200 --- ## `stream_context_set_option()` — internals @@ -32,6 +32,11 @@ function stream_context_set_option(resource $context, string $wrapper_or_options - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_option()`](../../../php/builtins/io/stream_context_set_option.md) diff --git a/docs/internals/builtins/io/stream_context_set_params.md b/docs/internals/builtins/io/stream_context_set_params.md index adacd430ba..20a881fa4b 100644 --- a/docs/internals/builtins/io/stream_context_set_params.md +++ b/docs/internals/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params() — internals" description: "Compiler internals for stream_context_set_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 197 + order: 201 --- ## `stream_context_set_params()` — internals @@ -32,6 +32,11 @@ function stream_context_set_params(resource $context, array $params): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_params()`](../../../php/builtins/io/stream_context_set_params.md) diff --git a/docs/internals/builtins/io/stream_copy_to_stream.md b/docs/internals/builtins/io/stream_copy_to_stream.md index c33e7ed443..3e62a542a3 100644 --- a/docs/internals/builtins/io/stream_copy_to_stream.md +++ b/docs/internals/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream() — internals" description: "Compiler internals for stream_copy_to_stream(): lowering path, type checks, and runtime helpers." sidebar: - order: 198 + order: 202 --- ## `stream_copy_to_stream()` — internals @@ -32,6 +32,11 @@ function stream_copy_to_stream(resource $from, resource $to, int $length = null, - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_copy_to_stream()`](../../../php/builtins/io/stream_copy_to_stream.md) diff --git a/docs/internals/builtins/io/stream_filter_register.md b/docs/internals/builtins/io/stream_filter_register.md index 4871ac00d3..3dbfd60fd0 100644 --- a/docs/internals/builtins/io/stream_filter_register.md +++ b/docs/internals/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register() — internals" description: "Compiler internals for stream_filter_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 199 + order: 203 --- ## `stream_filter_register()` — internals @@ -33,6 +33,11 @@ function stream_filter_register(string $filter_name, string $class): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_register()`](../../../php/builtins/io/stream_filter_register.md) diff --git a/docs/internals/builtins/io/stream_filter_remove.md b/docs/internals/builtins/io/stream_filter_remove.md index 4259c6188a..f3b85782d6 100644 --- a/docs/internals/builtins/io/stream_filter_remove.md +++ b/docs/internals/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove() — internals" description: "Compiler internals for stream_filter_remove(): lowering path, type checks, and runtime helpers." sidebar: - order: 200 + order: 204 --- ## `stream_filter_remove()` — internals @@ -33,6 +33,11 @@ function stream_filter_remove(resource $stream_filter): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_remove()`](../../../php/builtins/io/stream_filter_remove.md) diff --git a/docs/internals/builtins/io/stream_get_contents.md b/docs/internals/builtins/io/stream_get_contents.md index 2354839a01..0a67c802bb 100644 --- a/docs/internals/builtins/io/stream_get_contents.md +++ b/docs/internals/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents() — internals" description: "Compiler internals for stream_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 201 + order: 205 --- ## `stream_get_contents()` — internals @@ -32,6 +32,11 @@ function stream_get_contents(resource $stream, int $length = null, int $offset = - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_contents()`](../../../php/builtins/io/stream_get_contents.md) diff --git a/docs/internals/builtins/io/stream_get_filters.md b/docs/internals/builtins/io/stream_get_filters.md index eac2d3dabc..b471fff4e5 100644 --- a/docs/internals/builtins/io/stream_get_filters.md +++ b/docs/internals/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters() — internals" description: "Compiler internals for stream_get_filters(): lowering path, type checks, and runtime helpers." sidebar: - order: 202 + order: 206 --- ## `stream_get_filters()` — internals @@ -32,6 +32,11 @@ function stream_get_filters(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_filters()`](../../../php/builtins/io/stream_get_filters.md) diff --git a/docs/internals/builtins/io/stream_get_line.md b/docs/internals/builtins/io/stream_get_line.md index 52b01cbfb2..bff83ff822 100644 --- a/docs/internals/builtins/io/stream_get_line.md +++ b/docs/internals/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line() — internals" description: "Compiler internals for stream_get_line(): lowering path, type checks, and runtime helpers." sidebar: - order: 203 + order: 207 --- ## `stream_get_line()` — internals @@ -32,6 +32,11 @@ function stream_get_line(resource $stream, int $length, string $ending = ''): st - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_line()`](../../../php/builtins/io/stream_get_line.md) diff --git a/docs/internals/builtins/io/stream_get_meta_data.md b/docs/internals/builtins/io/stream_get_meta_data.md index c22e797cce..1559741cc5 100644 --- a/docs/internals/builtins/io/stream_get_meta_data.md +++ b/docs/internals/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data() — internals" description: "Compiler internals for stream_get_meta_data(): lowering path, type checks, and runtime helpers." sidebar: - order: 204 + order: 208 --- ## `stream_get_meta_data()` — internals @@ -33,6 +33,11 @@ function stream_get_meta_data(resource $stream): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_meta_data()`](../../../php/builtins/io/stream_get_meta_data.md) diff --git a/docs/internals/builtins/io/stream_get_transports.md b/docs/internals/builtins/io/stream_get_transports.md index 8c08491264..862c7266ed 100644 --- a/docs/internals/builtins/io/stream_get_transports.md +++ b/docs/internals/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports() — internals" description: "Compiler internals for stream_get_transports(): lowering path, type checks, and runtime helpers." sidebar: - order: 205 + order: 209 --- ## `stream_get_transports()` — internals @@ -32,6 +32,11 @@ function stream_get_transports(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_transports()`](../../../php/builtins/io/stream_get_transports.md) diff --git a/docs/internals/builtins/io/stream_get_wrappers.md b/docs/internals/builtins/io/stream_get_wrappers.md index f83d658932..2b47a190a1 100644 --- a/docs/internals/builtins/io/stream_get_wrappers.md +++ b/docs/internals/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers() — internals" description: "Compiler internals for stream_get_wrappers(): lowering path, type checks, and runtime helpers." sidebar: - order: 206 + order: 210 --- ## `stream_get_wrappers()` — internals @@ -32,6 +32,11 @@ function stream_get_wrappers(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_wrappers()`](../../../php/builtins/io/stream_get_wrappers.md) diff --git a/docs/internals/builtins/io/stream_is_local.md b/docs/internals/builtins/io/stream_is_local.md index 2a28e1703b..29135e7615 100644 --- a/docs/internals/builtins/io/stream_is_local.md +++ b/docs/internals/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local() — internals" description: "Compiler internals for stream_is_local(): lowering path, type checks, and runtime helpers." sidebar: - order: 207 + order: 211 --- ## `stream_is_local()` — internals @@ -32,6 +32,11 @@ function stream_is_local(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_is_local()`](../../../php/builtins/io/stream_is_local.md) diff --git a/docs/internals/builtins/io/stream_isatty.md b/docs/internals/builtins/io/stream_isatty.md index a5ee682754..1c5dc63f8b 100644 --- a/docs/internals/builtins/io/stream_isatty.md +++ b/docs/internals/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty() — internals" description: "Compiler internals for stream_isatty(): lowering path, type checks, and runtime helpers." sidebar: - order: 208 + order: 212 --- ## `stream_isatty()` — internals @@ -33,6 +33,11 @@ function stream_isatty(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_isatty()`](../../../php/builtins/io/stream_isatty.md) diff --git a/docs/internals/builtins/io/stream_resolve_include_path.md b/docs/internals/builtins/io/stream_resolve_include_path.md index 12122e031c..4cd73d4560 100644 --- a/docs/internals/builtins/io/stream_resolve_include_path.md +++ b/docs/internals/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path() — internals" description: "Compiler internals for stream_resolve_include_path(): lowering path, type checks, and runtime helpers." sidebar: - order: 209 + order: 213 --- ## `stream_resolve_include_path()` — internals @@ -34,6 +34,11 @@ function stream_resolve_include_path(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_resolve_include_path()`](../../../php/builtins/io/stream_resolve_include_path.md) diff --git a/docs/internals/builtins/io/stream_select.md b/docs/internals/builtins/io/stream_select.md index d5c0fc7e99..67037055ba 100644 --- a/docs/internals/builtins/io/stream_select.md +++ b/docs/internals/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select() — internals" description: "Compiler internals for stream_select(): lowering path, type checks, and runtime helpers." sidebar: - order: 210 + order: 214 --- ## `stream_select()` — internals @@ -33,6 +33,12 @@ function stream_select(array $read, array $write, array $except, int $seconds, i - **Arity**: takes 4–5 arguments (1 optional). - **By-reference parameters**: `$read`, `$write`, `$except`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$read`, `$write`, `$except`. + ## Cross-references - [User reference for `stream_select()`](../../../php/builtins/io/stream_select.md) diff --git a/docs/internals/builtins/io/stream_set_blocking.md b/docs/internals/builtins/io/stream_set_blocking.md index c2971c593a..ece06ff8e3 100644 --- a/docs/internals/builtins/io/stream_set_blocking.md +++ b/docs/internals/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking() — internals" description: "Compiler internals for stream_set_blocking(): lowering path, type checks, and runtime helpers." sidebar: - order: 211 + order: 215 --- ## `stream_set_blocking()` — internals @@ -34,6 +34,11 @@ function stream_set_blocking(resource $stream, bool $enable): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_blocking()`](../../../php/builtins/io/stream_set_blocking.md) diff --git a/docs/internals/builtins/io/stream_set_chunk_size.md b/docs/internals/builtins/io/stream_set_chunk_size.md index 3ebdaabd13..d380eae7c5 100644 --- a/docs/internals/builtins/io/stream_set_chunk_size.md +++ b/docs/internals/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size() — internals" description: "Compiler internals for stream_set_chunk_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 212 + order: 216 --- ## `stream_set_chunk_size()` — internals @@ -32,6 +32,11 @@ function stream_set_chunk_size(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_chunk_size()`](../../../php/builtins/io/stream_set_chunk_size.md) diff --git a/docs/internals/builtins/io/stream_set_read_buffer.md b/docs/internals/builtins/io/stream_set_read_buffer.md index e955cf8ecc..31953846a3 100644 --- a/docs/internals/builtins/io/stream_set_read_buffer.md +++ b/docs/internals/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer() — internals" description: "Compiler internals for stream_set_read_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 213 + order: 217 --- ## `stream_set_read_buffer()` — internals @@ -32,6 +32,11 @@ function stream_set_read_buffer(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_read_buffer()`](../../../php/builtins/io/stream_set_read_buffer.md) diff --git a/docs/internals/builtins/io/stream_set_timeout.md b/docs/internals/builtins/io/stream_set_timeout.md index 945078a811..2c08a2f859 100644 --- a/docs/internals/builtins/io/stream_set_timeout.md +++ b/docs/internals/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout() — internals" description: "Compiler internals for stream_set_timeout(): lowering path, type checks, and runtime helpers." sidebar: - order: 214 + order: 218 --- ## `stream_set_timeout()` — internals @@ -32,6 +32,11 @@ function stream_set_timeout(resource $stream, int $seconds, int $microseconds = - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_timeout()`](../../../php/builtins/io/stream_set_timeout.md) diff --git a/docs/internals/builtins/io/stream_set_write_buffer.md b/docs/internals/builtins/io/stream_set_write_buffer.md index 5687cee45a..8880f6671e 100644 --- a/docs/internals/builtins/io/stream_set_write_buffer.md +++ b/docs/internals/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer() — internals" description: "Compiler internals for stream_set_write_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 215 + order: 219 --- ## `stream_set_write_buffer()` — internals @@ -32,6 +32,11 @@ function stream_set_write_buffer(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_write_buffer()`](../../../php/builtins/io/stream_set_write_buffer.md) diff --git a/docs/internals/builtins/io/stream_socket_accept.md b/docs/internals/builtins/io/stream_socket_accept.md index 15bc1cd888..72294d6820 100644 --- a/docs/internals/builtins/io/stream_socket_accept.md +++ b/docs/internals/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept() — internals" description: "Compiler internals for stream_socket_accept(): lowering path, type checks, and runtime helpers." sidebar: - order: 216 + order: 220 --- ## `stream_socket_accept()` — internals @@ -34,6 +34,12 @@ function stream_socket_accept(resource $socket, float $timeout = null, string $p - **Arity**: takes 1–3 arguments (2 optional). - **By-reference parameters**: `$peer_name`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$peer_name`. + ## Cross-references - [User reference for `stream_socket_accept()`](../../../php/builtins/io/stream_socket_accept.md) diff --git a/docs/internals/builtins/io/stream_socket_client.md b/docs/internals/builtins/io/stream_socket_client.md index 86a0b421a4..667a4493b8 100644 --- a/docs/internals/builtins/io/stream_socket_client.md +++ b/docs/internals/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client() — internals" description: "Compiler internals for stream_socket_client(): lowering path, type checks, and runtime helpers." sidebar: - order: 217 + order: 221 --- ## `stream_socket_client()` — internals @@ -34,6 +34,11 @@ function stream_socket_client(string $address): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_client()`](../../../php/builtins/io/stream_socket_client.md) diff --git a/docs/internals/builtins/io/stream_socket_enable_crypto.md b/docs/internals/builtins/io/stream_socket_enable_crypto.md index 1b2277fb07..3f12cef623 100644 --- a/docs/internals/builtins/io/stream_socket_enable_crypto.md +++ b/docs/internals/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto() — internals" description: "Compiler internals for stream_socket_enable_crypto(): lowering path, type checks, and runtime helpers." sidebar: - order: 218 + order: 222 --- ## `stream_socket_enable_crypto()` — internals @@ -32,6 +32,11 @@ function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_enable_crypto()`](../../../php/builtins/io/stream_socket_enable_crypto.md) diff --git a/docs/internals/builtins/io/stream_socket_get_name.md b/docs/internals/builtins/io/stream_socket_get_name.md index 0108165a9d..1a983ced5d 100644 --- a/docs/internals/builtins/io/stream_socket_get_name.md +++ b/docs/internals/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name() — internals" description: "Compiler internals for stream_socket_get_name(): lowering path, type checks, and runtime helpers." sidebar: - order: 219 + order: 223 --- ## `stream_socket_get_name()` — internals @@ -33,6 +33,11 @@ function stream_socket_get_name(resource $socket, bool $remote): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_get_name()`](../../../php/builtins/io/stream_socket_get_name.md) diff --git a/docs/internals/builtins/io/stream_socket_pair.md b/docs/internals/builtins/io/stream_socket_pair.md index d0cca12362..11a3f2d947 100644 --- a/docs/internals/builtins/io/stream_socket_pair.md +++ b/docs/internals/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair() — internals" description: "Compiler internals for stream_socket_pair(): lowering path, type checks, and runtime helpers." sidebar: - order: 220 + order: 224 --- ## `stream_socket_pair()` — internals @@ -33,6 +33,11 @@ function stream_socket_pair(int $domain, int $type, int $protocol): mixed - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_pair()`](../../../php/builtins/io/stream_socket_pair.md) diff --git a/docs/internals/builtins/io/stream_socket_recvfrom.md b/docs/internals/builtins/io/stream_socket_recvfrom.md index 12ccb5cbcc..477c004ab9 100644 --- a/docs/internals/builtins/io/stream_socket_recvfrom.md +++ b/docs/internals/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom() — internals" description: "Compiler internals for stream_socket_recvfrom(): lowering path, type checks, and runtime helpers." sidebar: - order: 221 + order: 225 --- ## `stream_socket_recvfrom()` — internals @@ -33,6 +33,12 @@ function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, s - **Arity**: takes 2–4 arguments (2 optional). - **By-reference parameters**: `$address`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$address`. + ## Cross-references - [User reference for `stream_socket_recvfrom()`](../../../php/builtins/io/stream_socket_recvfrom.md) diff --git a/docs/internals/builtins/io/stream_socket_sendto.md b/docs/internals/builtins/io/stream_socket_sendto.md index 0350af291b..52c1ce1c38 100644 --- a/docs/internals/builtins/io/stream_socket_sendto.md +++ b/docs/internals/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto() — internals" description: "Compiler internals for stream_socket_sendto(): lowering path, type checks, and runtime helpers." sidebar: - order: 222 + order: 226 --- ## `stream_socket_sendto()` — internals @@ -32,6 +32,11 @@ function stream_socket_sendto(resource $socket, string $data, int $flags = 0, st - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_sendto()`](../../../php/builtins/io/stream_socket_sendto.md) diff --git a/docs/internals/builtins/io/stream_socket_server.md b/docs/internals/builtins/io/stream_socket_server.md index 86922f9e4e..370a71f217 100644 --- a/docs/internals/builtins/io/stream_socket_server.md +++ b/docs/internals/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server() — internals" description: "Compiler internals for stream_socket_server(): lowering path, type checks, and runtime helpers." sidebar: - order: 223 + order: 227 --- ## `stream_socket_server()` — internals @@ -33,6 +33,11 @@ function stream_socket_server(string $address): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_server()`](../../../php/builtins/io/stream_socket_server.md) diff --git a/docs/internals/builtins/io/stream_socket_shutdown.md b/docs/internals/builtins/io/stream_socket_shutdown.md index e5eb60e571..a3fe46d72c 100644 --- a/docs/internals/builtins/io/stream_socket_shutdown.md +++ b/docs/internals/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown() — internals" description: "Compiler internals for stream_socket_shutdown(): lowering path, type checks, and runtime helpers." sidebar: - order: 224 + order: 228 --- ## `stream_socket_shutdown()` — internals @@ -33,6 +33,11 @@ function stream_socket_shutdown(resource $stream, int $mode): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_shutdown()`](../../../php/builtins/io/stream_socket_shutdown.md) diff --git a/docs/internals/builtins/io/stream_supports_lock.md b/docs/internals/builtins/io/stream_supports_lock.md index 82a20d07f7..adf0c7dfbd 100644 --- a/docs/internals/builtins/io/stream_supports_lock.md +++ b/docs/internals/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock() — internals" description: "Compiler internals for stream_supports_lock(): lowering path, type checks, and runtime helpers." sidebar: - order: 225 + order: 229 --- ## `stream_supports_lock()` — internals @@ -33,6 +33,11 @@ function stream_supports_lock(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_supports_lock()`](../../../php/builtins/io/stream_supports_lock.md) diff --git a/docs/internals/builtins/io/stream_wrapper_register.md b/docs/internals/builtins/io/stream_wrapper_register.md index 4ed40920b3..fae50ea126 100644 --- a/docs/internals/builtins/io/stream_wrapper_register.md +++ b/docs/internals/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register() — internals" description: "Compiler internals for stream_wrapper_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 226 + order: 230 --- ## `stream_wrapper_register()` — internals @@ -33,6 +33,11 @@ function stream_wrapper_register(string $protocol, string $class, int $flags = 0 - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_register()`](../../../php/builtins/io/stream_wrapper_register.md) diff --git a/docs/internals/builtins/io/stream_wrapper_restore.md b/docs/internals/builtins/io/stream_wrapper_restore.md index 8e82185c1a..a2cc8bd362 100644 --- a/docs/internals/builtins/io/stream_wrapper_restore.md +++ b/docs/internals/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore() — internals" description: "Compiler internals for stream_wrapper_restore(): lowering path, type checks, and runtime helpers." sidebar: - order: 227 + order: 231 --- ## `stream_wrapper_restore()` — internals @@ -32,6 +32,11 @@ function stream_wrapper_restore(string $protocol): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_restore()`](../../../php/builtins/io/stream_wrapper_restore.md) diff --git a/docs/internals/builtins/io/stream_wrapper_unregister.md b/docs/internals/builtins/io/stream_wrapper_unregister.md index aaa6b3b57b..ed8ab7cf02 100644 --- a/docs/internals/builtins/io/stream_wrapper_unregister.md +++ b/docs/internals/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister() — internals" description: "Compiler internals for stream_wrapper_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 228 + order: 232 --- ## `stream_wrapper_unregister()` — internals @@ -33,6 +33,11 @@ function stream_wrapper_unregister(string $protocol): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_unregister()`](../../../php/builtins/io/stream_wrapper_unregister.md) diff --git a/docs/internals/builtins/io/vfprintf.md b/docs/internals/builtins/io/vfprintf.md index 3a7fdd8e50..266370d440 100644 --- a/docs/internals/builtins/io/vfprintf.md +++ b/docs/internals/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf() — internals" description: "Compiler internals for vfprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 229 + order: 233 --- ## `vfprintf()` — internals @@ -34,6 +34,11 @@ function vfprintf(resource $stream, string $format, array $values): int - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vfprintf()`](../../../php/builtins/io/vfprintf.md) diff --git a/docs/internals/builtins/json/json_decode.md b/docs/internals/builtins/json/json_decode.md index 9c7bdef87c..93169d8572 100644 --- a/docs/internals/builtins/json/json_decode.md +++ b/docs/internals/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode() — internals" description: "Compiler internals for json_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 230 + order: 234 --- ## `json_decode()` — internals @@ -33,6 +33,11 @@ function json_decode(string $json, bool $associative = null, int $depth = 512, i - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_decode()`](../../../php/builtins/json/json_decode.md) diff --git a/docs/internals/builtins/json/json_encode.md b/docs/internals/builtins/json/json_encode.md index 0b4a9d768a..37b908ea97 100644 --- a/docs/internals/builtins/json/json_encode.md +++ b/docs/internals/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode() — internals" description: "Compiler internals for json_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 231 + order: 235 --- ## `json_encode()` — internals @@ -32,6 +32,11 @@ function json_encode(mixed $value, int $flags = 0, int $depth = 512): string - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_encode()`](../../../php/builtins/json/json_encode.md) diff --git a/docs/internals/builtins/json/json_last_error.md b/docs/internals/builtins/json/json_last_error.md index 0e1d486891..de89d91702 100644 --- a/docs/internals/builtins/json/json_last_error.md +++ b/docs/internals/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error() — internals" description: "Compiler internals for json_last_error(): lowering path, type checks, and runtime helpers." sidebar: - order: 232 + order: 236 --- ## `json_last_error()` — internals @@ -33,6 +33,11 @@ function json_last_error(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_last_error()`](../../../php/builtins/json/json_last_error.md) diff --git a/docs/internals/builtins/json/json_last_error_msg.md b/docs/internals/builtins/json/json_last_error_msg.md index cb96b19c43..02f6d79abe 100644 --- a/docs/internals/builtins/json/json_last_error_msg.md +++ b/docs/internals/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg() — internals" description: "Compiler internals for json_last_error_msg(): lowering path, type checks, and runtime helpers." sidebar: - order: 233 + order: 237 --- ## `json_last_error_msg()` — internals @@ -34,6 +34,11 @@ function json_last_error_msg(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_last_error_msg()`](../../../php/builtins/json/json_last_error_msg.md) diff --git a/docs/internals/builtins/json/json_validate.md b/docs/internals/builtins/json/json_validate.md index 3fad80ebc2..19b93d6795 100644 --- a/docs/internals/builtins/json/json_validate.md +++ b/docs/internals/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate() — internals" description: "Compiler internals for json_validate(): lowering path, type checks, and runtime helpers." sidebar: - order: 234 + order: 238 --- ## `json_validate()` — internals @@ -33,6 +33,11 @@ function json_validate(string $json, int $depth = 512, int $flags = 0): bool - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_validate()`](../../../php/builtins/json/json_validate.md) diff --git a/docs/internals/builtins/math/abs.md b/docs/internals/builtins/math/abs.md index 7922313ede..5a9a42fdb7 100644 --- a/docs/internals/builtins/math/abs.md +++ b/docs/internals/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs() — internals" description: "Compiler internals for abs(): lowering path, type checks, and runtime helpers." sidebar: - order: 235 + order: 239 --- ## `abs()` — internals @@ -33,6 +33,11 @@ function abs(int $num): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/abs.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `abs()`](../../../php/builtins/math/abs.md) diff --git a/docs/internals/builtins/math/acos.md b/docs/internals/builtins/math/acos.md index 1d800b1477..64ef7b0b38 100644 --- a/docs/internals/builtins/math/acos.md +++ b/docs/internals/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos() — internals" description: "Compiler internals for acos(): lowering path, type checks, and runtime helpers." sidebar: - order: 236 + order: 240 --- ## `acos()` — internals @@ -32,6 +32,11 @@ function acos(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/acos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `acos()`](../../../php/builtins/math/acos.md) diff --git a/docs/internals/builtins/math/asin.md b/docs/internals/builtins/math/asin.md index 849ba063e2..052bd93436 100644 --- a/docs/internals/builtins/math/asin.md +++ b/docs/internals/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin() — internals" description: "Compiler internals for asin(): lowering path, type checks, and runtime helpers." sidebar: - order: 237 + order: 241 --- ## `asin()` — internals @@ -32,6 +32,11 @@ function asin(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/asin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `asin()`](../../../php/builtins/math/asin.md) diff --git a/docs/internals/builtins/math/atan.md b/docs/internals/builtins/math/atan.md index f60bbb0fe2..1e4325b36a 100644 --- a/docs/internals/builtins/math/atan.md +++ b/docs/internals/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan() — internals" description: "Compiler internals for atan(): lowering path, type checks, and runtime helpers." sidebar: - order: 238 + order: 242 --- ## `atan()` — internals @@ -32,6 +32,11 @@ function atan(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `atan()`](../../../php/builtins/math/atan.md) diff --git a/docs/internals/builtins/math/atan2.md b/docs/internals/builtins/math/atan2.md index e45ecf6a8c..2f063340bd 100644 --- a/docs/internals/builtins/math/atan2.md +++ b/docs/internals/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2() — internals" description: "Compiler internals for atan2(): lowering path, type checks, and runtime helpers." sidebar: - order: 239 + order: 243 --- ## `atan2()` — internals @@ -32,6 +32,11 @@ function atan2(float $y, float $x): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `atan2()`](../../../php/builtins/math/atan2.md) diff --git a/docs/internals/builtins/math/ceil.md b/docs/internals/builtins/math/ceil.md index cfaa80576d..e8960469c2 100644 --- a/docs/internals/builtins/math/ceil.md +++ b/docs/internals/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil() — internals" description: "Compiler internals for ceil(): lowering path, type checks, and runtime helpers." sidebar: - order: 240 + order: 244 --- ## `ceil()` — internals @@ -32,6 +32,11 @@ function ceil(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ceil()`](../../../php/builtins/math/ceil.md) diff --git a/docs/internals/builtins/math/clamp.md b/docs/internals/builtins/math/clamp.md index ca4f8a19b3..3e7c25b27e 100644 --- a/docs/internals/builtins/math/clamp.md +++ b/docs/internals/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp() — internals" description: "Compiler internals for clamp(): lowering path, type checks, and runtime helpers." sidebar: - order: 241 + order: 245 --- ## `clamp()` — internals @@ -32,6 +32,11 @@ function clamp(int $value, int $min, int $max): mixed - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `clamp()`](../../../php/builtins/math/clamp.md) diff --git a/docs/internals/builtins/math/cos.md b/docs/internals/builtins/math/cos.md index 0e995e6cc9..e6c61301d5 100644 --- a/docs/internals/builtins/math/cos.md +++ b/docs/internals/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos() — internals" description: "Compiler internals for cos(): lowering path, type checks, and runtime helpers." sidebar: - order: 242 + order: 246 --- ## `cos()` — internals @@ -32,6 +32,11 @@ function cos(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `cos()`](../../../php/builtins/math/cos.md) diff --git a/docs/internals/builtins/math/cosh.md b/docs/internals/builtins/math/cosh.md index 1ce7156dbe..7030bfdced 100644 --- a/docs/internals/builtins/math/cosh.md +++ b/docs/internals/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh() — internals" description: "Compiler internals for cosh(): lowering path, type checks, and runtime helpers." sidebar: - order: 243 + order: 247 --- ## `cosh()` — internals @@ -32,6 +32,11 @@ function cosh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `cosh()`](../../../php/builtins/math/cosh.md) diff --git a/docs/internals/builtins/math/deg2rad.md b/docs/internals/builtins/math/deg2rad.md index 57aa0dc01e..a73d330f79 100644 --- a/docs/internals/builtins/math/deg2rad.md +++ b/docs/internals/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad() — internals" description: "Compiler internals for deg2rad(): lowering path, type checks, and runtime helpers." sidebar: - order: 244 + order: 248 --- ## `deg2rad()` — internals @@ -32,6 +32,11 @@ function deg2rad(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `deg2rad()`](../../../php/builtins/math/deg2rad.md) diff --git a/docs/internals/builtins/math/exp.md b/docs/internals/builtins/math/exp.md index 8e55435630..dc18642e7a 100644 --- a/docs/internals/builtins/math/exp.md +++ b/docs/internals/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp() — internals" description: "Compiler internals for exp(): lowering path, type checks, and runtime helpers." sidebar: - order: 245 + order: 249 --- ## `exp()` — internals @@ -32,6 +32,11 @@ function exp(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/exp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exp()`](../../../php/builtins/math/exp.md) diff --git a/docs/internals/builtins/math/fdiv.md b/docs/internals/builtins/math/fdiv.md index 2779c26376..7ff386f1d7 100644 --- a/docs/internals/builtins/math/fdiv.md +++ b/docs/internals/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv() — internals" description: "Compiler internals for fdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 246 + order: 250 --- ## `fdiv()` — internals @@ -32,6 +32,11 @@ function fdiv(float $num1, float $num2): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fdiv()`](../../../php/builtins/math/fdiv.md) diff --git a/docs/internals/builtins/math/floor.md b/docs/internals/builtins/math/floor.md index c197992643..14f7a01be5 100644 --- a/docs/internals/builtins/math/floor.md +++ b/docs/internals/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor() — internals" description: "Compiler internals for floor(): lowering path, type checks, and runtime helpers." sidebar: - order: 247 + order: 251 --- ## `floor()` — internals @@ -32,6 +32,11 @@ function floor(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/floor.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `floor()`](../../../php/builtins/math/floor.md) diff --git a/docs/internals/builtins/math/fmod.md b/docs/internals/builtins/math/fmod.md index 9ad291188d..afcff9a447 100644 --- a/docs/internals/builtins/math/fmod.md +++ b/docs/internals/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod() — internals" description: "Compiler internals for fmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 248 + order: 252 --- ## `fmod()` — internals @@ -32,6 +32,11 @@ function fmod(float $num1, float $num2): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fmod()`](../../../php/builtins/math/fmod.md) diff --git a/docs/internals/builtins/math/hypot.md b/docs/internals/builtins/math/hypot.md index 37b485883f..ea2d25abe4 100644 --- a/docs/internals/builtins/math/hypot.md +++ b/docs/internals/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot() — internals" description: "Compiler internals for hypot(): lowering path, type checks, and runtime helpers." sidebar: - order: 249 + order: 253 --- ## `hypot()` — internals @@ -32,6 +32,11 @@ function hypot(float $x, float $y): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hypot()`](../../../php/builtins/math/hypot.md) diff --git a/docs/internals/builtins/math/intdiv.md b/docs/internals/builtins/math/intdiv.md index 8121f26dc3..671ed80851 100644 --- a/docs/internals/builtins/math/intdiv.md +++ b/docs/internals/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv() — internals" description: "Compiler internals for intdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 250 + order: 254 --- ## `intdiv()` — internals @@ -32,6 +32,11 @@ function intdiv(int $num1, int $num2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `intdiv()`](../../../php/builtins/math/intdiv.md) diff --git a/docs/internals/builtins/math/is_finite.md b/docs/internals/builtins/math/is_finite.md index 1ce482dba4..649142dde7 100644 --- a/docs/internals/builtins/math/is_finite.md +++ b/docs/internals/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite() — internals" description: "Compiler internals for is_finite(): lowering path, type checks, and runtime helpers." sidebar: - order: 251 + order: 255 --- ## `is_finite()` — internals @@ -32,6 +32,11 @@ function is_finite(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_finite()`](../../../php/builtins/math/is_finite.md) diff --git a/docs/internals/builtins/math/is_infinite.md b/docs/internals/builtins/math/is_infinite.md index 341d462e3f..d58d9f120a 100644 --- a/docs/internals/builtins/math/is_infinite.md +++ b/docs/internals/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite() — internals" description: "Compiler internals for is_infinite(): lowering path, type checks, and runtime helpers." sidebar: - order: 252 + order: 256 --- ## `is_infinite()` — internals @@ -32,6 +32,11 @@ function is_infinite(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_infinite()`](../../../php/builtins/math/is_infinite.md) diff --git a/docs/internals/builtins/math/is_nan.md b/docs/internals/builtins/math/is_nan.md index 3d54cef485..1ad2394394 100644 --- a/docs/internals/builtins/math/is_nan.md +++ b/docs/internals/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan() — internals" description: "Compiler internals for is_nan(): lowering path, type checks, and runtime helpers." sidebar: - order: 253 + order: 257 --- ## `is_nan()` — internals @@ -32,6 +32,11 @@ function is_nan(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_nan()`](../../../php/builtins/math/is_nan.md) diff --git a/docs/internals/builtins/math/log.md b/docs/internals/builtins/math/log.md index 11999ebe30..6d4cddf357 100644 --- a/docs/internals/builtins/math/log.md +++ b/docs/internals/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log() — internals" description: "Compiler internals for log(): lowering path, type checks, and runtime helpers." sidebar: - order: 254 + order: 258 --- ## `log()` — internals @@ -32,6 +32,11 @@ function log(float $num, float $base = 2.718281828459045): float - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log()`](../../../php/builtins/math/log.md) diff --git a/docs/internals/builtins/math/log10.md b/docs/internals/builtins/math/log10.md index b02032e611..171d366da7 100644 --- a/docs/internals/builtins/math/log10.md +++ b/docs/internals/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10() — internals" description: "Compiler internals for log10(): lowering path, type checks, and runtime helpers." sidebar: - order: 255 + order: 259 --- ## `log10()` — internals @@ -32,6 +32,11 @@ function log10(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log10.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log10()`](../../../php/builtins/math/log10.md) diff --git a/docs/internals/builtins/math/log2.md b/docs/internals/builtins/math/log2.md index ffa1dbca15..81b8cbb081 100644 --- a/docs/internals/builtins/math/log2.md +++ b/docs/internals/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2() — internals" description: "Compiler internals for log2(): lowering path, type checks, and runtime helpers." sidebar: - order: 256 + order: 260 --- ## `log2()` — internals @@ -32,6 +32,11 @@ function log2(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log2.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log2()`](../../../php/builtins/math/log2.md) diff --git a/docs/internals/builtins/math/max.md b/docs/internals/builtins/math/max.md index 0a31bbee79..ad78ea4d65 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max() — internals" description: "Compiler internals for max(): lowering path, type checks, and runtime helpers." sidebar: - order: 257 + order: 261 --- ## `max()` — internals @@ -33,6 +33,12 @@ function max(mixed $value, ...$values): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/max.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `max()`](../../../php/builtins/math/max.md) diff --git a/docs/internals/builtins/math/min.md b/docs/internals/builtins/math/min.md index d39f3d4b3e..d59126958c 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min() — internals" description: "Compiler internals for min(): lowering path, type checks, and runtime helpers." sidebar: - order: 258 + order: 262 --- ## `min()` — internals @@ -33,6 +33,12 @@ function min(mixed $value, ...$values): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/min.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `min()`](../../../php/builtins/math/min.md) diff --git a/docs/internals/builtins/math/mt_rand.md b/docs/internals/builtins/math/mt_rand.md index 468eb0ea11..60746fc7d9 100644 --- a/docs/internals/builtins/math/mt_rand.md +++ b/docs/internals/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand() — internals" description: "Compiler internals for mt_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 259 + order: 263 --- ## `mt_rand()` — internals @@ -33,6 +33,11 @@ function mt_rand(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mt_rand()`](../../../php/builtins/math/mt_rand.md) diff --git a/docs/internals/builtins/math/pi.md b/docs/internals/builtins/math/pi.md index 16b2a09935..17aaacff6b 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi() — internals" description: "Compiler internals for pi(): lowering path, type checks, and runtime helpers." sidebar: - order: 260 + order: 264 --- ## `pi()` — internals @@ -32,6 +32,11 @@ function pi(): float - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pi.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pi()`](../../../php/builtins/math/pi.md) diff --git a/docs/internals/builtins/math/pow.md b/docs/internals/builtins/math/pow.md index e0996e87bf..31f7bbafef 100644 --- a/docs/internals/builtins/math/pow.md +++ b/docs/internals/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow() — internals" description: "Compiler internals for pow(): lowering path, type checks, and runtime helpers." sidebar: - order: 261 + order: 265 --- ## `pow()` — internals @@ -32,6 +32,11 @@ function pow(float $num, float $exponent): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pow.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pow()`](../../../php/builtins/math/pow.md) diff --git a/docs/internals/builtins/math/rad2deg.md b/docs/internals/builtins/math/rad2deg.md index 621a148d0f..9602a4e606 100644 --- a/docs/internals/builtins/math/rad2deg.md +++ b/docs/internals/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg() — internals" description: "Compiler internals for rad2deg(): lowering path, type checks, and runtime helpers." sidebar: - order: 262 + order: 266 --- ## `rad2deg()` — internals @@ -32,6 +32,11 @@ function rad2deg(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rad2deg()`](../../../php/builtins/math/rad2deg.md) diff --git a/docs/internals/builtins/math/rand.md b/docs/internals/builtins/math/rand.md index 6aa48de0d2..d1b112c1e3 100644 --- a/docs/internals/builtins/math/rand.md +++ b/docs/internals/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand() — internals" description: "Compiler internals for rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 263 + order: 267 --- ## `rand()` — internals @@ -33,6 +33,11 @@ function rand(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rand()`](../../../php/builtins/math/rand.md) diff --git a/docs/internals/builtins/math/random_int.md b/docs/internals/builtins/math/random_int.md index 90c988932a..ba13aad3e0 100644 --- a/docs/internals/builtins/math/random_int.md +++ b/docs/internals/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int() — internals" description: "Compiler internals for random_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 264 + order: 268 --- ## `random_int()` — internals @@ -32,6 +32,11 @@ function random_int(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `random_int()`](../../../php/builtins/math/random_int.md) diff --git a/docs/internals/builtins/math/round.md b/docs/internals/builtins/math/round.md index de5adfdb6e..3ca3bd0f96 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round() — internals" description: "Compiler internals for round(): lowering path, type checks, and runtime helpers." sidebar: - order: 265 + order: 269 --- ## `round()` — internals @@ -32,6 +32,11 @@ function round(float $num, int $precision = 0): float - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/round.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `round()`](../../../php/builtins/math/round.md) diff --git a/docs/internals/builtins/math/sin.md b/docs/internals/builtins/math/sin.md index 28477b86ae..667c9121b8 100644 --- a/docs/internals/builtins/math/sin.md +++ b/docs/internals/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin() — internals" description: "Compiler internals for sin(): lowering path, type checks, and runtime helpers." sidebar: - order: 266 + order: 270 --- ## `sin()` — internals @@ -32,6 +32,11 @@ function sin(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sin()`](../../../php/builtins/math/sin.md) diff --git a/docs/internals/builtins/math/sinh.md b/docs/internals/builtins/math/sinh.md index f9ca8eb208..2e7cf70133 100644 --- a/docs/internals/builtins/math/sinh.md +++ b/docs/internals/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh() — internals" description: "Compiler internals for sinh(): lowering path, type checks, and runtime helpers." sidebar: - order: 267 + order: 271 --- ## `sinh()` — internals @@ -32,6 +32,11 @@ function sinh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sinh()`](../../../php/builtins/math/sinh.md) diff --git a/docs/internals/builtins/math/sqrt.md b/docs/internals/builtins/math/sqrt.md index 93c18ecd56..91f946a372 100644 --- a/docs/internals/builtins/math/sqrt.md +++ b/docs/internals/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt() — internals" description: "Compiler internals for sqrt(): lowering path, type checks, and runtime helpers." sidebar: - order: 268 + order: 272 --- ## `sqrt()` — internals @@ -32,6 +32,11 @@ function sqrt(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sqrt()`](../../../php/builtins/math/sqrt.md) diff --git a/docs/internals/builtins/math/tan.md b/docs/internals/builtins/math/tan.md index 3344f007d5..870d6cab25 100644 --- a/docs/internals/builtins/math/tan.md +++ b/docs/internals/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan() — internals" description: "Compiler internals for tan(): lowering path, type checks, and runtime helpers." sidebar: - order: 269 + order: 273 --- ## `tan()` — internals @@ -32,6 +32,11 @@ function tan(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tan()`](../../../php/builtins/math/tan.md) diff --git a/docs/internals/builtins/math/tanh.md b/docs/internals/builtins/math/tanh.md index 596d98d893..3fc53e3d67 100644 --- a/docs/internals/builtins/math/tanh.md +++ b/docs/internals/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh() — internals" description: "Compiler internals for tanh(): lowering path, type checks, and runtime helpers." sidebar: - order: 270 + order: 274 --- ## `tanh()` — internals @@ -32,6 +32,11 @@ function tanh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tanh()`](../../../php/builtins/math/tanh.md) diff --git a/docs/internals/builtins/misc/buffer_new.md b/docs/internals/builtins/misc/buffer_new.md index 1972bf2820..a044d59f59 100644 --- a/docs/internals/builtins/misc/buffer_new.md +++ b/docs/internals/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new() — internals" description: "Compiler internals for buffer_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 271 + order: 275 --- ## `buffer_new()` — internals @@ -28,6 +28,11 @@ function buffer_new(int $length): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_new()`](../../../php/builtins/misc/buffer_new.md) diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index a3181ab954..37079664aa 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define() — internals" description: "Compiler internals for define(): lowering path, type checks, and runtime helpers." sidebar: - order: 272 + order: 276 --- ## `define()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/define.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/define.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:82](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L82) (`lower_define`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:372](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L372) (`lower_define`) - **Function symbol**: `lower_define()` @@ -32,6 +32,11 @@ function define(string $constant_name, mixed $value): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/define.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/define.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `define()`](../../../php/builtins/misc/define.md) diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index ba5a89d8ad..83900ba1b5 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined() — internals" description: "Compiler internals for defined(): lowering path, type checks, and runtime helpers." sidebar: - order: 273 + order: 277 --- ## `defined()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:261](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L261) (`lower_defined`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:551](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L551) (`lower_defined`) - **Function symbol**: `lower_defined()` @@ -32,6 +32,11 @@ function defined(string $constant_name): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/defined.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `defined()`](../../../php/builtins/misc/defined.md) diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index f14496d564..4830ba86d9 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty() — internals" description: "Compiler internals for empty(): lowering path, type checks, and runtime helpers." sidebar: - order: 274 + order: 278 --- ## `empty()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:619](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L619) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1218](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1218) (`lower_empty`) - **Function symbol**: `lower_empty()` @@ -33,6 +33,11 @@ function empty(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `empty()`](../../../php/builtins/misc/empty.md) diff --git a/docs/internals/builtins/misc/header.md b/docs/internals/builtins/misc/header.md index 55a09c95d7..e250babb92 100644 --- a/docs/internals/builtins/misc/header.md +++ b/docs/internals/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header() — internals" description: "Compiler internals for header(): lowering path, type checks, and runtime helpers." sidebar: - order: 275 + order: 279 --- ## `header()` — internals @@ -38,6 +38,11 @@ function header(string $header, bool $replace = true, int $response_code = 0): v - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/header.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/header.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `header()`](../../../php/builtins/misc/header.md) diff --git a/docs/internals/builtins/misc/http_response_code.md b/docs/internals/builtins/misc/http_response_code.md index db23b4f74e..03e0f8aba4 100644 --- a/docs/internals/builtins/misc/http_response_code.md +++ b/docs/internals/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code() — internals" description: "Compiler internals for http_response_code(): lowering path, type checks, and runtime helpers." sidebar: - order: 276 + order: 280 --- ## `http_response_code()` — internals @@ -37,6 +37,11 @@ function http_response_code(int $response_code = 0): int - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `http_response_code()`](../../../php/builtins/misc/http_response_code.md) diff --git a/docs/internals/builtins/misc/isset.md b/docs/internals/builtins/misc/isset.md index 6951a04bb5..7f28f83f13 100644 --- a/docs/internals/builtins/misc/isset.md +++ b/docs/internals/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset() — internals" description: "Compiler internals for isset(): lowering path, type checks, and runtime helpers." sidebar: - order: 277 + order: 281 --- ## `isset()` — internals @@ -33,6 +33,12 @@ function isset(mixed $var, ...$vars): bool - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `isset()`](../../../php/builtins/misc/isset.md) diff --git a/docs/internals/builtins/misc/php_uname.md b/docs/internals/builtins/misc/php_uname.md index d24b3ebcb2..919f526874 100644 --- a/docs/internals/builtins/misc/php_uname.md +++ b/docs/internals/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname() — internals" description: "Compiler internals for php_uname(): lowering path, type checks, and runtime helpers." sidebar: - order: 278 + order: 282 --- ## `php_uname()` — internals @@ -33,6 +33,11 @@ function php_uname(string $mode = 'a'): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `php_uname()`](../../../php/builtins/misc/php_uname.md) diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index 0d0b423615..278abe7712 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion() — internals" description: "Compiler internals for phpversion(): lowering path, type checks, and runtime helpers." sidebar: - order: 279 + order: 283 --- ## `phpversion()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:251](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L251) (`lower_phpversion`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:541](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L541) (`lower_phpversion`) - **Function symbol**: `lower_phpversion()` @@ -32,6 +32,11 @@ function phpversion(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `phpversion()`](../../../php/builtins/misc/phpversion.md) diff --git a/docs/internals/builtins/misc/print_r.md b/docs/internals/builtins/misc/print_r.md index 7e8d6a11f4..160f3da280 100644 --- a/docs/internals/builtins/misc/print_r.md +++ b/docs/internals/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r() — internals" description: "Compiler internals for print_r(): lowering path, type checks, and runtime helpers." sidebar: - order: 280 + order: 284 --- ## `print_r()` — internals @@ -42,6 +42,11 @@ function print_r(mixed $value, bool $return = false): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `print_r()`](../../../php/builtins/misc/print_r.md) diff --git a/docs/internals/builtins/misc/serialize.md b/docs/internals/builtins/misc/serialize.md index fc56f74e99..d5c10d953b 100644 --- a/docs/internals/builtins/misc/serialize.md +++ b/docs/internals/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize() — internals" description: "Compiler internals for serialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 281 + order: 285 --- ## `serialize()` — internals @@ -38,6 +38,10 @@ function serialize(mixed $value): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `serialize()`](../../../php/builtins/misc/serialize.md) diff --git a/docs/internals/builtins/misc/unserialize.md b/docs/internals/builtins/misc/unserialize.md index c3b064a5fd..4022a76500 100644 --- a/docs/internals/builtins/misc/unserialize.md +++ b/docs/internals/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize() — internals" description: "Compiler internals for unserialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 282 + order: 286 --- ## `unserialize()` — internals @@ -38,6 +38,10 @@ function unserialize(string $data, mixed $options = []): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `unserialize()`](../../../php/builtins/misc/unserialize.md) diff --git a/docs/internals/builtins/misc/unset.md b/docs/internals/builtins/misc/unset.md index afe1bc1aad..0f717d1af0 100644 --- a/docs/internals/builtins/misc/unset.md +++ b/docs/internals/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset() — internals" description: "Compiler internals for unset(): lowering path, type checks, and runtime helpers." sidebar: - order: 283 + order: 287 --- ## `unset()` — internals @@ -33,6 +33,12 @@ function unset(mixed $var, ...$vars): void - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `unset()`](../../../php/builtins/misc/unset.md) diff --git a/docs/internals/builtins/misc/var_dump.md b/docs/internals/builtins/misc/var_dump.md index 9e1a6d1f7b..51ce948568 100644 --- a/docs/internals/builtins/misc/var_dump.md +++ b/docs/internals/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump() — internals" description: "Compiler internals for var_dump(): lowering path, type checks, and runtime helpers." sidebar: - order: 284 + order: 288 --- ## `var_dump()` — internals @@ -34,6 +34,12 @@ function var_dump(mixed $value, ...$values): void - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `var_dump()`](../../../php/builtins/misc/var_dump.md) diff --git a/docs/internals/builtins/pointer/ptr.md b/docs/internals/builtins/pointer/ptr.md index c03606a05b..1b6ae6c09e 100644 --- a/docs/internals/builtins/pointer/ptr.md +++ b/docs/internals/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr() — internals" description: "Compiler internals for ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 285 + order: 289 --- ## `ptr()` — internals @@ -32,6 +32,11 @@ function ptr(mixed $value): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr()`](../../../php/builtins/pointer/ptr.md) diff --git a/docs/internals/builtins/pointer/ptr_get.md b/docs/internals/builtins/pointer/ptr_get.md index 6727373272..524f0f1a0c 100644 --- a/docs/internals/builtins/pointer/ptr_get.md +++ b/docs/internals/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get() — internals" description: "Compiler internals for ptr_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 286 + order: 290 --- ## `ptr_get()` — internals @@ -32,6 +32,11 @@ function ptr_get(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_get()`](../../../php/builtins/pointer/ptr_get.md) diff --git a/docs/internals/builtins/pointer/ptr_is_null.md b/docs/internals/builtins/pointer/ptr_is_null.md index 56954203d0..40c5d408ec 100644 --- a/docs/internals/builtins/pointer/ptr_is_null.md +++ b/docs/internals/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null() — internals" description: "Compiler internals for ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 287 + order: 291 --- ## `ptr_is_null()` — internals @@ -32,6 +32,11 @@ function ptr_is_null(pointer $pointer): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_is_null()`](../../../php/builtins/pointer/ptr_is_null.md) diff --git a/docs/internals/builtins/pointer/ptr_null.md b/docs/internals/builtins/pointer/ptr_null.md index 40512628fb..32487821eb 100644 --- a/docs/internals/builtins/pointer/ptr_null.md +++ b/docs/internals/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null() — internals" description: "Compiler internals for ptr_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 288 + order: 292 --- ## `ptr_null()` — internals @@ -32,6 +32,11 @@ function ptr_null(): mixed - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_null()`](../../../php/builtins/pointer/ptr_null.md) diff --git a/docs/internals/builtins/pointer/ptr_offset.md b/docs/internals/builtins/pointer/ptr_offset.md index c6b593077d..6c5bf3dad2 100644 --- a/docs/internals/builtins/pointer/ptr_offset.md +++ b/docs/internals/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset() — internals" description: "Compiler internals for ptr_offset(): lowering path, type checks, and runtime helpers." sidebar: - order: 289 + order: 293 --- ## `ptr_offset()` — internals @@ -32,6 +32,11 @@ function ptr_offset(pointer $pointer, int $offset): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_offset()`](../../../php/builtins/pointer/ptr_offset.md) diff --git a/docs/internals/builtins/pointer/ptr_read16.md b/docs/internals/builtins/pointer/ptr_read16.md index bf576d19ea..a38f194c5b 100644 --- a/docs/internals/builtins/pointer/ptr_read16.md +++ b/docs/internals/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16() — internals" description: "Compiler internals for ptr_read16(): lowering path, type checks, and runtime helpers." sidebar: - order: 290 + order: 294 --- ## `ptr_read16()` — internals @@ -33,6 +33,11 @@ function ptr_read16(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read16()`](../../../php/builtins/pointer/ptr_read16.md) diff --git a/docs/internals/builtins/pointer/ptr_read32.md b/docs/internals/builtins/pointer/ptr_read32.md index f23fa0ac9c..26aead378c 100644 --- a/docs/internals/builtins/pointer/ptr_read32.md +++ b/docs/internals/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32() — internals" description: "Compiler internals for ptr_read32(): lowering path, type checks, and runtime helpers." sidebar: - order: 291 + order: 295 --- ## `ptr_read32()` — internals @@ -33,6 +33,11 @@ function ptr_read32(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read32()`](../../../php/builtins/pointer/ptr_read32.md) diff --git a/docs/internals/builtins/pointer/ptr_read8.md b/docs/internals/builtins/pointer/ptr_read8.md index 2583f5979e..ce1c57bcb1 100644 --- a/docs/internals/builtins/pointer/ptr_read8.md +++ b/docs/internals/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8() — internals" description: "Compiler internals for ptr_read8(): lowering path, type checks, and runtime helpers." sidebar: - order: 292 + order: 296 --- ## `ptr_read8()` — internals @@ -32,6 +32,11 @@ function ptr_read8(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read8()`](../../../php/builtins/pointer/ptr_read8.md) diff --git a/docs/internals/builtins/pointer/ptr_read_string.md b/docs/internals/builtins/pointer/ptr_read_string.md index e0224b38d9..82f33512ed 100644 --- a/docs/internals/builtins/pointer/ptr_read_string.md +++ b/docs/internals/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string() — internals" description: "Compiler internals for ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 293 + order: 297 --- ## `ptr_read_string()` — internals @@ -33,6 +33,11 @@ function ptr_read_string(pointer $pointer, int $length): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read_string()`](../../../php/builtins/pointer/ptr_read_string.md) diff --git a/docs/internals/builtins/pointer/ptr_set.md b/docs/internals/builtins/pointer/ptr_set.md index eff6bc0cfe..186aaf5267 100644 --- a/docs/internals/builtins/pointer/ptr_set.md +++ b/docs/internals/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set() — internals" description: "Compiler internals for ptr_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 294 + order: 298 --- ## `ptr_set()` — internals @@ -32,6 +32,11 @@ function ptr_set(pointer $pointer, mixed $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_set()`](../../../php/builtins/pointer/ptr_set.md) diff --git a/docs/internals/builtins/pointer/ptr_sizeof.md b/docs/internals/builtins/pointer/ptr_sizeof.md index c7e326e8be..d0616caf8f 100644 --- a/docs/internals/builtins/pointer/ptr_sizeof.md +++ b/docs/internals/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof() — internals" description: "Compiler internals for ptr_sizeof(): lowering path, type checks, and runtime helpers." sidebar: - order: 295 + order: 299 --- ## `ptr_sizeof()` — internals @@ -32,6 +32,11 @@ function ptr_sizeof(string $type): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_sizeof()`](../../../php/builtins/pointer/ptr_sizeof.md) diff --git a/docs/internals/builtins/pointer/ptr_write16.md b/docs/internals/builtins/pointer/ptr_write16.md index 0c633f322c..4ab6dcd112 100644 --- a/docs/internals/builtins/pointer/ptr_write16.md +++ b/docs/internals/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16() — internals" description: "Compiler internals for ptr_write16(): lowering path, type checks, and runtime helpers." sidebar: - order: 296 + order: 300 --- ## `ptr_write16()` — internals @@ -33,6 +33,11 @@ function ptr_write16(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write16()`](../../../php/builtins/pointer/ptr_write16.md) diff --git a/docs/internals/builtins/pointer/ptr_write32.md b/docs/internals/builtins/pointer/ptr_write32.md index 0304a4ffc9..7a3ac6eb84 100644 --- a/docs/internals/builtins/pointer/ptr_write32.md +++ b/docs/internals/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32() — internals" description: "Compiler internals for ptr_write32(): lowering path, type checks, and runtime helpers." sidebar: - order: 297 + order: 301 --- ## `ptr_write32()` — internals @@ -33,6 +33,11 @@ function ptr_write32(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write32()`](../../../php/builtins/pointer/ptr_write32.md) diff --git a/docs/internals/builtins/pointer/ptr_write8.md b/docs/internals/builtins/pointer/ptr_write8.md index 9b45cf5cfb..0c285cff20 100644 --- a/docs/internals/builtins/pointer/ptr_write8.md +++ b/docs/internals/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8() — internals" description: "Compiler internals for ptr_write8(): lowering path, type checks, and runtime helpers." sidebar: - order: 298 + order: 302 --- ## `ptr_write8()` — internals @@ -32,6 +32,11 @@ function ptr_write8(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write8()`](../../../php/builtins/pointer/ptr_write8.md) diff --git a/docs/internals/builtins/pointer/ptr_write_string.md b/docs/internals/builtins/pointer/ptr_write_string.md index 649930057a..1d8277df2a 100644 --- a/docs/internals/builtins/pointer/ptr_write_string.md +++ b/docs/internals/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string() — internals" description: "Compiler internals for ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 299 + order: 303 --- ## `ptr_write_string()` — internals @@ -33,6 +33,11 @@ function ptr_write_string(pointer $pointer, string $string): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write_string()`](../../../php/builtins/pointer/ptr_write_string.md) diff --git a/docs/internals/builtins/pointer/zval_free.md b/docs/internals/builtins/pointer/zval_free.md index 0a75804ce0..ffe045d625 100644 --- a/docs/internals/builtins/pointer/zval_free.md +++ b/docs/internals/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free() — internals" description: "Compiler internals for zval_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 300 + order: 304 --- ## `zval_free()` — internals @@ -33,6 +33,10 @@ function zval_free(pointer $zval): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_free()`](../../../php/builtins/pointer/zval_free.md) diff --git a/docs/internals/builtins/pointer/zval_pack.md b/docs/internals/builtins/pointer/zval_pack.md index 172ec6a16e..e903a83d31 100644 --- a/docs/internals/builtins/pointer/zval_pack.md +++ b/docs/internals/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack() — internals" description: "Compiler internals for zval_pack(): lowering path, type checks, and runtime helpers." sidebar: - order: 301 + order: 305 --- ## `zval_pack()` — internals @@ -42,6 +42,10 @@ function zval_pack(mixed $value): pointer - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_pack()`](../../../php/builtins/pointer/zval_pack.md) diff --git a/docs/internals/builtins/pointer/zval_type.md b/docs/internals/builtins/pointer/zval_type.md index 36633b0d95..2dedc2723e 100644 --- a/docs/internals/builtins/pointer/zval_type.md +++ b/docs/internals/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type() — internals" description: "Compiler internals for zval_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 302 + order: 306 --- ## `zval_type()` — internals @@ -34,6 +34,10 @@ function zval_type(pointer $zval): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_type()`](../../../php/builtins/pointer/zval_type.md) diff --git a/docs/internals/builtins/pointer/zval_unpack.md b/docs/internals/builtins/pointer/zval_unpack.md index 35dcefc48a..867ef4b5a3 100644 --- a/docs/internals/builtins/pointer/zval_unpack.md +++ b/docs/internals/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack() — internals" description: "Compiler internals for zval_unpack(): lowering path, type checks, and runtime helpers." sidebar: - order: 303 + order: 307 --- ## `zval_unpack()` — internals @@ -35,6 +35,10 @@ function zval_unpack(pointer $zval): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_unpack()`](../../../php/builtins/pointer/zval_unpack.md) diff --git a/docs/internals/builtins/process/die.md b/docs/internals/builtins/process/die.md index 11ae6e98db..1d3abf2306 100644 --- a/docs/internals/builtins/process/die.md +++ b/docs/internals/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die() — internals" description: "Compiler internals for die(): lowering path, type checks, and runtime helpers." sidebar: - order: 304 + order: 308 --- ## `die()` — internals @@ -28,6 +28,11 @@ function die(int $status): void - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/die.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/die.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `die()`](../../../php/builtins/process/die.md) diff --git a/docs/internals/builtins/process/exec.md b/docs/internals/builtins/process/exec.md index 94ef06ac80..324570a9a2 100644 --- a/docs/internals/builtins/process/exec.md +++ b/docs/internals/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec() — internals" description: "Compiler internals for exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 305 + order: 309 --- ## `exec()` — internals @@ -32,6 +32,11 @@ function exec(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exec()`](../../../php/builtins/process/exec.md) diff --git a/docs/internals/builtins/process/exit.md b/docs/internals/builtins/process/exit.md index dbfcf2c1f1..e837336792 100644 --- a/docs/internals/builtins/process/exit.md +++ b/docs/internals/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit() — internals" description: "Compiler internals for exit(): lowering path, type checks, and runtime helpers." sidebar: - order: 306 + order: 310 --- ## `exit()` — internals @@ -28,6 +28,11 @@ function exit(int $status): void - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/exit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/exit.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exit()`](../../../php/builtins/process/exit.md) diff --git a/docs/internals/builtins/process/passthru.md b/docs/internals/builtins/process/passthru.md index 23eec13986..ccb9755993 100644 --- a/docs/internals/builtins/process/passthru.md +++ b/docs/internals/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru() — internals" description: "Compiler internals for passthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 307 + order: 311 --- ## `passthru()` — internals @@ -34,6 +34,11 @@ function passthru(string $command): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `passthru()`](../../../php/builtins/process/passthru.md) diff --git a/docs/internals/builtins/process/pclose.md b/docs/internals/builtins/process/pclose.md index b05e466ea7..1be2c744ca 100644 --- a/docs/internals/builtins/process/pclose.md +++ b/docs/internals/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose() — internals" description: "Compiler internals for pclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 308 + order: 312 --- ## `pclose()` — internals @@ -33,6 +33,11 @@ function pclose(resource $handle): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pclose()`](../../../php/builtins/process/pclose.md) diff --git a/docs/internals/builtins/process/popen.md b/docs/internals/builtins/process/popen.md index 326cd40143..045c12c63a 100644 --- a/docs/internals/builtins/process/popen.md +++ b/docs/internals/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen() — internals" description: "Compiler internals for popen(): lowering path, type checks, and runtime helpers." sidebar: - order: 309 + order: 313 --- ## `popen()` — internals @@ -33,6 +33,11 @@ function popen(string $command, string $mode): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `popen()`](../../../php/builtins/process/popen.md) diff --git a/docs/internals/builtins/process/readline.md b/docs/internals/builtins/process/readline.md index e9312ff268..f7b04ba471 100644 --- a/docs/internals/builtins/process/readline.md +++ b/docs/internals/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline() — internals" description: "Compiler internals for readline(): lowering path, type checks, and runtime helpers." sidebar: - order: 310 + order: 314 --- ## `readline()` — internals @@ -33,6 +33,11 @@ function readline(string $prompt = null): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readline()`](../../../php/builtins/process/readline.md) diff --git a/docs/internals/builtins/process/shell_exec.md b/docs/internals/builtins/process/shell_exec.md index b1453d629b..3aa70c2307 100644 --- a/docs/internals/builtins/process/shell_exec.md +++ b/docs/internals/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec() — internals" description: "Compiler internals for shell_exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 311 + order: 315 --- ## `shell_exec()` — internals @@ -32,6 +32,11 @@ function shell_exec(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `shell_exec()`](../../../php/builtins/process/shell_exec.md) diff --git a/docs/internals/builtins/process/sleep.md b/docs/internals/builtins/process/sleep.md index b3db7c1251..f1663e5541 100644 --- a/docs/internals/builtins/process/sleep.md +++ b/docs/internals/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep() — internals" description: "Compiler internals for sleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 312 + order: 316 --- ## `sleep()` — internals @@ -33,6 +33,11 @@ function sleep(int $seconds): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sleep()`](../../../php/builtins/process/sleep.md) diff --git a/docs/internals/builtins/process/system.md b/docs/internals/builtins/process/system.md index 21a4512f69..aafbdcb26b 100644 --- a/docs/internals/builtins/process/system.md +++ b/docs/internals/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system() — internals" description: "Compiler internals for system(): lowering path, type checks, and runtime helpers." sidebar: - order: 313 + order: 317 --- ## `system()` — internals @@ -33,6 +33,11 @@ function system(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/system.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `system()`](../../../php/builtins/process/system.md) diff --git a/docs/internals/builtins/process/usleep.md b/docs/internals/builtins/process/usleep.md index 7f8d0973f3..916eb82a94 100644 --- a/docs/internals/builtins/process/usleep.md +++ b/docs/internals/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep() — internals" description: "Compiler internals for usleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 314 + order: 318 --- ## `usleep()` — internals @@ -33,6 +33,11 @@ function usleep(int $microseconds): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `usleep()`](../../../php/builtins/process/usleep.md) diff --git a/docs/internals/builtins/regex/mb_ereg_match.md b/docs/internals/builtins/regex/mb_ereg_match.md index 8f04e325ff..1da091a972 100644 --- a/docs/internals/builtins/regex/mb_ereg_match.md +++ b/docs/internals/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match() — internals" description: "Compiler internals for mb_ereg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 315 + order: 319 --- ## `mb_ereg_match()` — internals @@ -35,6 +35,11 @@ function mb_ereg_match(string $pattern, string $subject, string $options = null) - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mb_ereg_match()`](../../../php/builtins/regex/mb_ereg_match.md) diff --git a/docs/internals/builtins/regex/preg_match.md b/docs/internals/builtins/regex/preg_match.md index a2fdc18aee..565ee1c920 100644 --- a/docs/internals/builtins/regex/preg_match.md +++ b/docs/internals/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match() — internals" description: "Compiler internals for preg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 316 + order: 320 --- ## `preg_match()` — internals @@ -35,6 +35,12 @@ function preg_match(string $pattern, string $subject, array $matches = []): int - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$matches`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$matches`. + ## Cross-references - [User reference for `preg_match()`](../../../php/builtins/regex/preg_match.md) diff --git a/docs/internals/builtins/regex/preg_match_all.md b/docs/internals/builtins/regex/preg_match_all.md index d04fad507c..a59224a011 100644 --- a/docs/internals/builtins/regex/preg_match_all.md +++ b/docs/internals/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all() — internals" description: "Compiler internals for preg_match_all(): lowering path, type checks, and runtime helpers." sidebar: - order: 317 + order: 321 --- ## `preg_match_all()` — internals @@ -34,6 +34,12 @@ function preg_match_all(string $pattern, string $subject): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$matches`. + ## Cross-references - [User reference for `preg_match_all()`](../../../php/builtins/regex/preg_match_all.md) diff --git a/docs/internals/builtins/regex/preg_replace.md b/docs/internals/builtins/regex/preg_replace.md index 95a4078ed0..58e9d1f106 100644 --- a/docs/internals/builtins/regex/preg_replace.md +++ b/docs/internals/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace() — internals" description: "Compiler internals for preg_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 318 + order: 322 --- ## `preg_replace()` — internals @@ -33,6 +33,11 @@ function preg_replace(string $pattern, string $replacement, string $subject): st - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_replace()`](../../../php/builtins/regex/preg_replace.md) diff --git a/docs/internals/builtins/regex/preg_replace_callback.md b/docs/internals/builtins/regex/preg_replace_callback.md index f961174aa0..f14ba6919f 100644 --- a/docs/internals/builtins/regex/preg_replace_callback.md +++ b/docs/internals/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback() — internals" description: "Compiler internals for preg_replace_callback(): lowering path, type checks, and runtime helpers." sidebar: - order: 319 + order: 323 --- ## `preg_replace_callback()` — internals @@ -33,6 +33,11 @@ function preg_replace_callback(string $pattern, callable $callback, string $subj - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_replace_callback()`](../../../php/builtins/regex/preg_replace_callback.md) diff --git a/docs/internals/builtins/regex/preg_split.md b/docs/internals/builtins/regex/preg_split.md index 50f6002e95..500812b224 100644 --- a/docs/internals/builtins/regex/preg_split.md +++ b/docs/internals/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split() — internals" description: "Compiler internals for preg_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 320 + order: 324 --- ## `preg_split()` — internals @@ -33,6 +33,11 @@ function preg_split(string $pattern, string $subject, int $limit = -1, int $flag - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_split()`](../../../php/builtins/regex/preg_split.md) diff --git a/docs/internals/builtins/spl/iterator_apply.md b/docs/internals/builtins/spl/iterator_apply.md index 289cb98666..1c78f81696 100644 --- a/docs/internals/builtins/spl/iterator_apply.md +++ b/docs/internals/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply() — internals" description: "Compiler internals for iterator_apply(): lowering path, type checks, and runtime helpers." sidebar: - order: 321 + order: 325 --- ## `iterator_apply()` — internals @@ -32,6 +32,11 @@ function iterator_apply(traversable $iterator, callable $callback, array $args = - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_apply()`](../../../php/builtins/spl/iterator_apply.md) diff --git a/docs/internals/builtins/spl/iterator_count.md b/docs/internals/builtins/spl/iterator_count.md index 40f8558a8e..d61ef84c0d 100644 --- a/docs/internals/builtins/spl/iterator_count.md +++ b/docs/internals/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count() — internals" description: "Compiler internals for iterator_count(): lowering path, type checks, and runtime helpers." sidebar: - order: 322 + order: 326 --- ## `iterator_count()` — internals @@ -32,6 +32,11 @@ function iterator_count(traversable $iterator): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_count()`](../../../php/builtins/spl/iterator_count.md) diff --git a/docs/internals/builtins/spl/iterator_to_array.md b/docs/internals/builtins/spl/iterator_to_array.md index 59ed277019..ab3f1210e2 100644 --- a/docs/internals/builtins/spl/iterator_to_array.md +++ b/docs/internals/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array() — internals" description: "Compiler internals for iterator_to_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 323 + order: 327 --- ## `iterator_to_array()` — internals @@ -32,6 +32,11 @@ function iterator_to_array(traversable $iterator, bool $preserve_keys = true): a - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_to_array()`](../../../php/builtins/spl/iterator_to_array.md) diff --git a/docs/internals/builtins/spl/spl_autoload.md b/docs/internals/builtins/spl/spl_autoload.md index db56f3faf3..4350187c46 100644 --- a/docs/internals/builtins/spl/spl_autoload.md +++ b/docs/internals/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload() — internals" description: "Compiler internals for spl_autoload(): lowering path, type checks, and runtime helpers." sidebar: - order: 324 + order: 328 --- ## `spl_autoload()` — internals @@ -32,6 +32,11 @@ function spl_autoload(string $class, string $file_extensions = null): void - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload()`](../../../php/builtins/spl/spl_autoload.md) diff --git a/docs/internals/builtins/spl/spl_autoload_call.md b/docs/internals/builtins/spl/spl_autoload_call.md index 5151fdd170..61eead1dd0 100644 --- a/docs/internals/builtins/spl/spl_autoload_call.md +++ b/docs/internals/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call() — internals" description: "Compiler internals for spl_autoload_call(): lowering path, type checks, and runtime helpers." sidebar: - order: 325 + order: 329 --- ## `spl_autoload_call()` — internals @@ -32,6 +32,11 @@ function spl_autoload_call(string $class): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_call()`](../../../php/builtins/spl/spl_autoload_call.md) diff --git a/docs/internals/builtins/spl/spl_autoload_extensions.md b/docs/internals/builtins/spl/spl_autoload_extensions.md index 82aba526c5..ca4ad5b48c 100644 --- a/docs/internals/builtins/spl/spl_autoload_extensions.md +++ b/docs/internals/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions() — internals" description: "Compiler internals for spl_autoload_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 326 + order: 330 --- ## `spl_autoload_extensions()` — internals @@ -32,6 +32,11 @@ function spl_autoload_extensions(string $file_extensions = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_extensions()`](../../../php/builtins/spl/spl_autoload_extensions.md) diff --git a/docs/internals/builtins/spl/spl_autoload_functions.md b/docs/internals/builtins/spl/spl_autoload_functions.md index 64801b7387..6b051b651b 100644 --- a/docs/internals/builtins/spl/spl_autoload_functions.md +++ b/docs/internals/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions() — internals" description: "Compiler internals for spl_autoload_functions(): lowering path, type checks, and runtime helpers." sidebar: - order: 327 + order: 331 --- ## `spl_autoload_functions()` — internals @@ -32,6 +32,11 @@ function spl_autoload_functions(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_functions()`](../../../php/builtins/spl/spl_autoload_functions.md) diff --git a/docs/internals/builtins/spl/spl_autoload_register.md b/docs/internals/builtins/spl/spl_autoload_register.md index b0d2fdee95..45e28c4dfd 100644 --- a/docs/internals/builtins/spl/spl_autoload_register.md +++ b/docs/internals/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register() — internals" description: "Compiler internals for spl_autoload_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 328 + order: 332 --- ## `spl_autoload_register()` — internals @@ -32,6 +32,11 @@ function spl_autoload_register(callable $callback = null, bool $throw = true, bo - **Arity**: takes 0–3 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_register()`](../../../php/builtins/spl/spl_autoload_register.md) diff --git a/docs/internals/builtins/spl/spl_autoload_unregister.md b/docs/internals/builtins/spl/spl_autoload_unregister.md index 6c359de4b0..615c32f85c 100644 --- a/docs/internals/builtins/spl/spl_autoload_unregister.md +++ b/docs/internals/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister() — internals" description: "Compiler internals for spl_autoload_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 329 + order: 333 --- ## `spl_autoload_unregister()` — internals @@ -32,6 +32,11 @@ function spl_autoload_unregister(callable $callback): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_unregister()`](../../../php/builtins/spl/spl_autoload_unregister.md) diff --git a/docs/internals/builtins/spl/spl_classes.md b/docs/internals/builtins/spl/spl_classes.md index 698624bcbd..625e7d2ade 100644 --- a/docs/internals/builtins/spl/spl_classes.md +++ b/docs/internals/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes() — internals" description: "Compiler internals for spl_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 330 + order: 334 --- ## `spl_classes()` — internals @@ -33,6 +33,11 @@ function spl_classes(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_classes()`](../../../php/builtins/spl/spl_classes.md) diff --git a/docs/internals/builtins/spl/spl_object_hash.md b/docs/internals/builtins/spl/spl_object_hash.md index 63ffcd5bc2..d086f1789c 100644 --- a/docs/internals/builtins/spl/spl_object_hash.md +++ b/docs/internals/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash() — internals" description: "Compiler internals for spl_object_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 331 + order: 335 --- ## `spl_object_hash()` — internals @@ -33,6 +33,11 @@ function spl_object_hash(object $object): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_object_hash()`](../../../php/builtins/spl/spl_object_hash.md) diff --git a/docs/internals/builtins/spl/spl_object_id.md b/docs/internals/builtins/spl/spl_object_id.md index 45e8512cdc..377c1ee60f 100644 --- a/docs/internals/builtins/spl/spl_object_id.md +++ b/docs/internals/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id() — internals" description: "Compiler internals for spl_object_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 332 + order: 336 --- ## `spl_object_id()` — internals @@ -33,6 +33,11 @@ function spl_object_id(object $object): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_object_id()`](../../../php/builtins/spl/spl_object_id.md) diff --git a/docs/internals/builtins/streams/fsockopen.md b/docs/internals/builtins/streams/fsockopen.md index 292ccf02fa..ff1aa20e27 100644 --- a/docs/internals/builtins/streams/fsockopen.md +++ b/docs/internals/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen() — internals" description: "Compiler internals for fsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 333 + order: 337 --- ## `fsockopen()` — internals @@ -33,6 +33,12 @@ function fsockopen(string $hostname, int $port, int $error_code = null, string $ - **Arity**: takes 2–5 arguments (3 optional). - **By-reference parameters**: `$error_code`, `$error_message`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$error_code`, `$error_message`. + ## Cross-references - [User reference for `fsockopen()`](../../../php/builtins/streams/fsockopen.md) diff --git a/docs/internals/builtins/streams/pfsockopen.md b/docs/internals/builtins/streams/pfsockopen.md index a834e18d36..22d5e53db5 100644 --- a/docs/internals/builtins/streams/pfsockopen.md +++ b/docs/internals/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen() — internals" description: "Compiler internals for pfsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 334 + order: 338 --- ## `pfsockopen()` — internals @@ -33,6 +33,12 @@ function pfsockopen(string $hostname, int $port, int $error_code = null, string - **Arity**: takes 2–5 arguments (3 optional). - **By-reference parameters**: `$error_code`, `$error_message`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$error_code`, `$error_message`. + ## Cross-references - [User reference for `pfsockopen()`](../../../php/builtins/streams/pfsockopen.md) diff --git a/docs/internals/builtins/streams/stream_bucket_append.md b/docs/internals/builtins/streams/stream_bucket_append.md index 3035499463..dc670221d4 100644 --- a/docs/internals/builtins/streams/stream_bucket_append.md +++ b/docs/internals/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append() — internals" description: "Compiler internals for stream_bucket_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 335 + order: 339 --- ## `stream_bucket_append()` — internals @@ -32,6 +32,11 @@ function stream_bucket_append(mixed $brigade, mixed $bucket): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_append()`](../../../php/builtins/streams/stream_bucket_append.md) diff --git a/docs/internals/builtins/streams/stream_bucket_prepend.md b/docs/internals/builtins/streams/stream_bucket_prepend.md index baba2e47b7..11daabb523 100644 --- a/docs/internals/builtins/streams/stream_bucket_prepend.md +++ b/docs/internals/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend() — internals" description: "Compiler internals for stream_bucket_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 336 + order: 340 --- ## `stream_bucket_prepend()` — internals @@ -32,6 +32,11 @@ function stream_bucket_prepend(mixed $brigade, mixed $bucket): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_prepend()`](../../../php/builtins/streams/stream_bucket_prepend.md) diff --git a/docs/internals/builtins/streams/stream_filter_append.md b/docs/internals/builtins/streams/stream_filter_append.md index ddfcd5a2fe..2aaf87e35a 100644 --- a/docs/internals/builtins/streams/stream_filter_append.md +++ b/docs/internals/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append() — internals" description: "Compiler internals for stream_filter_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 337 + order: 341 --- ## `stream_filter_append()` — internals @@ -32,6 +32,11 @@ function stream_filter_append(resource $stream, string $filtername, int $read_wr - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_append()`](../../../php/builtins/streams/stream_filter_append.md) diff --git a/docs/internals/builtins/streams/stream_filter_prepend.md b/docs/internals/builtins/streams/stream_filter_prepend.md index 142ff7f65d..d9c6754932 100644 --- a/docs/internals/builtins/streams/stream_filter_prepend.md +++ b/docs/internals/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend() — internals" description: "Compiler internals for stream_filter_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 338 + order: 342 --- ## `stream_filter_prepend()` — internals @@ -32,6 +32,11 @@ function stream_filter_prepend(resource $stream, string $filtername, int $read_w - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_prepend()`](../../../php/builtins/streams/stream_filter_prepend.md) diff --git a/docs/internals/builtins/string/addslashes.md b/docs/internals/builtins/string/addslashes.md index 43bdfd806f..d3d4c065ed 100644 --- a/docs/internals/builtins/string/addslashes.md +++ b/docs/internals/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes() — internals" description: "Compiler internals for addslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 339 + order: 343 --- ## `addslashes()` — internals @@ -33,6 +33,11 @@ function addslashes(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `addslashes()`](../../../php/builtins/string/addslashes.md) diff --git a/docs/internals/builtins/string/base64_decode.md b/docs/internals/builtins/string/base64_decode.md index cf5ef15416..ea182c37d1 100644 --- a/docs/internals/builtins/string/base64_decode.md +++ b/docs/internals/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode() — internals" description: "Compiler internals for base64_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 340 + order: 344 --- ## `base64_decode()` — internals @@ -33,6 +33,11 @@ function base64_decode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `base64_decode()`](../../../php/builtins/string/base64_decode.md) diff --git a/docs/internals/builtins/string/base64_encode.md b/docs/internals/builtins/string/base64_encode.md index 6592f02674..f013aefce3 100644 --- a/docs/internals/builtins/string/base64_encode.md +++ b/docs/internals/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode() — internals" description: "Compiler internals for base64_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 341 + order: 345 --- ## `base64_encode()` — internals @@ -33,6 +33,11 @@ function base64_encode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `base64_encode()`](../../../php/builtins/string/base64_encode.md) diff --git a/docs/internals/builtins/string/bin2hex.md b/docs/internals/builtins/string/bin2hex.md index f2f72cd182..bece013c4a 100644 --- a/docs/internals/builtins/string/bin2hex.md +++ b/docs/internals/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex() — internals" description: "Compiler internals for bin2hex(): lowering path, type checks, and runtime helpers." sidebar: - order: 342 + order: 346 --- ## `bin2hex()` — internals @@ -33,6 +33,11 @@ function bin2hex(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `bin2hex()`](../../../php/builtins/string/bin2hex.md) diff --git a/docs/internals/builtins/string/chop.md b/docs/internals/builtins/string/chop.md index 93247bbe08..3f797a19b2 100644 --- a/docs/internals/builtins/string/chop.md +++ b/docs/internals/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop() — internals" description: "Compiler internals for chop(): lowering path, type checks, and runtime helpers." sidebar: - order: 343 + order: 347 --- ## `chop()` — internals @@ -32,6 +32,11 @@ function chop(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): strin - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chop()`](../../../php/builtins/string/chop.md) diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index f639686c1d..818ac4cf7d 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr() — internals" description: "Compiler internals for chr(): lowering path, type checks, and runtime helpers." sidebar: - order: 344 + order: 348 --- ## `chr()` — internals @@ -33,6 +33,11 @@ function chr(int $codepoint): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chr()`](../../../php/builtins/string/chr.md) diff --git a/docs/internals/builtins/string/crc32.md b/docs/internals/builtins/string/crc32.md index ea36ecf1bf..750fd11c00 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32() — internals" description: "Compiler internals for crc32(): lowering path, type checks, and runtime helpers." sidebar: - order: 345 + order: 349 --- ## `crc32()` — internals @@ -36,6 +36,11 @@ function crc32(string $string): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `crc32()`](../../../php/builtins/string/crc32.md) diff --git a/docs/internals/builtins/string/explode.md b/docs/internals/builtins/string/explode.md index c5ea4b6468..a2427c6c00 100644 --- a/docs/internals/builtins/string/explode.md +++ b/docs/internals/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode() — internals" description: "Compiler internals for explode(): lowering path, type checks, and runtime helpers." sidebar: - order: 346 + order: 350 --- ## `explode()` — internals @@ -34,6 +34,11 @@ function explode(string $separator, string $string, int $limit = PHP_INT_MAX): a - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/explode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `explode()`](../../../php/builtins/string/explode.md) diff --git a/docs/internals/builtins/string/grapheme_strrev.md b/docs/internals/builtins/string/grapheme_strrev.md index 65d236b7bd..0a96b1f829 100644 --- a/docs/internals/builtins/string/grapheme_strrev.md +++ b/docs/internals/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev() — internals" description: "Compiler internals for grapheme_strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 347 + order: 351 --- ## `grapheme_strrev()` — internals @@ -34,6 +34,11 @@ function grapheme_strrev(string $string): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `grapheme_strrev()`](../../../php/builtins/string/grapheme_strrev.md) diff --git a/docs/internals/builtins/string/gzcompress.md b/docs/internals/builtins/string/gzcompress.md index a4c9f8033f..22cb6394ac 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress() — internals" description: "Compiler internals for gzcompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 348 + order: 352 --- ## `gzcompress()` — internals @@ -32,6 +32,11 @@ function gzcompress(string $data, int $level = -1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzcompress()`](../../../php/builtins/string/gzcompress.md) diff --git a/docs/internals/builtins/string/gzdeflate.md b/docs/internals/builtins/string/gzdeflate.md index ddaacdfbcb..adaad84833 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate() — internals" description: "Compiler internals for gzdeflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 349 + order: 353 --- ## `gzdeflate()` — internals @@ -32,6 +32,11 @@ function gzdeflate(string $data, int $level = -1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzdeflate()`](../../../php/builtins/string/gzdeflate.md) diff --git a/docs/internals/builtins/string/gzinflate.md b/docs/internals/builtins/string/gzinflate.md index 464b9ebadb..8ab991a106 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate() — internals" description: "Compiler internals for gzinflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 350 + order: 354 --- ## `gzinflate()` — internals @@ -32,6 +32,11 @@ function gzinflate(string $data, int $max_length = 0): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzinflate()`](../../../php/builtins/string/gzinflate.md) diff --git a/docs/internals/builtins/string/gzuncompress.md b/docs/internals/builtins/string/gzuncompress.md index b035b545bb..4c6c9a18be 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress() — internals" description: "Compiler internals for gzuncompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 351 + order: 355 --- ## `gzuncompress()` — internals @@ -33,6 +33,11 @@ function gzuncompress(string $data, int $max_length = 0): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzuncompress()`](../../../php/builtins/string/gzuncompress.md) diff --git a/docs/internals/builtins/string/hash.md b/docs/internals/builtins/string/hash.md index 834b5e26bc..ee1208e971 100644 --- a/docs/internals/builtins/string/hash.md +++ b/docs/internals/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash() — internals" description: "Compiler internals for hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 352 + order: 356 --- ## `hash()` — internals @@ -33,6 +33,11 @@ function hash(string $algo, string $data, bool $binary = false): string - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash()`](../../../php/builtins/string/hash.md) diff --git a/docs/internals/builtins/string/hash_algos.md b/docs/internals/builtins/string/hash_algos.md index 084f3d91f8..88c16fc3a7 100644 --- a/docs/internals/builtins/string/hash_algos.md +++ b/docs/internals/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos() — internals" description: "Compiler internals for hash_algos(): lowering path, type checks, and runtime helpers." sidebar: - order: 353 + order: 357 --- ## `hash_algos()` — internals @@ -34,6 +34,11 @@ function hash_algos(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_algos()`](../../../php/builtins/string/hash_algos.md) diff --git a/docs/internals/builtins/string/hash_copy.md b/docs/internals/builtins/string/hash_copy.md index 3388b21256..18dc0532f4 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy() — internals" description: "Compiler internals for hash_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 354 + order: 358 --- ## `hash_copy()` — internals @@ -36,6 +36,11 @@ function hash_copy(resource $context): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_copy()`](../../../php/builtins/string/hash_copy.md) diff --git a/docs/internals/builtins/string/hash_equals.md b/docs/internals/builtins/string/hash_equals.md index 0258c85aa5..1783a09e8c 100644 --- a/docs/internals/builtins/string/hash_equals.md +++ b/docs/internals/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals() — internals" description: "Compiler internals for hash_equals(): lowering path, type checks, and runtime helpers." sidebar: - order: 355 + order: 359 --- ## `hash_equals()` — internals @@ -35,6 +35,11 @@ function hash_equals(string $known_string, string $user_string): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_equals()`](../../../php/builtins/string/hash_equals.md) diff --git a/docs/internals/builtins/string/hash_final.md b/docs/internals/builtins/string/hash_final.md index a0d4e30aed..e0f6773001 100644 --- a/docs/internals/builtins/string/hash_final.md +++ b/docs/internals/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final() — internals" description: "Compiler internals for hash_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 356 + order: 360 --- ## `hash_final()` — internals @@ -33,6 +33,11 @@ function hash_final(resource $context, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_final()`](../../../php/builtins/string/hash_final.md) diff --git a/docs/internals/builtins/string/hash_hmac.md b/docs/internals/builtins/string/hash_hmac.md index eba37cd6f1..10f244fcc7 100644 --- a/docs/internals/builtins/string/hash_hmac.md +++ b/docs/internals/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac() — internals" description: "Compiler internals for hash_hmac(): lowering path, type checks, and runtime helpers." sidebar: - order: 357 + order: 361 --- ## `hash_hmac()` — internals @@ -34,6 +34,11 @@ function hash_hmac(string $algo, string $data, string $key, bool $binary = false - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_hmac()`](../../../php/builtins/string/hash_hmac.md) diff --git a/docs/internals/builtins/string/hash_init.md b/docs/internals/builtins/string/hash_init.md index b83f92ed3c..a5461157bc 100644 --- a/docs/internals/builtins/string/hash_init.md +++ b/docs/internals/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init() — internals" description: "Compiler internals for hash_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 358 + order: 362 --- ## `hash_init()` — internals @@ -33,6 +33,11 @@ function hash_init(string $algo, int $flags = 0, string $key = ''): mixed - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_init()`](../../../php/builtins/string/hash_init.md) diff --git a/docs/internals/builtins/string/hash_update.md b/docs/internals/builtins/string/hash_update.md index 7ebe1692e2..24ff79a549 100644 --- a/docs/internals/builtins/string/hash_update.md +++ b/docs/internals/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update() — internals" description: "Compiler internals for hash_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 359 + order: 363 --- ## `hash_update()` — internals @@ -33,6 +33,11 @@ function hash_update(resource $context, string $data): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_update()`](../../../php/builtins/string/hash_update.md) diff --git a/docs/internals/builtins/string/hex2bin.md b/docs/internals/builtins/string/hex2bin.md index c1d005b41a..77c3fd985a 100644 --- a/docs/internals/builtins/string/hex2bin.md +++ b/docs/internals/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin() — internals" description: "Compiler internals for hex2bin(): lowering path, type checks, and runtime helpers." sidebar: - order: 360 + order: 364 --- ## `hex2bin()` — internals @@ -33,6 +33,11 @@ function hex2bin(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hex2bin()`](../../../php/builtins/string/hex2bin.md) diff --git a/docs/internals/builtins/string/html_entity_decode.md b/docs/internals/builtins/string/html_entity_decode.md index 065da3b5ef..88220e74ca 100644 --- a/docs/internals/builtins/string/html_entity_decode.md +++ b/docs/internals/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode() — internals" description: "Compiler internals for html_entity_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 361 + order: 365 --- ## `html_entity_decode()` — internals @@ -33,6 +33,11 @@ function html_entity_decode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `html_entity_decode()`](../../../php/builtins/string/html_entity_decode.md) diff --git a/docs/internals/builtins/string/htmlentities.md b/docs/internals/builtins/string/htmlentities.md index ee3fca42d7..9146e85efa 100644 --- a/docs/internals/builtins/string/htmlentities.md +++ b/docs/internals/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities() — internals" description: "Compiler internals for htmlentities(): lowering path, type checks, and runtime helpers." sidebar: - order: 362 + order: 366 --- ## `htmlentities()` — internals @@ -40,6 +40,11 @@ function htmlentities(string $string, int $flags = 11, string $encoding = 'UTF-8 - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `htmlentities()`](../../../php/builtins/string/htmlentities.md) diff --git a/docs/internals/builtins/string/htmlspecialchars.md b/docs/internals/builtins/string/htmlspecialchars.md index eaa1be0a5b..7bebeaea61 100644 --- a/docs/internals/builtins/string/htmlspecialchars.md +++ b/docs/internals/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars() — internals" description: "Compiler internals for htmlspecialchars(): lowering path, type checks, and runtime helpers." sidebar: - order: 363 + order: 367 --- ## `htmlspecialchars()` — internals @@ -40,6 +40,11 @@ function htmlspecialchars(string $string, int $flags = 11, string $encoding = 'U - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `htmlspecialchars()`](../../../php/builtins/string/htmlspecialchars.md) diff --git a/docs/internals/builtins/string/implode.md b/docs/internals/builtins/string/implode.md index 4ed1555f22..3e2ee5168a 100644 --- a/docs/internals/builtins/string/implode.md +++ b/docs/internals/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode() — internals" description: "Compiler internals for implode(): lowering path, type checks, and runtime helpers." sidebar: - order: 364 + order: 368 --- ## `implode()` — internals @@ -32,6 +32,11 @@ function implode(string $separator, array $array = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/implode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `implode()`](../../../php/builtins/string/implode.md) diff --git a/docs/internals/builtins/string/inet_ntop.md b/docs/internals/builtins/string/inet_ntop.md index d5816bdb03..171110348c 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop() — internals" description: "Compiler internals for inet_ntop(): lowering path, type checks, and runtime helpers." sidebar: - order: 365 + order: 369 --- ## `inet_ntop()` — internals @@ -33,6 +33,11 @@ function inet_ntop(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `inet_ntop()`](../../../php/builtins/string/inet_ntop.md) diff --git a/docs/internals/builtins/string/inet_pton.md b/docs/internals/builtins/string/inet_pton.md index de8321305a..d103808624 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton() — internals" description: "Compiler internals for inet_pton(): lowering path, type checks, and runtime helpers." sidebar: - order: 366 + order: 370 --- ## `inet_pton()` — internals @@ -33,6 +33,11 @@ function inet_pton(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `inet_pton()`](../../../php/builtins/string/inet_pton.md) diff --git a/docs/internals/builtins/string/ip2long.md b/docs/internals/builtins/string/ip2long.md index 88afffbe7d..f3e2527b7a 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long() — internals" description: "Compiler internals for ip2long(): lowering path, type checks, and runtime helpers." sidebar: - order: 367 + order: 371 --- ## `ip2long()` — internals @@ -34,6 +34,11 @@ function ip2long(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ip2long()`](../../../php/builtins/string/ip2long.md) diff --git a/docs/internals/builtins/string/lcfirst.md b/docs/internals/builtins/string/lcfirst.md index b7a9e571b0..8c9e2feed1 100644 --- a/docs/internals/builtins/string/lcfirst.md +++ b/docs/internals/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst() — internals" description: "Compiler internals for lcfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 368 + order: 372 --- ## `lcfirst()` — internals @@ -33,6 +33,11 @@ function lcfirst(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lcfirst()`](../../../php/builtins/string/lcfirst.md) diff --git a/docs/internals/builtins/string/long2ip.md b/docs/internals/builtins/string/long2ip.md index 91542b8e48..ef662e94ba 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip() — internals" description: "Compiler internals for long2ip(): lowering path, type checks, and runtime helpers." sidebar: - order: 369 + order: 373 --- ## `long2ip()` — internals @@ -34,6 +34,11 @@ function long2ip(int $ip): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `long2ip()`](../../../php/builtins/string/long2ip.md) diff --git a/docs/internals/builtins/string/ltrim.md b/docs/internals/builtins/string/ltrim.md index a8424df70b..73491daf2c 100644 --- a/docs/internals/builtins/string/ltrim.md +++ b/docs/internals/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim() — internals" description: "Compiler internals for ltrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 370 + order: 374 --- ## `ltrim()` — internals @@ -32,6 +32,11 @@ function ltrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): stri - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ltrim()`](../../../php/builtins/string/ltrim.md) diff --git a/docs/internals/builtins/string/md5.md b/docs/internals/builtins/string/md5.md index 3059c302ea..9518bf8c88 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 371 + order: 375 --- ## `md5()` — internals @@ -35,6 +35,11 @@ function md5(string $string, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/md5.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `md5()`](../../../php/builtins/string/md5.md) diff --git a/docs/internals/builtins/string/nl2br.md b/docs/internals/builtins/string/nl2br.md index 46df28ab4d..f3c46ae63a 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 372 + order: 376 --- ## `nl2br()` — internals @@ -33,6 +33,11 @@ function nl2br(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `nl2br()`](../../../php/builtins/string/nl2br.md) diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index ff8bc83703..8b94b884d7 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 373 + order: 377 --- ## `number_format()` — internals @@ -33,6 +33,11 @@ function number_format(float $num, int $decimals = 0, string $decimal_separator - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `number_format()`](../../../php/builtins/string/number_format.md) diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index f79f8660ad..36de3e8129 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 374 + order: 378 --- ## `ord()` — internals @@ -32,6 +32,11 @@ function ord(string $character): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ord.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ord()`](../../../php/builtins/string/ord.md) diff --git a/docs/internals/builtins/string/printf.md b/docs/internals/builtins/string/printf.md index f8482c32da..269c1d532b 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 379 --- ## `printf()` — internals @@ -34,6 +34,12 @@ function printf(string $format, ...$values): int - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `printf()`](../../../php/builtins/string/printf.md) diff --git a/docs/internals/builtins/string/rawurldecode.md b/docs/internals/builtins/string/rawurldecode.md index 50a9b774ef..f3f2283772 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 380 --- ## `rawurldecode()` — internals @@ -33,6 +33,11 @@ function rawurldecode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rawurldecode()`](../../../php/builtins/string/rawurldecode.md) diff --git a/docs/internals/builtins/string/rawurlencode.md b/docs/internals/builtins/string/rawurlencode.md index bd08e09e31..2b5970a386 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 381 --- ## `rawurlencode()` — internals @@ -33,6 +33,11 @@ function rawurlencode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rawurlencode()`](../../../php/builtins/string/rawurlencode.md) diff --git a/docs/internals/builtins/string/rtrim.md b/docs/internals/builtins/string/rtrim.md index 170c45b958..102f293b2b 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 382 --- ## `rtrim()` — internals @@ -32,6 +32,11 @@ function rtrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): stri - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rtrim()`](../../../php/builtins/string/rtrim.md) diff --git a/docs/internals/builtins/string/sha1.md b/docs/internals/builtins/string/sha1.md index b280768e3b..92f892c5d9 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 383 --- ## `sha1()` — internals @@ -34,6 +34,11 @@ function sha1(string $string, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sha1()`](../../../php/builtins/string/sha1.md) diff --git a/docs/internals/builtins/string/sprintf.md b/docs/internals/builtins/string/sprintf.md index 8b4727946b..1ad8926087 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 384 --- ## `sprintf()` — internals @@ -34,6 +34,12 @@ function sprintf(string $format, ...$values): string - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `sprintf()`](../../../php/builtins/string/sprintf.md) diff --git a/docs/internals/builtins/string/sscanf.md b/docs/internals/builtins/string/sscanf.md index f0201d4d3f..05bf35a145 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 385 --- ## `sscanf()` — internals @@ -35,6 +35,12 @@ function sscanf(string $string, string $format, ...$vars): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `sscanf()`](../../../php/builtins/string/sscanf.md) diff --git a/docs/internals/builtins/string/str_contains.md b/docs/internals/builtins/string/str_contains.md index 57903400db..985af2503c 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 386 --- ## `str_contains()` — internals @@ -33,6 +33,11 @@ function str_contains(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_contains()`](../../../php/builtins/string/str_contains.md) diff --git a/docs/internals/builtins/string/str_ends_with.md b/docs/internals/builtins/string/str_ends_with.md index 73c958d7ff..7751e3a6a3 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 387 --- ## `str_ends_with()` — internals @@ -33,6 +33,11 @@ function str_ends_with(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_ends_with()`](../../../php/builtins/string/str_ends_with.md) diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index 452c06ba77..3f1f7ba9be 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 388 --- ## `str_ireplace()` — internals @@ -32,6 +32,11 @@ function str_ireplace(string $search, string $replace, string $subject, int $cou - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_ireplace()`](../../../php/builtins/string/str_ireplace.md) diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index 185165ee14..d26a219b72 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 389 --- ## `str_pad()` — internals @@ -33,6 +33,11 @@ function str_pad(string $string, int $length, string $pad_string = ' ', int $pad - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_pad()`](../../../php/builtins/string/str_pad.md) diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index 74d8e41ef5..1066f3f0e1 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 390 --- ## `str_repeat()` — internals @@ -33,6 +33,11 @@ function str_repeat(string $string, int $times): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_repeat()`](../../../php/builtins/string/str_repeat.md) diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 41d9bd924a..1c8c2c06ef 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 391 --- ## `str_replace()` — internals @@ -32,6 +32,11 @@ function str_replace(string $search, string $replace, string $subject, int $coun - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_replace()`](../../../php/builtins/string/str_replace.md) diff --git a/docs/internals/builtins/string/str_split.md b/docs/internals/builtins/string/str_split.md index 8ce8559196..67b26a11c7 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 392 --- ## `str_split()` — internals @@ -33,6 +33,11 @@ function str_split(string $string, int $length = 1): array - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_split()`](../../../php/builtins/string/str_split.md) diff --git a/docs/internals/builtins/string/str_starts_with.md b/docs/internals/builtins/string/str_starts_with.md index 5ec7f6574c..ab14e8de86 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 393 --- ## `str_starts_with()` — internals @@ -33,6 +33,11 @@ function str_starts_with(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_starts_with()`](../../../php/builtins/string/str_starts_with.md) diff --git a/docs/internals/builtins/string/strcasecmp.md b/docs/internals/builtins/string/strcasecmp.md index 94012e8b41..7677ab0473 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 394 --- ## `strcasecmp()` — internals @@ -33,6 +33,11 @@ function strcasecmp(string $string1, string $string2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strcasecmp()`](../../../php/builtins/string/strcasecmp.md) diff --git a/docs/internals/builtins/string/strcmp.md b/docs/internals/builtins/string/strcmp.md index 98682318f4..fef1c20878 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 395 --- ## `strcmp()` — internals @@ -33,6 +33,11 @@ function strcmp(string $string1, string $string2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strcmp()`](../../../php/builtins/string/strcmp.md) diff --git a/docs/internals/builtins/string/stripslashes.md b/docs/internals/builtins/string/stripslashes.md index 9b04fd06c5..0fd90ea4a7 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 396 --- ## `stripslashes()` — internals @@ -33,6 +33,11 @@ function stripslashes(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stripslashes()`](../../../php/builtins/string/stripslashes.md) diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 34232da6d5..3a9144829b 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 397 --- ## `strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:494](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L494) (`lower_strlen`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1079](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1079) (`lower_strlen`) - **Function symbol**: `lower_strlen()` @@ -33,6 +33,11 @@ function strlen(string $string): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strlen()`](../../../php/builtins/string/strlen.md) diff --git a/docs/internals/builtins/string/strpos.md b/docs/internals/builtins/string/strpos.md index d659c06081..e8b8c04234 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 398 --- ## `strpos()` — internals @@ -32,6 +32,11 @@ function strpos(string $haystack, string $needle, int $offset = 0): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strpos()`](../../../php/builtins/string/strpos.md) diff --git a/docs/internals/builtins/string/strrev.md b/docs/internals/builtins/string/strrev.md index e660f9b8a0..3b6af20fae 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 399 --- ## `strrev()` — internals @@ -33,6 +33,11 @@ function strrev(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strrev()`](../../../php/builtins/string/strrev.md) diff --git a/docs/internals/builtins/string/strrpos.md b/docs/internals/builtins/string/strrpos.md index 2e112a52a5..b9ef683672 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 400 --- ## `strrpos()` — internals @@ -32,6 +32,11 @@ function strrpos(string $haystack, string $needle, int $offset = 0): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strrpos()`](../../../php/builtins/string/strrpos.md) diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index 3dc79f9836..2162c7e3c6 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 401 --- ## `strstr()` — internals @@ -32,6 +32,11 @@ function strstr(string $haystack, string $needle, bool $before_needle = false): - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strstr()`](../../../php/builtins/string/strstr.md) diff --git a/docs/internals/builtins/string/strtolower.md b/docs/internals/builtins/string/strtolower.md index 3a75ab8ee6..f7598055a4 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 402 --- ## `strtolower()` — internals @@ -33,6 +33,11 @@ function strtolower(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtolower()`](../../../php/builtins/string/strtolower.md) diff --git a/docs/internals/builtins/string/strtoupper.md b/docs/internals/builtins/string/strtoupper.md index a20e0d5d0e..3e1b44f203 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 403 --- ## `strtoupper()` — internals @@ -33,6 +33,11 @@ function strtoupper(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtoupper()`](../../../php/builtins/string/strtoupper.md) diff --git a/docs/internals/builtins/string/substr.md b/docs/internals/builtins/string/substr.md index ddaf9cb105..1dd8c158ab 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 404 --- ## `substr()` — internals @@ -33,6 +33,11 @@ function substr(string $string, int $offset, int $length = null): string - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `substr()`](../../../php/builtins/string/substr.md) diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index dd5bf4a2d1..7e8af23a91 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 405 --- ## `substr_replace()` — internals @@ -34,6 +34,11 @@ function substr_replace(string $string, string $replace, int $offset, int $lengt - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `substr_replace()`](../../../php/builtins/string/substr_replace.md) diff --git a/docs/internals/builtins/string/trim.md b/docs/internals/builtins/string/trim.md index 1167a00126..c4642fdbec 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 406 --- ## `trim()` — internals @@ -32,6 +32,11 @@ function trim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): strin - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/trim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `trim()`](../../../php/builtins/string/trim.md) diff --git a/docs/internals/builtins/string/ucfirst.md b/docs/internals/builtins/string/ucfirst.md index f1c7c5bc2d..8ada62280e 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 407 --- ## `ucfirst()` — internals @@ -33,6 +33,11 @@ function ucfirst(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ucfirst()`](../../../php/builtins/string/ucfirst.md) diff --git a/docs/internals/builtins/string/ucwords.md b/docs/internals/builtins/string/ucwords.md index c30c6a72a8..a8c2c0e952 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 408 --- ## `ucwords()` — internals @@ -33,6 +33,11 @@ function ucwords(string $string, string $separators = ' \t\r\n\x0c\x0b'): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ucwords()`](../../../php/builtins/string/ucwords.md) diff --git a/docs/internals/builtins/string/urldecode.md b/docs/internals/builtins/string/urldecode.md index dd47b4404c..3f34e71607 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 409 --- ## `urldecode()` — internals @@ -33,6 +33,11 @@ function urldecode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `urldecode()`](../../../php/builtins/string/urldecode.md) diff --git a/docs/internals/builtins/string/urlencode.md b/docs/internals/builtins/string/urlencode.md index 28031967cd..a796b83f6a 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 410 --- ## `urlencode()` — internals @@ -33,6 +33,11 @@ function urlencode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `urlencode()`](../../../php/builtins/string/urlencode.md) diff --git a/docs/internals/builtins/string/vprintf.md b/docs/internals/builtins/string/vprintf.md index bea0e79a04..cde64be266 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 411 --- ## `vprintf()` — internals @@ -33,6 +33,11 @@ function vprintf(string $format, array $values): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vprintf()`](../../../php/builtins/string/vprintf.md) diff --git a/docs/internals/builtins/string/vsprintf.md b/docs/internals/builtins/string/vsprintf.md index b85486eb0a..a2a594bb37 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 412 --- ## `vsprintf()` — internals @@ -33,6 +33,11 @@ function vsprintf(string $format, array $values): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vsprintf()`](../../../php/builtins/string/vsprintf.md) diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 13177007f7..3f1dc4258e 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 413 --- ## `wordwrap()` — internals @@ -34,6 +34,11 @@ function wordwrap(string $string, int $width = 75, string $break = '\n', bool $c - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `wordwrap()`](../../../php/builtins/string/wordwrap.md) diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 13f9260588..f6c9053f32 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 414 --- ## `boolval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:587](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L587) (`lower_boolval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1176](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1176) (`lower_boolval`) - **Function symbol**: `lower_boolval()` @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_mixed_cast_bool` ## Signature summary @@ -32,6 +33,11 @@ function boolval(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `boolval()`](../../../php/builtins/type/boolval.md) diff --git a/docs/internals/builtins/type/ctype_alnum.md b/docs/internals/builtins/type/ctype_alnum.md index d07a19941d..ee8a26ba95 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 415 --- ## `ctype_alnum()` — internals @@ -32,6 +32,11 @@ function ctype_alnum(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_alnum()`](../../../php/builtins/type/ctype_alnum.md) diff --git a/docs/internals/builtins/type/ctype_alpha.md b/docs/internals/builtins/type/ctype_alpha.md index 07994a87f3..c029edcf71 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 416 --- ## `ctype_alpha()` — internals @@ -32,6 +32,11 @@ function ctype_alpha(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_alpha()`](../../../php/builtins/type/ctype_alpha.md) diff --git a/docs/internals/builtins/type/ctype_digit.md b/docs/internals/builtins/type/ctype_digit.md index 55ca4af83e..dc9cd2cf56 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 417 --- ## `ctype_digit()` — internals @@ -32,6 +32,11 @@ function ctype_digit(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_digit()`](../../../php/builtins/type/ctype_digit.md) diff --git a/docs/internals/builtins/type/ctype_space.md b/docs/internals/builtins/type/ctype_space.md index dff9389a31..47ddb640ac 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 418 --- ## `ctype_space()` — internals @@ -32,6 +32,11 @@ function ctype_space(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_space()`](../../../php/builtins/type/ctype_space.md) diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 70d1628b01..9d7b91bb1d 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 419 --- ## `floatval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:557](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L557) (`lower_floatval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1142](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1142) (`lower_floatval`) - **Function symbol**: `lower_floatval()` @@ -21,6 +21,7 @@ sidebar: ## Runtime helpers The following runtime helpers are referenced: +- `__rt_mixed_cast_float` - `__rt_str_to_number` ## Signature summary @@ -33,6 +34,11 @@ function floatval(mixed $value): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `floatval()`](../../../php/builtins/type/floatval.md) diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index 7235132ff7..68aeaa0a4c 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 416 + order: 420 --- ## `get_resource_id()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_id.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:424](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L424) (`lower_get_resource_id`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:431](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L431) (`lower_get_resource_id`) - **Function symbol**: `lower_get_resource_id()` @@ -32,6 +32,11 @@ function get_resource_id(resource $resource): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_resource_id()`](../../../php/builtins/type/get_resource_id.md) diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index b76f7d6a3b..a40b2cb24f 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 417 + order: 421 --- ## `get_resource_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_type.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:412](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L412) (`lower_get_resource_type`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L419) (`lower_get_resource_type`) - **Function symbol**: `lower_get_resource_type()` @@ -32,6 +32,11 @@ function get_resource_type(resource $resource): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_resource_type()`](../../../php/builtins/type/get_resource_type.md) diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index e129b63f73..6940b55ff1 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 422 --- ## `gettype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/gettype.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:129](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L129) (`lower_gettype`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L419) (`lower_gettype`) - **Function symbol**: `lower_gettype()` @@ -32,6 +32,11 @@ function gettype(mixed $value): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gettype()`](../../../php/builtins/type/gettype.md) diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index f092b4e091..a7f42ca644 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 423 --- ## `intval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:524](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L524) (`lower_intval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1109](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1109) (`lower_intval`) - **Function symbol**: `lower_intval()` @@ -34,6 +34,11 @@ function intval(mixed $value): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/intval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `intval()`](../../../php/builtins/type/intval.md) diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 01835551a7..ebd1fa9a72 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 424 --- ## `is_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1004](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1004) (`lower_is_array`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1603](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1603) (`lower_is_array`) - **Function symbol**: `lower_is_array()` @@ -34,6 +34,11 @@ function is_array(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_array()`](../../../php/builtins/type/is_array.md) diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index 625121835e..4a9b9ba6cf 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 425 --- ## `is_bool()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` @@ -32,6 +32,11 @@ function is_bool(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_bool()`](../../../php/builtins/type/is_bool.md) diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index cb13a9c333..fe038fea6d 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 426 --- ## `is_callable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:319](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L319) (`lower_is_callable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:712](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L712) (`lower_is_callable`) - **Function symbol**: `lower_is_callable()` @@ -23,7 +23,6 @@ sidebar: The following runtime helpers are referenced: - `__rt_is_callable_array` - `__rt_is_callable_assoc` -- `__rt_is_callable_object` ## Signature summary @@ -35,6 +34,12 @@ function is_callable(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$callable_name`. + ## Cross-references - [User reference for `is_callable()`](../../../php/builtins/type/is_callable.md) diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index e9c07a96bc..4649c86f79 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 427 --- ## `is_float()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` @@ -32,6 +32,11 @@ function is_float(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_float()`](../../../php/builtins/type/is_float.md) diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 6c7ab32a8e..ac685e9c43 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 428 --- ## `is_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` @@ -32,6 +32,11 @@ function is_int(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_int()`](../../../php/builtins/type/is_int.md) diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index 611f2fa937..2ab3517822 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 429 --- ## `is_iterable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:795](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L795) (`lower_is_iterable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1394](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1394) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` @@ -32,6 +32,11 @@ function is_iterable(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_iterable()`](../../../php/builtins/type/is_iterable.md) diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index c7a1b70df9..b9dba16781 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 430 --- ## `is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:994](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L994) (`lower_is_null_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1593](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1593) (`lower_is_null_builtin`) - **Function symbol**: `lower_is_null_builtin()` @@ -32,6 +32,11 @@ function is_null(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_null()`](../../../php/builtins/type/is_null.md) diff --git a/docs/internals/builtins/type/is_numeric.md b/docs/internals/builtins/type/is_numeric.md index 63460e474e..fb1de2f6b3 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 431 --- ## `is_numeric()` — internals @@ -32,6 +32,11 @@ function is_numeric(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_numeric()`](../../../php/builtins/type/is_numeric.md) diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index 700c2004d2..b6cb3a488c 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 432 --- ## `is_object()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1019](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1019) (`lower_is_object`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1618](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1618) (`lower_is_object`) - **Function symbol**: `lower_is_object()` @@ -33,6 +33,11 @@ function is_object(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_object()`](../../../php/builtins/type/is_object.md) diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 50ef78f7a6..25cb576ab1 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 433 --- ## `is_resource()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_resource.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:400](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L400) (`lower_is_resource`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:407](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L407) (`lower_is_resource`) - **Function symbol**: `lower_is_resource()` @@ -32,6 +32,11 @@ function is_resource(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_resource()`](../../../php/builtins/type/is_resource.md) diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index e02f6771df..3bf4b2e635 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 434 --- ## `is_scalar()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1035](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1035) (`lower_is_scalar`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1634](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1634) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` @@ -34,6 +34,11 @@ function is_scalar(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_scalar()`](../../../php/builtins/type/is_scalar.md) diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index c9906e7c32..3eeaf38330 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 435 --- ## `is_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` @@ -32,6 +32,11 @@ function is_string(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_string()`](../../../php/builtins/type/is_string.md) diff --git a/docs/internals/builtins/type/settype.md b/docs/internals/builtins/type/settype.md index 4fb5b65b76..91a22393a9 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 432 + order: 436 --- ## `settype()` — internals @@ -33,6 +33,12 @@ function settype(mixed $var, string $type): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$var`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/settype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$var`. + ## Cross-references - [User reference for `settype()`](../../../php/builtins/type/settype.md) diff --git a/docs/internals/eval-runtime.md b/docs/internals/eval-runtime.md new file mode 100644 index 0000000000..1434dba62b --- /dev/null +++ b/docs/internals/eval-runtime.md @@ -0,0 +1,197 @@ +--- +title: "Eval Runtime Architecture" +description: "How literal eval is planned for AOT lowering, when Magician is linked, and how native and interpreted scopes share values." +sidebar: + order: 9 +--- + +**Sources:** `src/eval_aot.rs`, `src/ir_lower/expr/mod.rs`, +`src/ir_lower/program.rs`, `src/codegen/lower_inst/builtins/eval.rs`, +`src/codegen_support/runtime/eval_bridge.rs`, +`src/codegen_support/runtime/eval_scope.rs`, and +`crates/elephc-magician/`. + +Eval is an experimental hybrid feature. Ordinary elephc source and eligible +literal eval fragments are compiled ahead of time. Only fragments that require +runtime parsing or dynamic symbol behavior use the optional Magician +interpreter. The resulting executable remains standalone: Magician is a static +bridge library, not an external PHP or Zend dependency. + +For PHP-visible behavior and the supported fragment subset, see +[Eval](../php/eval.md). This page documents the compiler/runtime boundary. + +## Pipeline position + +`eval` is not a dedicated lexer token or AST node. The parser produces an +ordinary `ExprKind::FunctionCall`; later semantic passes recognize the +case-insensitive PHP language-construct name. + +```text +eval($code) + -> checker: exactly one argument, result Mixed, conservative barrier + -> EIR lowering: BuiltinCall or EvalLiteralCall + -> literal planner, when the source is statically known + -> direct/native EIR + -> EIR plus core eval-scope helpers + -> Magician interpreter fallback + -> target-aware assembly and optional bridge linking +``` + +The checker and AST optimizer deliberately stay conservative even for a source +literal. The more precise AOT decision happens during EIR lowering, after the +front end has already preserved PHP's dynamic semantics and diagnostics. + +## Execution-path decision + +| Path | Typical input | Runtime requirements | +|---|---|---| +| Direct AOT | A literal fragment whose operations and caller-local reads/writes can be lowered statically | No eval context, scope, or Magician library | +| Scope-backed AOT | A literal fragment that is statically lowerable but needs materialized known scope values | Core `eval_scope` runtime feature; no interpreter library | +| Interpreter fallback | A dynamic string or a literal requiring dynamic declarations, includes, references, dynamic calls, or another unsupported AOT shape | `eval_bridge`, synchronized scopes, PCRE2, and `elephc_magician` | + +`src/eval_aot.rs` parses literal fragments at compile time, applies call-site +magic-constant metadata, records known scope reads and writes, and produces an +`EvalAotPlan`. A plan can contain a fully static EIR body, a scope-read EIR +body, or a conservative fallback reason. + +Current fallback classes include parse failures, `include`/`require`, runtime +declarations, global/static scope, references and by-reference operations, +dynamic calls or class/member resolution, unsupported object/array/iterable +shapes, `try`/`throw`, unsupported control flow, and unsupported static calls. +Eligibility is intentionally conservative: a fragment falls back rather than +being partially compiled with different observable behavior. + +`src/ir_lower/program.rs` repeats the final bridge-requirement check against the +completed EIR module. This accounts for actual local-slot types and supported +static function/method targets before setting `RuntimeFeatures::eval_bridge`. +Consequently, the presence of `EvalLiteralCall` alone does not imply that the +binary links Magician. + +## EIR representation + +Literal calls use `EvalLiteralCall`, carrying the fragment in the module data +pool. Dynamic calls remain ordinary builtin calls until the eval lowering path +materializes the runtime code string. + +The eval-specific EIR operations are: + +| Operation | Responsibility | +|---|---| +| `EvalLiteralCall` | Preserve a literal fragment for AOT planning or interpreter fallback. | +| `EvalScopeGet`, `EvalScopeSet` | Read or update a named boxed cell in a materialized eval scope. | +| `EvalFunctionCall`, `EvalFunctionCallArray` | Dispatch a function created or registered in the persistent eval context. | +| `EvalFunctionExists`, `EvalClassExists` | Probe dynamic symbols that may have been created by an earlier eval barrier. | +| `EvalConstantExists`, `EvalConstantFetch` | Probe or fetch constants retained in the eval context. | +| `EvalObjectNew` | Construct a class that may have been declared at runtime. | +| `EvalStaticMethodCall` | Dispatch a static method whose target may come from eval metadata. | + +The conservative default effects for eval calls include arbitrary observable +call effects. Symbol probes read global state; scope access reads or writes heap +state and can fail; constant fetches also carry ownership/refcount effects. +Later lowering may refine a literal call after the AOT plan proves a narrower +path. + +Three addressable local kinds hold eval state when required: + +| `LocalKind` | Lifetime and role | +|---|---| +| `EvalContext` | Persistent Magician context for declarations, constants, callable metadata, and interpreter state. Its presence requires the full bridge. | +| `EvalScope` | Materialized activation/closure scope shared with the executing fragment. | +| `EvalGlobalScope` | Materialized program-global scope used by `global` aliases and CLI argument globals. | + +Frame sizing and cleanup see these slots before assembly emission. A direct AOT +fragment does not declare them merely because the source contains `eval()`. + +## Checker and optimizer barrier + +The type checker enforces exactly one argument, infers that argument for its +side effects, and gives the call the static type `Mixed`. After the call it: + +- marks the active statement stream as having crossed eval; +- widens known local types to `Mixed`; +- drops closure, callable-signature, capture, and callable-target facts; +- permits later reads of variables and dynamic symbols that eval may have + created. + +AST constant propagation reports `Invalidation::All` for eval. This prevents a +pre-call constant or alias fact from being reused after code that can create, +overwrite, or unset caller-visible state. EIR planning may later omit the +physical runtime barrier for a proven literal path without weakening those +front-end safety rules. + +## Dynamic bridge lifecycle + +When interpreter fallback is required, generated code performs these steps: + +1. Coerce the code argument to a PHP string. +2. Lazily allocate the `EvalContext`, activation `EvalScope`, and, when needed, + `EvalGlobalScope`. +3. Register bridge-compatible AOT functions, methods, constructors, class + metadata, parameter names, defaults, and visibility information. +4. Flush visible locals, by-reference cells, closure captures, and eligible + globals into boxed scope cells. +5. Set call-site file, directory, namespace, class, trait, function, and method + metadata used by magic constants. +6. Call `__elephc_eval_execute` through the target-aware ABI. +7. Reload dirty, created, or unset scope entries and propagate return/fatal/ + throwable state through the normal generated runtime paths. + +Top-level scope setup also seeds `$argc` and `$argv`. Function fragments can +bind those values or compiler-known program globals with PHP `global` aliases. +By-value closure captures synchronize only their captured copy; by-reference +captures share a ref cell whose storage is widened before it crosses the +barrier when necessary. + +## Shared value ABI and ownership + +Magician does not introduce a second PHP value layout. The generated runtime +exports C-ABI hooks that box, unbox, retain, release, compare, cast, iterate, +and mutate the same `Mixed` cells used by native code. Array writes still pass +through the normal copy-on-write helpers, and object/class operations reuse +generated metadata when the bridge shape is supported. + +Scope setters retain the value stored in the context; getters return values +with the ownership expected by their EIR result. Normal returns, runtime +fatals, thrown values, early fragment returns, and function cleanup must all +balance those cells. Persistent declarations and metadata live in the eval +context until its owning generated function or process scope is destroyed. + +## Parsing and cache + +Dynamic source is parsed into Magician's immutable EvalIR. The process-wide +parse cache stores both successful parse results and parse errors by exact +source bytes: + +- FIFO capacity: 256 distinct fragments; +- maximum cacheable fragment size: 64 KiB; +- cached data: immutable `Arc` or `EvalParseError` only; +- excluded data: scopes, cells, declarations, context, and call-site magic + constant values. + +The same cache is used by the public eval FFI entry and nested eval/include +execution. Large one-off fragments bypass it instead of occupying global cache +capacity. + +## Linking and targets + +`RuntimeFeatures::eval_scope` emits only the core scope helpers. +`RuntimeFeatures::eval_bridge` additionally links PCRE2 and +`libelephc_magician.a`. The bridge is registered in `src/linker.rs` as +`--with-eval` with the optional `ELEPHC_MAGICIAN_LIB_DIR` archive-directory +override. Normal compilation derives the feature automatically; `--with-eval` +force-loads the archive and increases binary size but does not alter AOT +eligibility. + +All eval lowering and bridge ABI paths are target-aware and covered by +dedicated integration shards on macOS ARM64, Linux ARM64, and Linux x86_64. +Shared lowering must use the existing ABI helpers rather than assume a specific +register set or object format. + +## Documentation ownership + +The exhaustive language subset, reflection behavior, builtins, and known gaps +belong in [Eval](../php/eval.md). Per-builtin AOT/eval availability is generated +from the `builtin!` and `eval_builtin!` registries in the +[Builtin Reference](../php/builtins.md). [The Runtime](the-runtime.md) documents +the shared `__rt_*` assembly families and links here for the optional eval +boundary. diff --git a/docs/internals/how-elephc-works.md b/docs/internals/how-elephc-works.md index efe80d9024..6e566acb82 100644 --- a/docs/internals/how-elephc-works.md +++ b/docs/internals/how-elephc-works.md @@ -355,7 +355,8 @@ In normal compile mode, the toolchain flow is: 2. Write the program assembly to `file.s` 3. Optionally write `file.map` 4. Assemble `file.s` into `file.o` -5. Link `file.o` together with the cached runtime object into the final executable +5. Link `file.o` together with the cached runtime object, any required optional + bridge archives, and system libraries into the final executable If `--timings` is enabled, elephc prints the duration of each major phase to stderr so you can see where time is being spent. @@ -371,7 +372,7 @@ ld -arch arm64 -e _main -o file file.o -lSystem -syslibroot /path/to/sdk On Linux, elephc invokes the native assembler/linker for the requested target. - **`as`** (assembler) converts the user assembly text mnemonics into binary machine code, producing an object file (`.o`) -- **`ld`** (linker) resolves label addresses, links the user object together with the cached runtime object and any requested system libraries, and produces the final native executable (Mach-O on macOS, ELF on Linux) +- **`ld`** (linker) resolves label addresses, links the user object together with the cached runtime object, required bridge staticlibs, and requested system libraries, and produces the final native executable (Mach-O on macOS, ELF on Linux) The `.o` file is deleted after linking. The result is a standalone executable. @@ -384,7 +385,7 @@ With `--emit cdylib` the same flow produces a shared library instead: codegen em big ``` -The binary runs directly on the CPU. There is no PHP interpreter or VM at runtime. The kernel loads the executable for the target platform into memory, jumps to the entry point, and the CPU executes the instructions we generated. The binary still contains elephc's emitted helper routines and links the platform's system libraries for OS/libc services. +The binary runs directly on the CPU and has no PHP, Zend Engine, or external VM dependency. The kernel loads the executable for the target platform into memory, jumps to the entry point, and the CPU executes the instructions we generated. The binary still contains elephc's emitted helper routines and links the platform's system libraries for OS/libc services. If experimental dynamic `eval()` is required, the optional Magician interpreter is statically embedded and handles only those fragments; eligible literal eval stays in native EIR. ## The complete flow @@ -441,7 +442,7 @@ The binary runs directly on the CPU. There is no PHP interpreter or VM at runtim file.o (machine code bytes for user program) │ ▼ ld (linker) - file (user object + cached runtime object) + file (user object + cached runtime object + optional bridge staticlibs) │ ▼ CPU "big\n" diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index 782d87dac6..f3fa075f31 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -2,7 +2,7 @@ title: "Memory Model" description: "Stack frames, heap allocation, and memory management." sidebar: - order: 9 + order: 10 --- elephc manages memory without calling `malloc`/`free` for PHP values directly. Storage lives on the **stack** (automatic, per-function), in fixed BSS regions, or in a compiler-managed **heap buffer** with a free-list allocator, reference counting, and a targeted cycle collector for array/hash/object graphs. The final binary still links the target platform's system C library for OS and libc services. diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 4af8188e3a..e9886e4a2d 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -6,8 +6,9 @@ sidebar: --- **Source:** assembly emitter `src/codegen/`; EIR lowering `src/ir_lower/`; -IR model and validation `src/ir/`; shared runtime/ABI support -`src/codegen_support/`. +literal eval planning `src/eval_aot.rs`; IR model and validation `src/ir/`; +shared runtime/ABI support `src/codegen_support/`; optional bridge crates under +`crates/`. Codegen is a single EIR pipeline. The checked and optimized AST is always lowered into EIR, IR passes run over that module, and `src/codegen/` emits the @@ -46,6 +47,7 @@ and link the resulting user object against the cached runtime object. | `src/codegen/mod.rs` | Public codegen facade, EIR backend entry points, runtime metadata finalization | | `src/codegen/block_emit.rs` | Function/block traversal, prologues, top-level entry and deferred EIR wrappers | | `src/codegen/lower_inst.rs`, `src/codegen/lower_inst/` | Instruction lowering and builtin-specific EIR emission | +| `src/codegen/lower_inst/builtins/eval.rs` | Literal-eval AOT bodies, scope synchronization, bridge calls, and dynamic post-barrier dispatch | | `src/codegen/lower_term.rs` | Terminator lowering for returns, branches, switches, and unreachable paths | | `src/codegen/context.rs` | EIR function emission state and value materialization helpers | | `src/codegen/frame.rs` | Stack-frame sizing, local slots, register allocation integration | @@ -56,6 +58,7 @@ and link the resulting user object against the cached runtime object. | `src/codegen_support/value_boxing.rs` | Shared scalar/string/array/object/iterable boxing into runtime `Mixed` cells | | `src/codegen_support/wrappers/` | Shared callback and fiber wrapper emitters used by deferred EIR wrapper emission | | `src/codegen_support/runtime/` | Shared `__rt_*` routines and runtime data emission | +| `src/codegen_support/runtime/eval_bridge.rs`, `eval_scope.rs` | C-ABI value hooks and core materialized-scope helpers for eval | | `src/codegen_support/platform/` | Target descriptions and assembler/linker naming conventions | The active backend must remain target-aware. New lowering paths should use the @@ -63,17 +66,40 @@ ABI helpers instead of hardcoding AArch64 or x86_64 register and stack details. ## Runtime Split -Codegen produces two linked artifacts: +Codegen always produces two compiler-owned artifacts: 1. **User assembly** from `src/codegen/`, containing lowered PHP functions, methods, top-level entry code, user metadata, and literal data. 2. **Runtime object** from `src/codegen_support/runtime/`, cached by compiler version, target, heap size, runtime features, and PIC mode. +The linker may also add optional bridge staticlibs as a third class of input. +For example, dynamic eval links `libelephc_magician.a`; fully AOT literal eval +does not. Bridge archives are discovered and linked by `src/linker.rs` rather +than emitted by the assembly backend. + Runtime feature selection is derived from the EIR module plus CLI-owned modes such as `--web`. This keeps ordinary binaries from carrying unused helper families while preserving deterministic linking. +## Eval Lowering Boundary + +Literal `eval()` calls reach EIR as `EvalLiteralCall`. The shared planner in +`src/eval_aot.rs` classifies the fragment before target assembly is chosen: + +1. Direct-local and fully static plans become native instructions or internal + EIR functions and require no eval runtime state. +2. Scope-backed AOT plans use `EvalScopeGet`/`EvalScopeSet` and enable only the + core `eval_scope` runtime feature. +3. Dynamic or unsupported plans materialize `EvalContext`, `EvalScope`, and + optional `EvalGlobalScope` slots, call the Magician C ABI, and enable + `eval_bridge`. + +The lowerer preserves PHP source evaluation order before ABI materialization, +boxes values into the shared `Mixed` cell representation, and uses the normal +target-aware call helpers on macOS ARM64, Linux ARM64, and Linux x86_64. See +[Eval Runtime Architecture](eval-runtime.md) for the complete boundary. + ## Emit Modes `--emit executable` is the default and emits a process entry point. `--emit diff --git a/docs/internals/the-ir.md b/docs/internals/the-ir.md index 28a40ed833..2066a7cc31 100644 --- a/docs/internals/the-ir.md +++ b/docs/internals/the-ir.md @@ -2,7 +2,7 @@ title: "The EIR Design" description: "Specification for elephc's default intermediate representation between AST optimization and assembly emission." sidebar: - order: 13 + order: 14 --- **Status:** EIR is the canonical compiler IR and backend contract for v1.0. @@ -10,8 +10,6 @@ The diagnostic `--emit-ir` path lowers the checked and optimized AST into validated textual EIR, and normal executable/cdylib builds lower that same EIR into assembly. -**Implementation phases:** `.plans/eir-*.md` - **Authoritative source audit for this spec:** - `src/types/model.rs` @@ -251,17 +249,24 @@ pub enum LocalKind { StaticLocal, RefCell, HiddenTemp, + OwnedTemp, TryHandler, ClosureCapture, NamedArgTemp, IteratorState, GeneratorState, + EvalContext, + EvalScope, + EvalGlobalScope, } ``` `LocalSlot` exists even in SSA form because PHP locals, globals, statics, references, try handlers, and hidden temporaries have addressable storage or -observable lifetime behavior. +observable lifetime behavior. `OwnedTemp` carries cleanup-owning temporaries; +the three eval slot kinds hold the optional interpreter context, activation +scope, and program-global scope. Fully native literal eval paths do not declare +those slots. ## Value Ownership @@ -565,6 +570,26 @@ observable order: 4. Materialize ABI parameters in callee signature order in the backend. 5. Avoid temp preevaluation for ref-like and mutating parameters. +### Eval and Dynamic Symbols + +| Op | Operands | Result | Effects | +|---|---|---|---| +| `EvalLiteralCall` | literal data id plus lowered call operands | `Heap(Mixed)` | conservative call effects until literal planning refines the path | +| `EvalScopeGet` | scope handle plus global-name immediate | boxed value | `reads_heap`, `may_fatal` | +| `EvalScopeSet` | scope handle and value plus global-name immediate | `Void` | `reads_heap`, `writes_heap`, `refcount_op`, `may_fatal` | +| `EvalFunctionCall` | normalized positional values plus dynamic-function metadata | `Heap(Mixed)` | conservative dynamic-call effects | +| `EvalFunctionCallArray` | argument-array value plus function-name data id | `Heap(Mixed)` | conservative dynamic-call effects | +| `EvalFunctionExists`, `EvalClassExists`, `EvalConstantExists` | symbol-name data id | `I64` bool | `reads_global` | +| `EvalConstantFetch` | constant-name data id | `Heap(Mixed)` | global/heap reads, heap/refcount writes, `may_fatal` | +| `EvalObjectNew` | constructor arguments plus dynamic-class metadata | object or `Mixed` | conservative construction/call effects | +| `EvalStaticMethodCall` | normalized arguments plus target data id | typed or `Mixed` | conservative dynamic-call effects | + +`EvalLiteralCall` does not by itself require the interpreter. The target- +independent planner in `src/eval_aot.rs` can lower the fragment to an internal +EIR function, synchronize supported locals directly, use only the core +eval-scope helpers, or retain interpreter fallback. See +[Eval Runtime Architecture](eval-runtime.md). + ### Externs, Pointers, Buffers, and Packed Data | Op | Operands | Result | Effects | @@ -1232,7 +1257,7 @@ instructions, but they must be represented in metadata or consumed by lowering. | `PostIncrement` | Load local, save old value, increment, store, return old value. | | `PreDecrement` | Load local, decrement, store, return new value. | | `PostDecrement` | Load local, save old value, decrement, store, return old value. | -| `FunctionCall` | If extern: `ExternCall`; if builtin: `BuiltinCall`; otherwise `Call`/variant dispatch after shared argument planning. | +| `FunctionCall` | If extern: `ExternCall`; if builtin: `BuiltinCall`; otherwise `Call`/variant dispatch after shared argument planning. Literal `eval()` becomes `EvalLiteralCall` so later lowering can choose native, scope-only, or interpreter execution. | | `ArrayLiteral` | Allocate indexed array and insert elements in source order, including spread handling. | | `ArrayLiteralAssoc` | Allocate hash table and insert key/value pairs in source order; empty hash keeps mixed key/value metadata. | | `Match` | Lower subject once; build strict-comparison arm CFG; default absence may `Fatal` via match-unhandled runtime. | @@ -1358,10 +1383,13 @@ Runtime effect categories: | I/O | streams, filesystem, stat/path helpers | | pointers | C string conversion and pointer string helpers | | fibers | stack allocation, switch, start/resume/suspend/throw/getters | +| eval scope | materialized boxed scope access without requiring the interpreter | +| eval bridge | Magician context, dynamic parsing/execution, symbol registration, and native callback hooks | `RuntimeFeatures` remains the mechanism for optional runtime categories such as -regex. EIR lowering must set required runtime features when it emits operations -that need optional helpers. +regex, `eval_scope`, and `eval_bridge`. EIR lowering must set required runtime +features when it emits operations that need optional helpers. A literal eval +that is fully lowered to native EIR leaves both eval runtime features disabled. ## Target-Aware Backend Boundary diff --git a/docs/internals/the-optimizer.md b/docs/internals/the-optimizer.md index 843d99658c..de84c51c37 100644 --- a/docs/internals/the-optimizer.md +++ b/docs/internals/the-optimizer.md @@ -342,6 +342,21 @@ It now recognizes a useful subset of call expressions precisely, but it still do That conservatism is why the pass is safe to run by default: if an expression could have runtime behavior and elephc cannot prove otherwise with its local summaries, the optimizer prefers to keep it. +### Eval as a dynamic barrier + +`eval()` is the strongest AST-level invalidation case. Its argument is evaluated +normally, but the call itself is observable, may warn/throw/fatal, and can read, +write, create, or unset caller-visible variables and dynamic symbols. Constant +propagation therefore returns `Invalidation::All`; effect analysis must never +classify eval as pure, even when its return value is unused. + +This remains true for a literal source string during AST optimization. A later +target-independent planner can lower an eligible literal to EIR and omit the +physical interpreter/scope barrier, but the AST optimizer does not speculate +across that boundary. The separation keeps type and propagation facts safe +while still allowing the backend to produce bridge-free native code. See +[Eval Runtime Architecture](eval-runtime.md). + ## Pipe operator optimizations PHP 8.5's pipe operator `|>` is implemented as a dedicated `ExprKind::Pipe` diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index eb5da2f4c0..efa9d3d3bc 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -230,12 +230,12 @@ forms such as `?T|U` and normalize accepted declarations. | Type | Fields | Description | |---|---|---| | `Visibility` | `Public`, `Protected`, `Private` | Enum for property/method visibility | -| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, and property names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs; parameter reflection is not implemented yet. | +| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, property, and method-parameter names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs. Method parameter names, positions, optional/variadic/by-reference flags, declared-type presence, and method-parameter attributes feed the supported `ReflectionMethod::getParameters()` / `ReflectionParameter` slice. | | `AttributeGroup` | `attributes`, `span` | One bracketed attribute group. Declaration sites can carry one or more groups. | | `EnumCaseDecl` | `name`, `value`, `span`, `attributes` | A backed or unit enum case declaration, with declaration-level attributes preserved in the AST. | | `ClassConst` | `name`, `visibility`, `is_final`, `value`, `span`, `attributes` | A class, interface, or trait constant declaration. | | `ClassProperty` | `name`, `visibility`, `type_expr`, `hooks`, `readonly`, `is_final`, `is_static`, `is_abstract`, `by_ref`, `default`, `span`, `attributes` | A property declaration inside a class, trait, or interface, optionally carrying a parsed property type declaration, hook contract, static-property marker, by-reference promotion marker, or declaration-level attributes | -| `ClassMethod` | `name`, `visibility`, `is_static`, `is_abstract`, `is_final`, `has_body`, `params`, `variadic`, `return_type`, `body`, `span`, `attributes` | A method declaration inside a class, trait, or interface | +| `ClassMethod` | `name`, `visibility`, `is_static`, `is_abstract`, `is_final`, `has_body`, `params`, `param_attributes`, `variadic`, `return_type`, `body`, `span`, `attributes` | A method declaration inside a class, trait, or interface, including source-order parameter attribute groups | | `CatchClause` | `exception_types`, `variable`, `body` | A catch arm. `exception_types` supports both single-type and PHP-style multi-catch (`TypeA | TypeB`), and `variable` is optional for PHP 8-style `catch (Exception)` | | `StaticReceiver` | `Named(Name)`, `Self_`, `Static`, `Parent` | Left-hand side of `ClassName::method()`, `self::method()`, `static::method()`, and `parent::method()` | | `TraitUse` | `trait_names`, `adaptations`, `span` | A `use TraitA, TraitB { ... }` clause inside a class or trait body | diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 51e3143d00..d453d1e5f3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -11,6 +11,26 @@ The runtime is a collection of **hand-written assembly routines** that handle op These routines end up in every compiled binary. In the CLI flow they are usually pre-assembled into the cached runtime object rather than textually appended to each user `.s` file, but they are still part of the final executable rather than an external shared dependency. +## Eval execution paths + +`eval()` is an optional hybrid boundary rather than part of the ordinary +shared runtime. Eligible literal fragments are parsed at compile time and +lowered directly to EIR. Some literal fragments use only the core eval-scope +helpers, while dynamic or unsupported fragments link the +`elephc_magician` static interpreter bridge. + +The bridge shares elephc's boxed `Mixed` cells, copy-on-write containers, +generated class/callable metadata, diagnostics, exceptions, and target-aware +ABI helpers. It is embedded only when the final EIR module requires the +`eval_bridge` runtime feature; the presence of a fully native literal +`eval()` call does not force it into the binary. + +The compile-time planner, scope/context lifecycle, eval-specific EIR +instructions, parse cache, ownership rules, and bridge linking contract are +documented in [Eval Runtime Architecture](eval-runtime.md). PHP-visible +syntax, supported statements and builtins, reflection behavior, safety, and +limitations live in [Eval](../php/eval.md). + ## Why a runtime? Some operations can't be done with a few inline instructions: @@ -841,7 +861,7 @@ Additionally, the runtime emits static data tables: - `_instanceof_target_count`, `_instanceof_target_entries`, `_instanceof_name_*` — case-insensitive class/interface name metadata used by dynamic `instanceof` string targets, including leading-backslash aliases - `_class_gc_desc_count`, `_class_gc_desc_ptrs`, `_class_gc_desc_` — per-class property traversal metadata used by object deep-free and cycle collection - `_class_json_desc_ptrs`, `_class_json_desc_`, `_class_json_pname__`, `_json_exception_class_id`, `_stdclass_class_id` — JSON object encoding descriptors, JsonException construction metadata, and stdClass runtime class id -- `_class_attribute_count`, `_class_attribute_ptrs`, `_class_attributes_` — emitted class-level PHP attribute metadata. The current PHP-facing helpers and Reflection owner constructors materialize their results from the same `ClassInfo` metadata during codegen, rather than doing dynamic runtime class/member lookup. +- `_class_attribute_count`, `_class_attribute_ptrs`, `_class_attributes_` — emitted class-level PHP attribute metadata. The current PHP-facing helpers and class/enum Reflection attribute constructors materialize their results from the same `ClassInfo` metadata during codegen, rather than doing dynamic runtime class/member lookup. - `_class_vtable_ptrs`, `_class_vtable_` — per-class virtual-method tables used by inheritance dispatch through `class_id` - `_class_static_vtable_ptrs`, `_class_static_vtable_` — per-class static-method tables used by late static binding - `_class_destruct_ptrs` — class_id-indexed `__destruct` method pointers (or `0`) consulted by `__rt_call_object_destructor` during object deep-free diff --git a/docs/internals/the-type-checker.md b/docs/internals/the-type-checker.md index 284c229bfc..b26eb2ba4b 100644 --- a/docs/internals/the-type-checker.md +++ b/docs/internals/the-type-checker.md @@ -168,6 +168,25 @@ Built-in functions have hardcoded type signatures (see below). User-defined func Codegen receives enough signature information to evaluate named/spread arguments in PHP source order while still materializing values in ABI parameter order. +### Eval barrier + +`eval()` is recognized as a PHP language construct rather than a registry-backed +builtin. The checker requires exactly one argument, infers that argument for its +normal side effects, and assigns the call the static result type `Mixed`. + +After an eval call, the caller-visible environment becomes dynamic: the +fragment may overwrite or unset existing locals, introduce new locals, declare +symbols, or mutate referenced storage. The checker therefore marks an eval +barrier, widens known local types to `Mixed`, discards callable/capture facts, +and permits later reads of variables and class-like symbols that may have been +created at runtime. + +This rule is intentionally conservative for literal strings too. The later EIR +planner may prove that a literal fragment can be AOT-lowered without runtime +scope or interpreter state, but that backend decision must not hide frontend +diagnostics or make pre-call type facts unsound. See +[Eval Runtime Architecture](eval-runtime.md). + ## Built-in function signatures **Files:** `src/types/checker/builtins/`, plus `src/types/checker/mod.rs` and `src/types/checker/inference/` for special expression forms such as `ExprKind::PtrCast`, `ExprKind::InstanceOf`, and `ExprKind::Print` diff --git a/docs/internals/what-is-a-compiler.md b/docs/internals/what-is-a-compiler.md index efa8866bee..3a7f3af65a 100644 --- a/docs/internals/what-is-a-compiler.md +++ b/docs/internals/what-is-a-compiler.md @@ -26,13 +26,13 @@ The key difference: an interpreter is always present at runtime, translating as ## What elephc does -elephc is a compiler. It takes PHP source code and produces a native binary for the supported targets. No PHP interpreter involved. The output is a standalone executable, just like a C program compiled with `gcc`. +elephc is a compiler. It takes PHP source code and produces a native binary for the supported targets. No external PHP interpreter is required. The output is a standalone executable, just like a C program compiled with `gcc`. ``` hello.php → elephc → hello (native executable) → runs directly on CPU ``` -The resulting binary has no dependency on PHP and no interpreter or VM. It includes elephc's emitted helper routines and links the platform's native system libraries for OS and libc services. +The resulting binary has no dependency on PHP, the Zend Engine, or an external VM. It includes elephc's emitted helper routines and links the platform's native system libraries for OS and libc services. Experimental `eval()` is a hybrid boundary: eligible literals are still AOT-compiled, while dynamic fragments embed the optional Magician interpreter bridge inside the executable. ## The phases of compilation @@ -74,6 +74,6 @@ elephc handles the first four phases. The last two (assembler and linker) are de PHP is normally interpreted, and that's fine for web servers. So why compile it? -Compiling PHP turns it into fast, standalone native binaries with no interpreter, VM, or runtime dependency — you ship a single executable. PHP is also a great language to compile: its syntax is simple enough to be tractable but rich enough to be interesting (strings, arrays, functions, control flow, type coercion), and millions of developers already know it. +Compiling PHP turns it into fast, standalone native binaries without an external PHP interpreter or VM — you ship a single executable. PHP is also a great language to compile: its syntax is simple enough to be tractable but rich enough to be interesting (strings, arrays, functions, control flow, type coercion), and millions of developers already know it. Programs that use experimental dynamic `eval()` may carry the optional embedded Magician bridge; the rest of the program remains native code. The fact that the output is *real assembly* means you can see exactly what the CPU does for every PHP construct. Many of the internals documents use AArch64 examples because that was the original backend and remains the most explanatory path, but the same compiler pipeline now targets more than one platform/architecture pair. `echo 1 + 2` isn't magic — it's a few data moves, an add, a call to a conversion routine, and a system call. You can trace every step. diff --git a/docs/php/arrays.md b/docs/php/arrays.md index 8501b3d3c2..35f246ae75 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -2,7 +2,7 @@ title: "Arrays" description: "Indexed arrays, associative arrays, copy-on-write, and built-in array functions." sidebar: - order: 7 + order: 8 --- ## Indexed arrays @@ -46,7 +46,7 @@ $map["age"] = "30"; // add new key Associative arrays use a hash table runtime. If later values do not match the first value type, the checker widens to internal `mixed` runtime shape. -Keys follow PHP's array-key normalization. Integer keys remain integers, booleans and floats normalize to integer keys, numeric strings such as `"1"` normalize to the integer key `1`, and strings with leading zeroes such as `"01"` remain string keys. This applies to literals, reads and writes, `foreach`, `array_keys()`, `array_search()`, `array_key_exists()`, `array_flip()`, JSON object keys, and array union. +Keys follow PHP's array-key normalization. Integer keys remain integers, booleans and floats normalize to integer keys, `null` normalizes to the empty-string key, numeric strings such as `"1"` normalize to the integer key `1`, and strings with leading zeroes such as `"01"` remain string keys. This applies to literals, reads and writes, `foreach`, `array_keys()`, `array_search()`, `array_key_exists()`, `array_flip()`, JSON object keys, and array union. ```php $a->weight <=> $b->weight)` works without writing `($a, $b)` type hints, and `usort($dates, fn($a, $b) => $a <=> $b)` over `DateTime`/`DateTimeImmutable` compares by instant. Explicit hints (`function (Item $a, Item $b)`) are equally accepted. Sorting an array of **strings** with a user comparator is not yet supported and reports a clear unsupported-feature error. -**Not supported by design:** `compact()`, `extract()` require runtime variable-name tables and are listed in the roadmap's "Will not implement" section. +**Not supported yet:** `compact()` and `extract()` need dynamic access to the +current variable scope. Magician's materialized named scope makes that behavior +feasible, but these functions are not wired into the compiler or interpreter +today. Use an associative array explicitly in portable elephc code. diff --git a/docs/php/builtins.md b/docs/php/builtins.md index 2549d7161d..e953762a18 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -7,437 +7,441 @@ sidebar: ## Builtins -| Function | Signature | Returns | -|---|---|---| -| [`array_all()`](./builtins/array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_any()`](./builtins/array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | -| [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | -| [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_fill()`](./builtins/array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | -| [`array_fill_keys()`](./builtins/array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | -| [`array_filter()`](./builtins/array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | -| [`array_find()`](./builtins/array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | -| [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_intersect_assoc()`](./builtins/array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_intersect_key()`](./builtins/array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_is_list()`](./builtins/array/array_is_list.md) | `(mixed $array): bool` | `bool` | -| [`array_key_exists()`](./builtins/array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | -| [`array_key_first()`](./builtins/array/array_key_first.md) | `(array $array): mixed` | `mixed` | -| [`array_key_last()`](./builtins/array/array_key_last.md) | `(array $array): mixed` | `mixed` | -| [`array_keys()`](./builtins/array/array_keys.md) | `(array $array): array` | `array` | -| [`array_map()`](./builtins/array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | -| [`array_merge()`](./builtins/array/array_merge.md) | `(...$arrays): array` | `array` | -| [`array_merge_recursive()`](./builtins/array/array_merge_recursive.md) | `(...$arrays): array` | `array` | -| [`array_multisort()`](./builtins/array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | -| [`array_pad()`](./builtins/array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | -| [`array_pop()`](./builtins/array/array_pop.md) | `(array $array): mixed` | `mixed` | -| [`array_product()`](./builtins/array/array_product.md) | `(array $array): int` | `int` | -| [`array_push()`](./builtins/array/array_push.md) | `(array $array, ...$values): void` | `void` | -| [`array_rand()`](./builtins/array/array_rand.md) | `(array $array): int` | `int` | -| [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | -| [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | -| [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | -| [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | -| [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_uintersect()`](./builtins/array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_unique()`](./builtins/array/array_unique.md) | `(array $array): array` | `array` | -| [`array_unshift()`](./builtins/array/array_unshift.md) | `(array $array, ...$values): int` | `int` | -| [`array_values()`](./builtins/array/array_values.md) | `(array $array): array` | `array` | -| [`array_walk()`](./builtins/array/array_walk.md) | `(array $array, callable $callback): void` | `void` | -| [`array_walk_recursive()`](./builtins/array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | -| [`arsort()`](./builtins/array/arsort.md) | `(array $array): bool` | `bool` | -| [`asort()`](./builtins/array/asort.md) | `(array $array): bool` | `bool` | -| [`call_user_func()`](./builtins/array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./builtins/array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`count()`](./builtins/array/count.md) | `(array $value, int $mode = 0): int` | `int` | -| [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | -| [`krsort()`](./builtins/array/krsort.md) | `(array $array): bool` | `bool` | -| [`ksort()`](./builtins/array/ksort.md) | `(array $array): bool` | `bool` | -| [`natcasesort()`](./builtins/array/natcasesort.md) | `(array $array): bool` | `bool` | -| [`natsort()`](./builtins/array/natsort.md) | `(array $array): bool` | `bool` | -| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | -| [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | -| [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | -| [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | -| [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | -| [`buffer_len()`](./builtins/buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | -| [`class_alias()`](./builtins/class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | -| [`class_attribute_args()`](./builtins/class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | -| [`class_attribute_names()`](./builtins/class/class_attribute_names.md) | `(string $class_name): array` | `array` | -| [`class_exists()`](./builtins/class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | -| [`class_get_attributes()`](./builtins/class/class_get_attributes.md) | `(string $class_name): array` | `array` | -| [`class_implements()`](./builtins/class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_parents()`](./builtins/class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_uses()`](./builtins/class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`enum_exists()`](./builtins/class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | -| [`function_exists()`](./builtins/class/function_exists.md) | `(string $function): bool` | `bool` | -| [`get_class()`](./builtins/class/get_class.md) | `(object $object = null): string` | `string` | -| [`get_declared_classes()`](./builtins/class/get_declared_classes.md) | `(): array` | `array` | -| [`get_declared_interfaces()`](./builtins/class/get_declared_interfaces.md) | `(): array` | `array` | -| [`get_declared_traits()`](./builtins/class/get_declared_traits.md) | `(): array` | `array` | -| [`get_parent_class()`](./builtins/class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | -| [`interface_exists()`](./builtins/class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | -| [`is_a()`](./builtins/class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | -| [`is_subclass_of()`](./builtins/class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | -| [`trait_exists()`](./builtins/class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | -| [`checkdate()`](./builtins/date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | -| [`date()`](./builtins/date/date.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`date_default_timezone_get()`](./builtins/date/date_default_timezone_get.md) | `(): string` | `string` | -| [`date_default_timezone_set()`](./builtins/date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | -| [`getdate()`](./builtins/date/getdate.md) | `(int $timestamp = null): array` | `array` | -| [`gmdate()`](./builtins/date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`gmmktime()`](./builtins/date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`hrtime()`](./builtins/date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | -| [`localtime()`](./builtins/date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | -| [`microtime()`](./builtins/date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | -| [`mktime()`](./builtins/date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`strtotime()`](./builtins/date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | -| [`time()`](./builtins/date/time.md) | `(): int` | `int` | -| [`basename()`](./builtins/filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | -| [`chdir()`](./builtins/filesystem/chdir.md) | `(string $directory): bool` | `bool` | -| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`chmod()`](./builtins/filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | -| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | -| [`clearstatcache()`](./builtins/filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | -| [`copy()`](./builtins/filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | -| [`dirname()`](./builtins/filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | -| [`disk_free_space()`](./builtins/filesystem/disk_free_space.md) | `(string $directory): float` | `float` | -| [`disk_total_space()`](./builtins/filesystem/disk_total_space.md) | `(string $directory): float` | `float` | -| [`file_exists()`](./builtins/filesystem/file_exists.md) | `(string $filename): bool` | `bool` | -| [`fileatime()`](./builtins/filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | -| [`filectime()`](./builtins/filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | -| [`filegroup()`](./builtins/filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | -| [`fileinode()`](./builtins/filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | -| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): int` | `int` | -| [`fileowner()`](./builtins/filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | -| [`fileperms()`](./builtins/filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | -| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): int` | `int` | -| [`filetype()`](./builtins/filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | -| [`fnmatch()`](./builtins/filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | -| [`getcwd()`](./builtins/filesystem/getcwd.md) | `(): string` | `string` | -| [`getenv()`](./builtins/filesystem/getenv.md) | `(string $name): mixed` | `mixed` | -| [`glob()`](./builtins/filesystem/glob.md) | `(string $pattern): array` | `array` | -| [`is_dir()`](./builtins/filesystem/is_dir.md) | `(string $filename): bool` | `bool` | -| [`is_executable()`](./builtins/filesystem/is_executable.md) | `(string $filename): bool` | `bool` | -| [`is_file()`](./builtins/filesystem/is_file.md) | `(string $filename): bool` | `bool` | -| [`is_link()`](./builtins/filesystem/is_link.md) | `(string $filename): bool` | `bool` | -| [`is_readable()`](./builtins/filesystem/is_readable.md) | `(string $filename): bool` | `bool` | -| [`is_writable()`](./builtins/filesystem/is_writable.md) | `(string $filename): bool` | `bool` | -| [`is_writeable()`](./builtins/filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | -| [`lchgrp()`](./builtins/filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | -| [`link()`](./builtins/filesystem/link.md) | `(string $target, string $link): bool` | `bool` | -| [`linkinfo()`](./builtins/filesystem/linkinfo.md) | `(string $path): int` | `int` | -| [`lstat()`](./builtins/filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | -| [`mkdir()`](./builtins/filesystem/mkdir.md) | `(string $directory): bool` | `bool` | -| [`pathinfo()`](./builtins/filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | -| [`putenv()`](./builtins/filesystem/putenv.md) | `(string $assignment): bool` | `bool` | -| [`readfile()`](./builtins/filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | -| [`readlink()`](./builtins/filesystem/readlink.md) | `(string $path): mixed` | `mixed` | -| [`realpath()`](./builtins/filesystem/realpath.md) | `(string $path): mixed` | `mixed` | -| [`realpath_cache_get()`](./builtins/filesystem/realpath_cache_get.md) | `(): array` | `array` | -| [`realpath_cache_size()`](./builtins/filesystem/realpath_cache_size.md) | `(): int` | `int` | -| [`rename()`](./builtins/filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | -| [`rmdir()`](./builtins/filesystem/rmdir.md) | `(string $directory): bool` | `bool` | -| [`scandir()`](./builtins/filesystem/scandir.md) | `(string $directory): array` | `array` | -| [`stat()`](./builtins/filesystem/stat.md) | `(string $filename): mixed` | `mixed` | -| [`symlink()`](./builtins/filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | -| [`sys_get_temp_dir()`](./builtins/filesystem/sys_get_temp_dir.md) | `(): string` | `string` | -| [`tempnam()`](./builtins/filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | -| [`tmpfile()`](./builtins/filesystem/tmpfile.md) | `(): mixed` | `mixed` | -| [`touch()`](./builtins/filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | -| [`umask()`](./builtins/filesystem/umask.md) | `(int $mask = null): int` | `int` | -| [`unlink()`](./builtins/filesystem/unlink.md) | `(string $filename): bool` | `bool` | -| [`closedir()`](./builtins/io/closedir.md) | `(resource $dir_handle): void` | `void` | -| [`fclose()`](./builtins/io/fclose.md) | `(resource $stream): bool` | `bool` | -| [`fdatasync()`](./builtins/io/fdatasync.md) | `(resource $stream): bool` | `bool` | -| [`feof()`](./builtins/io/feof.md) | `(resource $stream): bool` | `bool` | -| [`fflush()`](./builtins/io/fflush.md) | `(resource $stream): bool` | `bool` | -| [`fgetc()`](./builtins/io/fgetc.md) | `(resource $stream): mixed` | `mixed` | -| [`fgetcsv()`](./builtins/io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | -| [`fgets()`](./builtins/io/fgets.md) | `(resource $stream): mixed` | `mixed` | -| [`file()`](./builtins/io/file.md) | `(string $filename): array` | `array` | -| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | -| [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | -| [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | -| [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | -| [`fpassthru()`](./builtins/io/fpassthru.md) | `(resource $stream): int` | `int` | -| [`fprintf()`](./builtins/io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | -| [`fputcsv()`](./builtins/io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | -| [`fread()`](./builtins/io/fread.md) | `(resource $stream, int $length): string` | `string` | -| [`fscanf()`](./builtins/io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | -| [`fseek()`](./builtins/io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | -| [`fstat()`](./builtins/io/fstat.md) | `(resource $stream): mixed` | `mixed` | -| [`fsync()`](./builtins/io/fsync.md) | `(resource $stream): bool` | `bool` | -| [`ftell()`](./builtins/io/ftell.md) | `(resource $stream): int` | `int` | -| [`ftruncate()`](./builtins/io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | -| [`fwrite()`](./builtins/io/fwrite.md) | `(resource $stream, string $data): int` | `int` | -| [`gethostbyaddr()`](./builtins/io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | -| [`gethostbyname()`](./builtins/io/gethostbyname.md) | `(string $hostname): string` | `string` | -| [`gethostname()`](./builtins/io/gethostname.md) | `(): string` | `string` | -| [`getprotobyname()`](./builtins/io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | -| [`getprotobynumber()`](./builtins/io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | -| [`getservbyname()`](./builtins/io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | -| [`getservbyport()`](./builtins/io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | -| [`hash_file()`](./builtins/io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | -| [`opendir()`](./builtins/io/opendir.md) | `(string $directory): mixed` | `mixed` | -| [`readdir()`](./builtins/io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | -| [`rewind()`](./builtins/io/rewind.md) | `(resource $stream): bool` | `bool` | -| [`rewinddir()`](./builtins/io/rewinddir.md) | `(resource $dir_handle): void` | `void` | -| [`stream_bucket_make_writeable()`](./builtins/io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | -| [`stream_bucket_new()`](./builtins/io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | -| [`stream_context_create()`](./builtins/io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | -| [`stream_context_get_default()`](./builtins/io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | -| [`stream_context_get_options()`](./builtins/io/stream_context_get_options.md) | `(resource $context): array` | `array` | -| [`stream_context_get_params()`](./builtins/io/stream_context_get_params.md) | `(resource $context): array` | `array` | -| [`stream_context_set_default()`](./builtins/io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_set_option()`](./builtins/io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | -| [`stream_context_set_params()`](./builtins/io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | -| [`stream_copy_to_stream()`](./builtins/io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_filter_register()`](./builtins/io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | -| [`stream_filter_remove()`](./builtins/io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | -| [`stream_get_contents()`](./builtins/io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_get_filters()`](./builtins/io/stream_get_filters.md) | `(): array` | `array` | -| [`stream_get_line()`](./builtins/io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | -| [`stream_get_meta_data()`](./builtins/io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | -| [`stream_get_transports()`](./builtins/io/stream_get_transports.md) | `(): array` | `array` | -| [`stream_get_wrappers()`](./builtins/io/stream_get_wrappers.md) | `(): array` | `array` | -| [`stream_is_local()`](./builtins/io/stream_is_local.md) | `(resource $stream): bool` | `bool` | -| [`stream_isatty()`](./builtins/io/stream_isatty.md) | `(resource $stream): bool` | `bool` | -| [`stream_resolve_include_path()`](./builtins/io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | -| [`stream_select()`](./builtins/io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | -| [`stream_set_blocking()`](./builtins/io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | -| [`stream_set_chunk_size()`](./builtins/io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_read_buffer()`](./builtins/io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_timeout()`](./builtins/io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | -| [`stream_set_write_buffer()`](./builtins/io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_socket_accept()`](./builtins/io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | -| [`stream_socket_client()`](./builtins/io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | -| [`stream_socket_get_name()`](./builtins/io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | -| [`stream_socket_pair()`](./builtins/io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | -| [`stream_socket_recvfrom()`](./builtins/io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_sendto()`](./builtins/io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_server()`](./builtins/io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_shutdown()`](./builtins/io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | -| [`stream_supports_lock()`](./builtins/io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | -| [`stream_wrapper_register()`](./builtins/io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | -| [`stream_wrapper_restore()`](./builtins/io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | -| [`stream_wrapper_unregister()`](./builtins/io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | -| [`vfprintf()`](./builtins/io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | -| [`json_decode()`](./builtins/json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | -| [`json_encode()`](./builtins/json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | -| [`json_last_error()`](./builtins/json/json_last_error.md) | `(): int` | `int` | -| [`json_last_error_msg()`](./builtins/json/json_last_error_msg.md) | `(): string` | `string` | -| [`json_validate()`](./builtins/json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | -| [`abs()`](./builtins/math/abs.md) | `(int $num): mixed` | `mixed` | -| [`acos()`](./builtins/math/acos.md) | `(float $num): float` | `float` | -| [`asin()`](./builtins/math/asin.md) | `(float $num): float` | `float` | -| [`atan()`](./builtins/math/atan.md) | `(float $num): float` | `float` | -| [`atan2()`](./builtins/math/atan2.md) | `(float $y, float $x): float` | `float` | -| [`ceil()`](./builtins/math/ceil.md) | `(float $num): float` | `float` | -| [`clamp()`](./builtins/math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | -| [`cos()`](./builtins/math/cos.md) | `(float $num): float` | `float` | -| [`cosh()`](./builtins/math/cosh.md) | `(float $num): float` | `float` | -| [`deg2rad()`](./builtins/math/deg2rad.md) | `(float $num): float` | `float` | -| [`exp()`](./builtins/math/exp.md) | `(float $num): float` | `float` | -| [`fdiv()`](./builtins/math/fdiv.md) | `(float $num1, float $num2): float` | `float` | -| [`floor()`](./builtins/math/floor.md) | `(float $num): float` | `float` | -| [`fmod()`](./builtins/math/fmod.md) | `(float $num1, float $num2): float` | `float` | -| [`hypot()`](./builtins/math/hypot.md) | `(float $x, float $y): float` | `float` | -| [`intdiv()`](./builtins/math/intdiv.md) | `(int $num1, int $num2): int` | `int` | -| [`is_finite()`](./builtins/math/is_finite.md) | `(float $num): bool` | `bool` | -| [`is_infinite()`](./builtins/math/is_infinite.md) | `(float $num): bool` | `bool` | -| [`is_nan()`](./builtins/math/is_nan.md) | `(float $num): bool` | `bool` | -| [`log()`](./builtins/math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | -| [`log10()`](./builtins/math/log10.md) | `(float $num): float` | `float` | -| [`log2()`](./builtins/math/log2.md) | `(float $num): float` | `float` | -| [`max()`](./builtins/math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`mt_rand()`](./builtins/math/mt_rand.md) | `(int $min, int $max): int` | `int` | -| [`pi()`](./builtins/math/pi.md) | `(): float` | `float` | -| [`pow()`](./builtins/math/pow.md) | `(float $num, float $exponent): float` | `float` | -| [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | -| [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | -| [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | -| [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | -| [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | -| [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | -| [`tan()`](./builtins/math/tan.md) | `(float $num): float` | `float` | -| [`tanh()`](./builtins/math/tanh.md) | `(float $num): float` | `float` | -| [`buffer_new()`](./builtins/misc/buffer_new.md) | `(int $length): mixed` | `mixed` | -| [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | -| [`defined()`](./builtins/misc/defined.md) | `(string $constant_name): bool` | `bool` | -| [`empty()`](./builtins/misc/empty.md) | `(mixed $value): bool` | `bool` | -| [`header()`](./builtins/misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | -| [`http_response_code()`](./builtins/misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | -| [`isset()`](./builtins/misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | -| [`php_uname()`](./builtins/misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | -| [`phpversion()`](./builtins/misc/phpversion.md) | `(): string` | `string` | -| [`print_r()`](./builtins/misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | -| [`serialize()`](./builtins/misc/serialize.md) | `(mixed $value): string` | `string` | -| [`unserialize()`](./builtins/misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | -| [`unset()`](./builtins/misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | -| [`var_dump()`](./builtins/misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | -| [`ptr()`](./builtins/pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | -| [`ptr_get()`](./builtins/pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | -| [`ptr_is_null()`](./builtins/pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | -| [`ptr_null()`](./builtins/pointer/ptr_null.md) | `(): mixed` | `mixed` | -| [`ptr_offset()`](./builtins/pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | -| [`ptr_read16()`](./builtins/pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read32()`](./builtins/pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read8()`](./builtins/pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read_string()`](./builtins/pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | -| [`ptr_set()`](./builtins/pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | -| [`ptr_sizeof()`](./builtins/pointer/ptr_sizeof.md) | `(string $type): int` | `int` | -| [`ptr_write16()`](./builtins/pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write32()`](./builtins/pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write8()`](./builtins/pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write_string()`](./builtins/pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | -| [`zval_free()`](./builtins/pointer/zval_free.md) | `(pointer $zval): void` | `void` | -| [`zval_pack()`](./builtins/pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | -| [`zval_type()`](./builtins/pointer/zval_type.md) | `(pointer $zval): int` | `int` | -| [`zval_unpack()`](./builtins/pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | -| [`die()`](./builtins/process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./builtins/process/exec.md) | `(string $command): string` | `string` | -| [`exit()`](./builtins/process/exit.md) | `(int $status): void` | `void` | -| [`passthru()`](./builtins/process/passthru.md) | `(string $command): void` | `void` | -| [`pclose()`](./builtins/process/pclose.md) | `(resource $handle): int` | `int` | -| [`popen()`](./builtins/process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | -| [`readline()`](./builtins/process/readline.md) | `(string $prompt = null): mixed` | `mixed` | -| [`shell_exec()`](./builtins/process/shell_exec.md) | `(string $command): string` | `string` | -| [`sleep()`](./builtins/process/sleep.md) | `(int $seconds): int` | `int` | -| [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | -| [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | -| [`mb_ereg_match()`](./builtins/regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | -| [`preg_match()`](./builtins/regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | -| [`preg_match_all()`](./builtins/regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | -| [`preg_replace()`](./builtins/regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | -| [`preg_replace_callback()`](./builtins/regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | -| [`preg_split()`](./builtins/regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | -| [`iterator_apply()`](./builtins/spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | -| [`iterator_count()`](./builtins/spl/iterator_count.md) | `(traversable $iterator): int` | `int` | -| [`iterator_to_array()`](./builtins/spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | -| [`spl_autoload()`](./builtins/spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | -| [`spl_autoload_call()`](./builtins/spl/spl_autoload_call.md) | `(string $class): void` | `void` | -| [`spl_autoload_extensions()`](./builtins/spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | -| [`spl_autoload_functions()`](./builtins/spl/spl_autoload_functions.md) | `(): array` | `array` | -| [`spl_autoload_register()`](./builtins/spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | -| [`spl_autoload_unregister()`](./builtins/spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | -| [`spl_classes()`](./builtins/spl/spl_classes.md) | `(): array` | `array` | -| [`spl_object_hash()`](./builtins/spl/spl_object_hash.md) | `(object $object): string` | `string` | -| [`spl_object_id()`](./builtins/spl/spl_object_id.md) | `(object $object): int` | `int` | -| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`stream_bucket_append()`](./builtins/streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_bucket_prepend()`](./builtins/streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`addslashes()`](./builtins/string/addslashes.md) | `(string $string): string` | `string` | -| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string): string` | `string` | -| [`base64_encode()`](./builtins/string/base64_encode.md) | `(string $string): string` | `string` | -| [`bin2hex()`](./builtins/string/bin2hex.md) | `(string $string): string` | `string` | -| [`chop()`](./builtins/string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`chr()`](./builtins/string/chr.md) | `(int $codepoint): string` | `string` | -| [`crc32()`](./builtins/string/crc32.md) | `(string $string): int` | `int` | -| [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | -| [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | -| [`gzcompress()`](./builtins/string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzdeflate()`](./builtins/string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzinflate()`](./builtins/string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`gzuncompress()`](./builtins/string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`hash()`](./builtins/string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | -| [`hash_algos()`](./builtins/string/hash_algos.md) | `(): array` | `array` | -| [`hash_copy()`](./builtins/string/hash_copy.md) | `(resource $context): mixed` | `mixed` | -| [`hash_equals()`](./builtins/string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | -| [`hash_final()`](./builtins/string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | -| [`hash_hmac()`](./builtins/string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | -| [`hash_init()`](./builtins/string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | -| [`hash_update()`](./builtins/string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | -| [`hex2bin()`](./builtins/string/hex2bin.md) | `(string $string): string` | `string` | -| [`html_entity_decode()`](./builtins/string/html_entity_decode.md) | `(string $string): string` | `string` | -| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`implode()`](./builtins/string/implode.md) | `(string $separator, array $array = null): string` | `string` | -| [`inet_ntop()`](./builtins/string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | -| [`inet_pton()`](./builtins/string/inet_pton.md) | `(string $ip): mixed` | `mixed` | -| [`ip2long()`](./builtins/string/ip2long.md) | `(string $ip): mixed` | `mixed` | -| [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | -| [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | -| [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary = false): string` | `string` | -| [`nl2br()`](./builtins/string/nl2br.md) | `(string $string): string` | `string` | -| [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | -| [`ord()`](./builtins/string/ord.md) | `(string $character): int` | `int` | -| [`printf()`](./builtins/string/printf.md) | `(string $format, ...$values): int` | `int` | -| [`rawurldecode()`](./builtins/string/rawurldecode.md) | `(string $string): string` | `string` | -| [`rawurlencode()`](./builtins/string/rawurlencode.md) | `(string $string): string` | `string` | -| [`rtrim()`](./builtins/string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`sha1()`](./builtins/string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | -| [`sprintf()`](./builtins/string/sprintf.md) | `(string $format, ...$values): string` | `string` | -| [`sscanf()`](./builtins/string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | -| [`str_contains()`](./builtins/string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ends_with()`](./builtins/string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ireplace()`](./builtins/string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_pad()`](./builtins/string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | -| [`str_repeat()`](./builtins/string/str_repeat.md) | `(string $string, int $times): string` | `string` | -| [`str_replace()`](./builtins/string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length = 1): array` | `array` | -| [`str_starts_with()`](./builtins/string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`strcasecmp()`](./builtins/string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | -| [`strcmp()`](./builtins/string/strcmp.md) | `(string $string1, string $string2): int` | `int` | -| [`stripslashes()`](./builtins/string/stripslashes.md) | `(string $string): string` | `string` | -| [`strlen()`](./builtins/string/strlen.md) | `(string $string): int` | `int` | -| [`strpos()`](./builtins/string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strrev()`](./builtins/string/strrev.md) | `(string $string): string` | `string` | -| [`strrpos()`](./builtins/string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | -| [`strtolower()`](./builtins/string/strtolower.md) | `(string $string): string` | `string` | -| [`strtoupper()`](./builtins/string/strtoupper.md) | `(string $string): string` | `string` | -| [`substr()`](./builtins/string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | -| [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | -| [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`ucfirst()`](./builtins/string/ucfirst.md) | `(string $string): string` | `string` | -| [`ucwords()`](./builtins/string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | -| [`urldecode()`](./builtins/string/urldecode.md) | `(string $string): string` | `string` | -| [`urlencode()`](./builtins/string/urlencode.md) | `(string $string): string` | `string` | -| [`vprintf()`](./builtins/string/vprintf.md) | `(string $format, array $values): int` | `int` | -| [`vsprintf()`](./builtins/string/vsprintf.md) | `(string $format, array $values): string` | `string` | -| [`wordwrap()`](./builtins/string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | -| [`boolval()`](./builtins/type/boolval.md) | `(mixed $value): bool` | `bool` | -| [`ctype_alnum()`](./builtins/type/ctype_alnum.md) | `(string $text): bool` | `bool` | -| [`ctype_alpha()`](./builtins/type/ctype_alpha.md) | `(string $text): bool` | `bool` | -| [`ctype_digit()`](./builtins/type/ctype_digit.md) | `(string $text): bool` | `bool` | -| [`ctype_space()`](./builtins/type/ctype_space.md) | `(string $text): bool` | `bool` | -| [`floatval()`](./builtins/type/floatval.md) | `(mixed $value): float` | `float` | -| [`get_resource_id()`](./builtins/type/get_resource_id.md) | `(resource $resource): int` | `int` | -| [`get_resource_type()`](./builtins/type/get_resource_type.md) | `(resource $resource): string` | `string` | -| [`gettype()`](./builtins/type/gettype.md) | `(mixed $value): string` | `string` | -| [`intval()`](./builtins/type/intval.md) | `(mixed $value): int` | `int` | -| [`is_array()`](./builtins/type/is_array.md) | `(mixed $value): bool` | `bool` | -| [`is_bool()`](./builtins/type/is_bool.md) | `(mixed $value): bool` | `bool` | -| [`is_callable()`](./builtins/type/is_callable.md) | `(mixed $value): bool` | `bool` | -| [`is_float()`](./builtins/type/is_float.md) | `(mixed $value): bool` | `bool` | -| [`is_int()`](./builtins/type/is_int.md) | `(mixed $value): bool` | `bool` | -| [`is_iterable()`](./builtins/type/is_iterable.md) | `(mixed $value): bool` | `bool` | -| [`is_null()`](./builtins/type/is_null.md) | `(mixed $value): bool` | `bool` | -| [`is_numeric()`](./builtins/type/is_numeric.md) | `(mixed $value): bool` | `bool` | -| [`is_object()`](./builtins/type/is_object.md) | `(mixed $value): bool` | `bool` | -| [`is_resource()`](./builtins/type/is_resource.md) | `(mixed $value): bool` | `bool` | -| [`is_scalar()`](./builtins/type/is_scalar.md) | `(mixed $value): bool` | `bool` | -| [`is_string()`](./builtins/type/is_string.md) | `(mixed $value): bool` | `bool` | -| [`settype()`](./builtins/type/settype.md) | `(mixed $var, string $type): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`array_all()`](./builtins/array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_any()`](./builtins/array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | +| [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_fill()`](./builtins/array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_fill_keys()`](./builtins/array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_filter()`](./builtins/array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | ✓ | ✓ | +| [`array_find()`](./builtins/array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | ✓ | — | +| [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_intersect_assoc()`](./builtins/array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_intersect_key()`](./builtins/array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_is_list()`](./builtins/array/array_is_list.md) | `(mixed $array): bool` | `bool` | ✓ | — | +| [`array_key_exists()`](./builtins/array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | ✓ | ✓ | +| [`array_key_first()`](./builtins/array/array_key_first.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_key_last()`](./builtins/array/array_key_last.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_keys()`](./builtins/array/array_keys.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_map()`](./builtins/array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge()`](./builtins/array/array_merge.md) | `(...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge_recursive()`](./builtins/array/array_merge_recursive.md) | `(...$arrays): array` | `array` | ✓ | — | +| [`array_multisort()`](./builtins/array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | ✓ | — | +| [`array_pad()`](./builtins/array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_pop()`](./builtins/array/array_pop.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_product()`](./builtins/array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_push()`](./builtins/array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_rand()`](./builtins/array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | +| [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_uintersect()`](./builtins/array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_unique()`](./builtins/array/array_unique.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_unshift()`](./builtins/array/array_unshift.md) | `(array $array, ...$values): int` | `int` | ✓ | ✓ | +| [`array_values()`](./builtins/array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_walk()`](./builtins/array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | +| [`array_walk_recursive()`](./builtins/array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | +| [`arsort()`](./builtins/array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`asort()`](./builtins/array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`call_user_func()`](./builtins/array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | +| [`call_user_func_array()`](./builtins/array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | +| [`count()`](./builtins/array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`krsort()`](./builtins/array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./builtins/array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./builtins/array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./builtins/array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_len()`](./builtins/buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | +| [`class_alias()`](./builtins/class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_attribute_args()`](./builtins/class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | ✓ | ✓ | +| [`class_attribute_names()`](./builtins/class/class_attribute_names.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_exists()`](./builtins/class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_get_attributes()`](./builtins/class/class_get_attributes.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_implements()`](./builtins/class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_parents()`](./builtins/class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_uses()`](./builtins/class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`enum_exists()`](./builtins/class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`function_exists()`](./builtins/class/function_exists.md) | `(string $function): bool` | `bool` | ✓ | ✓ | +| [`get_called_class()`](./builtins/class/get_called_class.md) | `(): mixed` | `mixed` | — | ✓ | +| [`get_class()`](./builtins/class/get_class.md) | `(object $object = null): string` | `string` | ✓ | ✓ | +| [`get_class_methods()`](./builtins/class/get_class_methods.md) | `(mixed $object_or_class): mixed` | `mixed` | — | ✓ | +| [`get_class_vars()`](./builtins/class/get_class_vars.md) | `(mixed $class): mixed` | `mixed` | — | ✓ | +| [`get_declared_classes()`](./builtins/class/get_declared_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_interfaces()`](./builtins/class/get_declared_interfaces.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_traits()`](./builtins/class/get_declared_traits.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_object_vars()`](./builtins/class/get_object_vars.md) | `(mixed $object): mixed` | `mixed` | — | ✓ | +| [`get_parent_class()`](./builtins/class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | ✓ | ✓ | +| [`interface_exists()`](./builtins/class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`is_a()`](./builtins/class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | ✓ | ✓ | +| [`is_subclass_of()`](./builtins/class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | ✓ | ✓ | +| [`trait_exists()`](./builtins/class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`checkdate()`](./builtins/date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | ✓ | ✓ | +| [`date()`](./builtins/date/date.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_get()`](./builtins/date/date_default_timezone_get.md) | `(): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_set()`](./builtins/date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | ✓ | ✓ | +| [`getdate()`](./builtins/date/getdate.md) | `(int $timestamp = null): array` | `array` | ✓ | ✓ | +| [`gmdate()`](./builtins/date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`gmmktime()`](./builtins/date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`hrtime()`](./builtins/date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | ✓ | ✓ | +| [`localtime()`](./builtins/date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | ✓ | ✓ | +| [`microtime()`](./builtins/date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | ✓ | ✓ | +| [`mktime()`](./builtins/date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`strtotime()`](./builtins/date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | ✓ | ✓ | +| [`time()`](./builtins/date/time.md) | `(): int` | `int` | ✓ | ✓ | +| [`basename()`](./builtins/filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | +| [`chdir()`](./builtins/filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chmod()`](./builtins/filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`clearstatcache()`](./builtins/filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | ✓ | ✓ | +| [`copy()`](./builtins/filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`dirname()`](./builtins/filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | ✓ | ✓ | +| [`disk_free_space()`](./builtins/filesystem/disk_free_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`disk_total_space()`](./builtins/filesystem/disk_total_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`file_exists()`](./builtins/filesystem/file_exists.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`fileatime()`](./builtins/filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filectime()`](./builtins/filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filegroup()`](./builtins/filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileinode()`](./builtins/filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`fileowner()`](./builtins/filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileperms()`](./builtins/filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filetype()`](./builtins/filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fnmatch()`](./builtins/filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`getcwd()`](./builtins/filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | +| [`getenv()`](./builtins/filesystem/getenv.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | +| [`glob()`](./builtins/filesystem/glob.md) | `(string $pattern): array` | `array` | ✓ | ✓ | +| [`is_dir()`](./builtins/filesystem/is_dir.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_executable()`](./builtins/filesystem/is_executable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_file()`](./builtins/filesystem/is_file.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_link()`](./builtins/filesystem/is_link.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_readable()`](./builtins/filesystem/is_readable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writable()`](./builtins/filesystem/is_writable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writeable()`](./builtins/filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./builtins/filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`link()`](./builtins/filesystem/link.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`linkinfo()`](./builtins/filesystem/linkinfo.md) | `(string $path): int` | `int` | ✓ | ✓ | +| [`lstat()`](./builtins/filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`mkdir()`](./builtins/filesystem/mkdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`pathinfo()`](./builtins/filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | ✓ | ✓ | +| [`putenv()`](./builtins/filesystem/putenv.md) | `(string $assignment): bool` | `bool` | ✓ | ✓ | +| [`readfile()`](./builtins/filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`readlink()`](./builtins/filesystem/readlink.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath()`](./builtins/filesystem/realpath.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath_cache_get()`](./builtins/filesystem/realpath_cache_get.md) | `(): array` | `array` | ✓ | ✓ | +| [`realpath_cache_size()`](./builtins/filesystem/realpath_cache_size.md) | `(): int` | `int` | ✓ | ✓ | +| [`rename()`](./builtins/filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`rmdir()`](./builtins/filesystem/rmdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`scandir()`](./builtins/filesystem/scandir.md) | `(string $directory): array` | `array` | ✓ | ✓ | +| [`stat()`](./builtins/filesystem/stat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`symlink()`](./builtins/filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`sys_get_temp_dir()`](./builtins/filesystem/sys_get_temp_dir.md) | `(): string` | `string` | ✓ | ✓ | +| [`tempnam()`](./builtins/filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | ✓ | ✓ | +| [`tmpfile()`](./builtins/filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`touch()`](./builtins/filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | +| [`umask()`](./builtins/filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | +| [`unlink()`](./builtins/filesystem/unlink.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`closedir()`](./builtins/io/closedir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`fclose()`](./builtins/io/fclose.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fdatasync()`](./builtins/io/fdatasync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`feof()`](./builtins/io/feof.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fflush()`](./builtins/io/fflush.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fgetc()`](./builtins/io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fgetcsv()`](./builtins/io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | +| [`fgets()`](./builtins/io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./builtins/io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | +| [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | +| [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | +| [`fpassthru()`](./builtins/io/fpassthru.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`fprintf()`](./builtins/io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`fputcsv()`](./builtins/io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | ✓ | ✓ | +| [`fread()`](./builtins/io/fread.md) | `(resource $stream, int $length): string` | `string` | ✓ | ✓ | +| [`fscanf()`](./builtins/io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`fseek()`](./builtins/io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | ✓ | ✓ | +| [`fstat()`](./builtins/io/fstat.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fsync()`](./builtins/io/fsync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`ftell()`](./builtins/io/ftell.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`ftruncate()`](./builtins/io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | ✓ | ✓ | +| [`fwrite()`](./builtins/io/fwrite.md) | `(resource $stream, string $data): int` | `int` | ✓ | ✓ | +| [`gethostbyaddr()`](./builtins/io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`gethostbyname()`](./builtins/io/gethostbyname.md) | `(string $hostname): string` | `string` | ✓ | ✓ | +| [`gethostname()`](./builtins/io/gethostname.md) | `(): string` | `string` | ✓ | ✓ | +| [`getprotobyname()`](./builtins/io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getprotobynumber()`](./builtins/io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyname()`](./builtins/io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyport()`](./builtins/io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`hash_file()`](./builtins/io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | ✓ | ✓ | +| [`opendir()`](./builtins/io/opendir.md) | `(string $directory): mixed` | `mixed` | ✓ | ✓ | +| [`readdir()`](./builtins/io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | ✓ | ✓ | +| [`rewind()`](./builtins/io/rewind.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`rewinddir()`](./builtins/io/rewinddir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`stream_bucket_make_writeable()`](./builtins/io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_new()`](./builtins/io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_create()`](./builtins/io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_default()`](./builtins/io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_options()`](./builtins/io/stream_context_get_options.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_get_params()`](./builtins/io/stream_context_get_params.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_set_default()`](./builtins/io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_set_option()`](./builtins/io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | ✓ | ✓ | +| [`stream_context_set_params()`](./builtins/io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | ✓ | ✓ | +| [`stream_copy_to_stream()`](./builtins/io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_register()`](./builtins/io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | ✓ | ✓ | +| [`stream_filter_remove()`](./builtins/io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | ✓ | ✓ | +| [`stream_get_contents()`](./builtins/io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_get_filters()`](./builtins/io/stream_get_filters.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_line()`](./builtins/io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | ✓ | ✓ | +| [`stream_get_meta_data()`](./builtins/io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | ✓ | ✓ | +| [`stream_get_transports()`](./builtins/io/stream_get_transports.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_wrappers()`](./builtins/io/stream_get_wrappers.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_is_local()`](./builtins/io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_isatty()`](./builtins/io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_resolve_include_path()`](./builtins/io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`stream_select()`](./builtins/io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_set_blocking()`](./builtins/io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | +| [`stream_set_chunk_size()`](./builtins/io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_read_buffer()`](./builtins/io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_timeout()`](./builtins/io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_set_write_buffer()`](./builtins/io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_socket_accept()`](./builtins/io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_client()`](./builtins/io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | ✓ | ✓ | +| [`stream_socket_get_name()`](./builtins/io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_pair()`](./builtins/io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_recvfrom()`](./builtins/io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_sendto()`](./builtins/io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_server()`](./builtins/io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_shutdown()`](./builtins/io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | ✓ | ✓ | +| [`stream_supports_lock()`](./builtins/io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_register()`](./builtins/io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_restore()`](./builtins/io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_unregister()`](./builtins/io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`vfprintf()`](./builtins/io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | ✓ | ✓ | +| [`json_decode()`](./builtins/json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | ✓ | ✓ | +| [`json_encode()`](./builtins/json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | ✓ | ✓ | +| [`json_last_error()`](./builtins/json/json_last_error.md) | `(): int` | `int` | ✓ | ✓ | +| [`json_last_error_msg()`](./builtins/json/json_last_error_msg.md) | `(): string` | `string` | ✓ | ✓ | +| [`json_validate()`](./builtins/json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`abs()`](./builtins/math/abs.md) | `(int $num): mixed` | `mixed` | ✓ | ✓ | +| [`acos()`](./builtins/math/acos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`asin()`](./builtins/math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan()`](./builtins/math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan2()`](./builtins/math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`ceil()`](./builtins/math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`clamp()`](./builtins/math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | +| [`cos()`](./builtins/math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`cosh()`](./builtins/math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`deg2rad()`](./builtins/math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`exp()`](./builtins/math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fdiv()`](./builtins/math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`floor()`](./builtins/math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fmod()`](./builtins/math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hypot()`](./builtins/math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | +| [`intdiv()`](./builtins/math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | +| [`is_finite()`](./builtins/math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_infinite()`](./builtins/math/is_infinite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_nan()`](./builtins/math/is_nan.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`log()`](./builtins/math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | ✓ | ✓ | +| [`log10()`](./builtins/math/log10.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`log2()`](./builtins/math/log2.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`max()`](./builtins/math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`mt_rand()`](./builtins/math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`pi()`](./builtins/math/pi.md) | `(): float` | `float` | ✓ | ✓ | +| [`pow()`](./builtins/math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | +| [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tan()`](./builtins/math/tan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tanh()`](./builtins/math/tanh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`buffer_new()`](./builtins/misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | +| [`defined()`](./builtins/misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | +| [`empty()`](./builtins/misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`header()`](./builtins/misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | ✓ | ✓ | +| [`http_response_code()`](./builtins/misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | ✓ | ✓ | +| [`isset()`](./builtins/misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | ✓ | ✓ | +| [`php_uname()`](./builtins/misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | ✓ | ✓ | +| [`phpversion()`](./builtins/misc/phpversion.md) | `(): string` | `string` | ✓ | ✓ | +| [`print_r()`](./builtins/misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | ✓ | ✓ | +| [`serialize()`](./builtins/misc/serialize.md) | `(mixed $value): string` | `string` | ✓ | — | +| [`unserialize()`](./builtins/misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | ✓ | — | +| [`unset()`](./builtins/misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | ✓ | ✓ | +| [`var_dump()`](./builtins/misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | ✓ | ✓ | +| [`ptr()`](./builtins/pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_get()`](./builtins/pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_is_null()`](./builtins/pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | ✓ | ✓ | +| [`ptr_null()`](./builtins/pointer/ptr_null.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_offset()`](./builtins/pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_read16()`](./builtins/pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read32()`](./builtins/pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read8()`](./builtins/pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read_string()`](./builtins/pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | ✓ | ✓ | +| [`ptr_set()`](./builtins/pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | ✓ | ✓ | +| [`ptr_sizeof()`](./builtins/pointer/ptr_sizeof.md) | `(string $type): int` | `int` | ✓ | ✓ | +| [`ptr_write16()`](./builtins/pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write32()`](./builtins/pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write8()`](./builtins/pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write_string()`](./builtins/pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | ✓ | ✓ | +| [`zval_free()`](./builtins/pointer/zval_free.md) | `(pointer $zval): void` | `void` | ✓ | — | +| [`zval_pack()`](./builtins/pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | ✓ | — | +| [`zval_type()`](./builtins/pointer/zval_type.md) | `(pointer $zval): int` | `int` | ✓ | — | +| [`zval_unpack()`](./builtins/pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | ✓ | — | +| [`die()`](./builtins/process/die.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`exec()`](./builtins/process/exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`exit()`](./builtins/process/exit.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`passthru()`](./builtins/process/passthru.md) | `(string $command): void` | `void` | ✓ | ✓ | +| [`pclose()`](./builtins/process/pclose.md) | `(resource $handle): int` | `int` | ✓ | ✓ | +| [`popen()`](./builtins/process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | ✓ | ✓ | +| [`readline()`](./builtins/process/readline.md) | `(string $prompt = null): mixed` | `mixed` | ✓ | ✓ | +| [`shell_exec()`](./builtins/process/shell_exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`sleep()`](./builtins/process/sleep.md) | `(int $seconds): int` | `int` | ✓ | ✓ | +| [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | ✓ | ✓ | +| [`mb_ereg_match()`](./builtins/regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | +| [`preg_match()`](./builtins/regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match_all()`](./builtins/regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | ✓ | ✓ | +| [`preg_replace()`](./builtins/regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_replace_callback()`](./builtins/regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_split()`](./builtins/regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | ✓ | ✓ | +| [`iterator_apply()`](./builtins/spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | ✓ | ✓ | +| [`iterator_count()`](./builtins/spl/iterator_count.md) | `(traversable $iterator): int` | `int` | ✓ | ✓ | +| [`iterator_to_array()`](./builtins/spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | ✓ | ✓ | +| [`spl_autoload()`](./builtins/spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | ✓ | ✓ | +| [`spl_autoload_call()`](./builtins/spl/spl_autoload_call.md) | `(string $class): void` | `void` | ✓ | ✓ | +| [`spl_autoload_extensions()`](./builtins/spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | ✓ | ✓ | +| [`spl_autoload_functions()`](./builtins/spl/spl_autoload_functions.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_autoload_register()`](./builtins/spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | ✓ | ✓ | +| [`spl_autoload_unregister()`](./builtins/spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | ✓ | ✓ | +| [`spl_classes()`](./builtins/spl/spl_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_object_hash()`](./builtins/spl/spl_object_hash.md) | `(object $object): string` | `string` | ✓ | ✓ | +| [`spl_object_id()`](./builtins/spl/spl_object_id.md) | `(object $object): int` | `int` | ✓ | ✓ | +| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_append()`](./builtins/streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_bucket_prepend()`](./builtins/streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`addslashes()`](./builtins/string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_encode()`](./builtins/string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`bin2hex()`](./builtins/string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`chop()`](./builtins/string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`chr()`](./builtins/string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`crc32()`](./builtins/string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | +| [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | +| [`gzcompress()`](./builtins/string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzdeflate()`](./builtins/string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzinflate()`](./builtins/string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`gzuncompress()`](./builtins/string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`hash()`](./builtins/string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_algos()`](./builtins/string/hash_algos.md) | `(): array` | `array` | ✓ | ✓ | +| [`hash_copy()`](./builtins/string/hash_copy.md) | `(resource $context): mixed` | `mixed` | ✓ | ✓ | +| [`hash_equals()`](./builtins/string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | ✓ | ✓ | +| [`hash_final()`](./builtins/string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_hmac()`](./builtins/string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_init()`](./builtins/string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | ✓ | ✓ | +| [`hash_update()`](./builtins/string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | ✓ | ✓ | +| [`hex2bin()`](./builtins/string/hex2bin.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`html_entity_decode()`](./builtins/string/html_entity_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`implode()`](./builtins/string/implode.md) | `(string $separator, array $array = null): string` | `string` | ✓ | ✓ | +| [`inet_ntop()`](./builtins/string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`inet_pton()`](./builtins/string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`ip2long()`](./builtins/string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | +| [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`nl2br()`](./builtins/string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | +| [`ord()`](./builtins/string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | +| [`printf()`](./builtins/string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`rawurldecode()`](./builtins/string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rawurlencode()`](./builtins/string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rtrim()`](./builtins/string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`sha1()`](./builtins/string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`sprintf()`](./builtins/string/sprintf.md) | `(string $format, ...$values): string` | `string` | ✓ | ✓ | +| [`sscanf()`](./builtins/string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`str_contains()`](./builtins/string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ends_with()`](./builtins/string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ireplace()`](./builtins/string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_pad()`](./builtins/string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | ✓ | ✓ | +| [`str_repeat()`](./builtins/string/str_repeat.md) | `(string $string, int $times): string` | `string` | ✓ | ✓ | +| [`str_replace()`](./builtins/string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | +| [`str_starts_with()`](./builtins/string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`strcasecmp()`](./builtins/string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`strcmp()`](./builtins/string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripslashes()`](./builtins/string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strlen()`](./builtins/string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strpos()`](./builtins/string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strrev()`](./builtins/string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strrpos()`](./builtins/string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | ✓ | ✓ | +| [`strtolower()`](./builtins/string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtoupper()`](./builtins/string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`substr()`](./builtins/string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`ucfirst()`](./builtins/string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`ucwords()`](./builtins/string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | ✓ | ✓ | +| [`urldecode()`](./builtins/string/urldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`urlencode()`](./builtins/string/urlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`vprintf()`](./builtins/string/vprintf.md) | `(string $format, array $values): int` | `int` | ✓ | ✓ | +| [`vsprintf()`](./builtins/string/vsprintf.md) | `(string $format, array $values): string` | `string` | ✓ | ✓ | +| [`wordwrap()`](./builtins/string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | ✓ | ✓ | +| [`boolval()`](./builtins/type/boolval.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`ctype_alnum()`](./builtins/type/ctype_alnum.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_alpha()`](./builtins/type/ctype_alpha.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_digit()`](./builtins/type/ctype_digit.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_space()`](./builtins/type/ctype_space.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`floatval()`](./builtins/type/floatval.md) | `(mixed $value): float` | `float` | ✓ | ✓ | +| [`get_resource_id()`](./builtins/type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | +| [`get_resource_type()`](./builtins/type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | +| [`gettype()`](./builtins/type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | +| [`intval()`](./builtins/type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`is_array()`](./builtins/type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_bool()`](./builtins/type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_callable()`](./builtins/type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_float()`](./builtins/type/is_float.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_int()`](./builtins/type/is_int.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_iterable()`](./builtins/type/is_iterable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_null()`](./builtins/type/is_null.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_numeric()`](./builtins/type/is_numeric.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_object()`](./builtins/type/is_object.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_resource()`](./builtins/type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_scalar()`](./builtins/type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_string()`](./builtins/type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./builtins/type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index f2f4d44092..3cee753978 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -7,68 +7,68 @@ sidebar: ## Array builtins -| Function | Signature | Returns | -|---|---|---| -| [`array_all()`](./array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_any()`](./array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | -| [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | -| [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_fill()`](./array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | -| [`array_fill_keys()`](./array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | -| [`array_filter()`](./array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | -| [`array_find()`](./array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | -| [`array_flip()`](./array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_intersect_assoc()`](./array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_intersect_key()`](./array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_is_list()`](./array/array_is_list.md) | `(mixed $array): bool` | `bool` | -| [`array_key_exists()`](./array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | -| [`array_key_first()`](./array/array_key_first.md) | `(array $array): mixed` | `mixed` | -| [`array_key_last()`](./array/array_key_last.md) | `(array $array): mixed` | `mixed` | -| [`array_keys()`](./array/array_keys.md) | `(array $array): array` | `array` | -| [`array_map()`](./array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | -| [`array_merge()`](./array/array_merge.md) | `(...$arrays): array` | `array` | -| [`array_merge_recursive()`](./array/array_merge_recursive.md) | `(...$arrays): array` | `array` | -| [`array_multisort()`](./array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | -| [`array_pad()`](./array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | -| [`array_pop()`](./array/array_pop.md) | `(array $array): mixed` | `mixed` | -| [`array_product()`](./array/array_product.md) | `(array $array): int` | `int` | -| [`array_push()`](./array/array_push.md) | `(array $array, ...$values): void` | `void` | -| [`array_rand()`](./array/array_rand.md) | `(array $array): int` | `int` | -| [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | -| [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | -| [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | -| [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | -| [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_uintersect()`](./array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_unique()`](./array/array_unique.md) | `(array $array): array` | `array` | -| [`array_unshift()`](./array/array_unshift.md) | `(array $array, ...$values): int` | `int` | -| [`array_values()`](./array/array_values.md) | `(array $array): array` | `array` | -| [`array_walk()`](./array/array_walk.md) | `(array $array, callable $callback): void` | `void` | -| [`array_walk_recursive()`](./array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | -| [`arsort()`](./array/arsort.md) | `(array $array): bool` | `bool` | -| [`asort()`](./array/asort.md) | `(array $array): bool` | `bool` | -| [`call_user_func()`](./array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`count()`](./array/count.md) | `(array $value, int $mode = 0): int` | `int` | -| [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | -| [`krsort()`](./array/krsort.md) | `(array $array): bool` | `bool` | -| [`ksort()`](./array/ksort.md) | `(array $array): bool` | `bool` | -| [`natcasesort()`](./array/natcasesort.md) | `(array $array): bool` | `bool` | -| [`natsort()`](./array/natsort.md) | `(array $array): bool` | `bool` | -| [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | -| [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | -| [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | -| [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | -| [`uasort()`](./array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`uksort()`](./array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`usort()`](./array/usort.md) | `(array $array, callable $callback): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`array_all()`](./array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_any()`](./array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | +| [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_fill()`](./array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_fill_keys()`](./array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_filter()`](./array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | ✓ | ✓ | +| [`array_find()`](./array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | ✓ | — | +| [`array_flip()`](./array/array_flip.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_intersect_assoc()`](./array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_intersect_key()`](./array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_is_list()`](./array/array_is_list.md) | `(mixed $array): bool` | `bool` | ✓ | — | +| [`array_key_exists()`](./array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | ✓ | ✓ | +| [`array_key_first()`](./array/array_key_first.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_key_last()`](./array/array_key_last.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_keys()`](./array/array_keys.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_map()`](./array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge()`](./array/array_merge.md) | `(...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge_recursive()`](./array/array_merge_recursive.md) | `(...$arrays): array` | `array` | ✓ | — | +| [`array_multisort()`](./array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | ✓ | — | +| [`array_pad()`](./array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_pop()`](./array/array_pop.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_product()`](./array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_push()`](./array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_rand()`](./array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | +| [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_uintersect()`](./array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_unique()`](./array/array_unique.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_unshift()`](./array/array_unshift.md) | `(array $array, ...$values): int` | `int` | ✓ | ✓ | +| [`array_values()`](./array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_walk()`](./array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | +| [`array_walk_recursive()`](./array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | +| [`arsort()`](./array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`asort()`](./array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`call_user_func()`](./array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | +| [`call_user_func_array()`](./array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | +| [`count()`](./array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`krsort()`](./array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array/array_all.md b/docs/php/builtins/array/array_all.md index 20fa87b4bd..f440ee7aa7 100644 --- a/docs/php/builtins/array/array_all.md +++ b/docs/php/builtins/array/array_all.md @@ -19,6 +19,11 @@ Returns true when every array element satisfies the predicate callback. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_any.md b/docs/php/builtins/array/array_any.md index c8c8658fad..1ba9cee69a 100644 --- a/docs/php/builtins/array/array_any.md +++ b/docs/php/builtins/array/array_any.md @@ -19,6 +19,11 @@ Returns true when at least one array element satisfies the predicate callback. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_chunk.md b/docs/php/builtins/array/array_chunk.md index 3bf1f31b8c..ec6fd9120b 100644 --- a/docs/php/builtins/array/array_chunk.md +++ b/docs/php/builtins/array/array_chunk.md @@ -19,6 +19,11 @@ Splits an array into chunks of the given size. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_column.md b/docs/php/builtins/array/array_column.md index c4f21c1c22..50f1476463 100644 --- a/docs/php/builtins/array/array_column.md +++ b/docs/php/builtins/array/array_column.md @@ -19,6 +19,11 @@ Returns the values from a single column of an array of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_combine.md b/docs/php/builtins/array/array_combine.md index 18b4617d4d..4b1a732456 100644 --- a/docs/php/builtins/array/array_combine.md +++ b/docs/php/builtins/array/array_combine.md @@ -19,6 +19,11 @@ Creates an array by using one array for keys and another for values. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff.md b/docs/php/builtins/array/array_diff.md index ea514aa384..c6b87a11e7 100644 --- a/docs/php/builtins/array/array_diff.md +++ b/docs/php/builtins/array/array_diff.md @@ -19,6 +19,11 @@ Computes the difference of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff_assoc.md b/docs/php/builtins/array/array_diff_assoc.md index 488a870134..a8eac7c79c 100644 --- a/docs/php/builtins/array/array_diff_assoc.md +++ b/docs/php/builtins/array/array_diff_assoc.md @@ -19,6 +19,11 @@ Computes the difference of arrays with additional index check. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff_key.md b/docs/php/builtins/array/array_diff_key.md index 2fd900fa8a..1e8ee4a3b7 100644 --- a/docs/php/builtins/array/array_diff_key.md +++ b/docs/php/builtins/array/array_diff_key.md @@ -19,6 +19,11 @@ Computes the difference of arrays using keys for comparison. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_fill.md b/docs/php/builtins/array/array_fill.md index c6140e68f5..e1b269d0a4 100644 --- a/docs/php/builtins/array/array_fill.md +++ b/docs/php/builtins/array/array_fill.md @@ -20,6 +20,11 @@ Fill an array with values. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_fill_keys.md b/docs/php/builtins/array/array_fill_keys.md index d8c4197c76..5cdd56b8d8 100644 --- a/docs/php/builtins/array/array_fill_keys.md +++ b/docs/php/builtins/array/array_fill_keys.md @@ -19,6 +19,11 @@ Fill an array with values, specifying keys. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_filter.md b/docs/php/builtins/array/array_filter.md index 0aca1d8604..50a9b378c6 100644 --- a/docs/php/builtins/array/array_filter.md +++ b/docs/php/builtins/array/array_filter.md @@ -20,6 +20,11 @@ Filters elements of an array using a callback function. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_find.md b/docs/php/builtins/array/array_find.md index e21471ea11..1ed4954d8b 100644 --- a/docs/php/builtins/array/array_find.md +++ b/docs/php/builtins/array/array_find.md @@ -19,6 +19,11 @@ Returns the first element satisfying a predicate callback, or null. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_flip.md b/docs/php/builtins/array/array_flip.md index d4a791bde8..027d087f4f 100644 --- a/docs/php/builtins/array/array_flip.md +++ b/docs/php/builtins/array/array_flip.md @@ -18,6 +18,11 @@ Exchanges all keys with their associated values in an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect.md b/docs/php/builtins/array/array_intersect.md index 55c5ce3ab5..671ae97944 100644 --- a/docs/php/builtins/array/array_intersect.md +++ b/docs/php/builtins/array/array_intersect.md @@ -19,6 +19,11 @@ Computes the intersection of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect_assoc.md b/docs/php/builtins/array/array_intersect_assoc.md index 889e440ea1..434a4e9577 100644 --- a/docs/php/builtins/array/array_intersect_assoc.md +++ b/docs/php/builtins/array/array_intersect_assoc.md @@ -19,6 +19,11 @@ Computes the intersection of arrays with additional index check. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect_key.md b/docs/php/builtins/array/array_intersect_key.md index d9fb6a57c0..bd76ac445a 100644 --- a/docs/php/builtins/array/array_intersect_key.md +++ b/docs/php/builtins/array/array_intersect_key.md @@ -19,6 +19,11 @@ Computes the intersection of arrays using keys for comparison. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_is_list.md b/docs/php/builtins/array/array_is_list.md index c5264801c8..d689508738 100644 --- a/docs/php/builtins/array/array_is_list.md +++ b/docs/php/builtins/array/array_is_list.md @@ -18,6 +18,11 @@ Checks whether an array is a list (sequential 0-based integer keys). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_exists.md b/docs/php/builtins/array/array_key_exists.md index bee1b864a3..33b65b9050 100644 --- a/docs/php/builtins/array/array_key_exists.md +++ b/docs/php/builtins/array/array_key_exists.md @@ -19,6 +19,11 @@ Checks if the given key or index exists in the array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_first.md b/docs/php/builtins/array/array_key_first.md index 64e4ab6b28..8b430fe25c 100644 --- a/docs/php/builtins/array/array_key_first.md +++ b/docs/php/builtins/array/array_key_first.md @@ -18,6 +18,11 @@ Gets the first key of an array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_last.md b/docs/php/builtins/array/array_key_last.md index 50da0ddb35..ca00017d7d 100644 --- a/docs/php/builtins/array/array_key_last.md +++ b/docs/php/builtins/array/array_key_last.md @@ -18,6 +18,11 @@ Gets the last key of an array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_keys.md b/docs/php/builtins/array/array_keys.md index dd9eab185a..3063349c30 100644 --- a/docs/php/builtins/array/array_keys.md +++ b/docs/php/builtins/array/array_keys.md @@ -18,6 +18,11 @@ Returns all the keys of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_map.md b/docs/php/builtins/array/array_map.md index c1dcfac6ca..9d07785976 100644 --- a/docs/php/builtins/array/array_map.md +++ b/docs/php/builtins/array/array_map.md @@ -20,6 +20,11 @@ Applies a callback to the elements of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_merge.md b/docs/php/builtins/array/array_merge.md index 944a0185fb..35e27a8b00 100644 --- a/docs/php/builtins/array/array_merge.md +++ b/docs/php/builtins/array/array_merge.md @@ -18,6 +18,11 @@ Merges the elements of two arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_merge_recursive.md b/docs/php/builtins/array/array_merge_recursive.md index 4cccbd6f2a..43c92f9e3f 100644 --- a/docs/php/builtins/array/array_merge_recursive.md +++ b/docs/php/builtins/array/array_merge_recursive.md @@ -18,6 +18,11 @@ Recursively merges two arrays, combining scalar collisions into lists. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_multisort.md b/docs/php/builtins/array/array_multisort.md index 0d03600acb..0cce61476a 100644 --- a/docs/php/builtins/array/array_multisort.md +++ b/docs/php/builtins/array/array_multisort.md @@ -19,6 +19,11 @@ Sorts multiple arrays or multi-dimensional arrays. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_pad.md b/docs/php/builtins/array/array_pad.md index 57e9cdf9fc..bb2a4021aa 100644 --- a/docs/php/builtins/array/array_pad.md +++ b/docs/php/builtins/array/array_pad.md @@ -20,6 +20,11 @@ Pads an array to the specified length with a value. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_pop.md b/docs/php/builtins/array/array_pop.md index 640e0c5c4d..4857dc9d13 100644 --- a/docs/php/builtins/array/array_pop.md +++ b/docs/php/builtins/array/array_pop.md @@ -18,6 +18,11 @@ Pops the element off the end of array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_product.md b/docs/php/builtins/array/array_product.md index bcf9ca0e28..f7980f2795 100644 --- a/docs/php/builtins/array/array_product.md +++ b/docs/php/builtins/array/array_product.md @@ -18,6 +18,11 @@ Calculate the product of values in an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_push.md b/docs/php/builtins/array/array_push.md index 1024eb9e30..e964d9cf9d 100644 --- a/docs/php/builtins/array/array_push.md +++ b/docs/php/builtins/array/array_push.md @@ -19,6 +19,11 @@ Pushes one or more elements onto the end of array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_rand.md b/docs/php/builtins/array/array_rand.md index 6194bc5301..fa845001c4 100644 --- a/docs/php/builtins/array/array_rand.md +++ b/docs/php/builtins/array/array_rand.md @@ -18,6 +18,11 @@ Pick one or more random keys out of an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_reduce.md b/docs/php/builtins/array/array_reduce.md index 63782c9e33..e0bcba350a 100644 --- a/docs/php/builtins/array/array_reduce.md +++ b/docs/php/builtins/array/array_reduce.md @@ -20,6 +20,11 @@ Iteratively reduces an array to a single value using a callback function. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_replace.md b/docs/php/builtins/array/array_replace.md index f5964ad250..accd854023 100644 --- a/docs/php/builtins/array/array_replace.md +++ b/docs/php/builtins/array/array_replace.md @@ -19,6 +19,11 @@ Replaces elements from passed arrays into the first array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_replace_recursive.md b/docs/php/builtins/array/array_replace_recursive.md index 379edc8897..56e4d1cd41 100644 --- a/docs/php/builtins/array/array_replace_recursive.md +++ b/docs/php/builtins/array/array_replace_recursive.md @@ -19,6 +19,11 @@ Replaces elements from passed arrays into the first array recursively. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_reverse.md b/docs/php/builtins/array/array_reverse.md index 6bd856cbd5..303c296e47 100644 --- a/docs/php/builtins/array/array_reverse.md +++ b/docs/php/builtins/array/array_reverse.md @@ -18,6 +18,11 @@ Returns an array with the elements in reverse order. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_search.md b/docs/php/builtins/array/array_search.md index f2e1a42180..5b0617bc97 100644 --- a/docs/php/builtins/array/array_search.md +++ b/docs/php/builtins/array/array_search.md @@ -20,6 +20,11 @@ Searches the array for a given value and returns the first corresponding key if **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_shift.md b/docs/php/builtins/array/array_shift.md index bd4d8dc9b5..031a7af292 100644 --- a/docs/php/builtins/array/array_shift.md +++ b/docs/php/builtins/array/array_shift.md @@ -18,6 +18,11 @@ Shifts an element off the beginning of array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_slice.md b/docs/php/builtins/array/array_slice.md index 8abb0326ca..792f77b8b9 100644 --- a/docs/php/builtins/array/array_slice.md +++ b/docs/php/builtins/array/array_slice.md @@ -20,6 +20,11 @@ Extracts a slice of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_splice.md b/docs/php/builtins/array/array_splice.md index eba2b7a107..29eb4e5d37 100644 --- a/docs/php/builtins/array/array_splice.md +++ b/docs/php/builtins/array/array_splice.md @@ -20,6 +20,11 @@ Removes a portion of the array and replaces it with something else. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_sum.md b/docs/php/builtins/array/array_sum.md index 775aceb2fc..c40224640b 100644 --- a/docs/php/builtins/array/array_sum.md +++ b/docs/php/builtins/array/array_sum.md @@ -18,6 +18,11 @@ Calculate the sum of values in an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_udiff.md b/docs/php/builtins/array/array_udiff.md index 29fed10285..0e7f340810 100644 --- a/docs/php/builtins/array/array_udiff.md +++ b/docs/php/builtins/array/array_udiff.md @@ -20,6 +20,11 @@ Computes the difference of arrays using a callback comparator. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_uintersect.md b/docs/php/builtins/array/array_uintersect.md index 9ae4631678..44f48f2442 100644 --- a/docs/php/builtins/array/array_uintersect.md +++ b/docs/php/builtins/array/array_uintersect.md @@ -20,6 +20,11 @@ Computes the intersection of arrays using a callback comparator. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_unique.md b/docs/php/builtins/array/array_unique.md index f69c66df25..1e7aa64610 100644 --- a/docs/php/builtins/array/array_unique.md +++ b/docs/php/builtins/array/array_unique.md @@ -18,6 +18,11 @@ Removes duplicate values from an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_unshift.md b/docs/php/builtins/array/array_unshift.md index cd8fa5e2ea..1895f522aa 100644 --- a/docs/php/builtins/array/array_unshift.md +++ b/docs/php/builtins/array/array_unshift.md @@ -19,6 +19,11 @@ Prepends one or more elements to the beginning of an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_values.md b/docs/php/builtins/array/array_values.md index 4e3a852a99..1c3b535acc 100644 --- a/docs/php/builtins/array/array_values.md +++ b/docs/php/builtins/array/array_values.md @@ -18,6 +18,11 @@ Returns all the values of an array, re-indexed numerically. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_walk.md b/docs/php/builtins/array/array_walk.md index 0b6a72bec4..02fac18576 100644 --- a/docs/php/builtins/array/array_walk.md +++ b/docs/php/builtins/array/array_walk.md @@ -19,6 +19,11 @@ Applies a user function to every member of an array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_walk_recursive.md b/docs/php/builtins/array/array_walk_recursive.md index 7de26032cf..a68806d5a9 100644 --- a/docs/php/builtins/array/array_walk_recursive.md +++ b/docs/php/builtins/array/array_walk_recursive.md @@ -19,6 +19,11 @@ Applies a user function recursively to every member of an array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/arsort.md b/docs/php/builtins/array/arsort.md index b13789bfc9..dac5fc4854 100644 --- a/docs/php/builtins/array/arsort.md +++ b/docs/php/builtins/array/arsort.md @@ -18,6 +18,11 @@ Sorts an array in descending order and maintains index association. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/asort.md b/docs/php/builtins/array/asort.md index cf3377b9de..6d01495b69 100644 --- a/docs/php/builtins/array/asort.md +++ b/docs/php/builtins/array/asort.md @@ -18,6 +18,11 @@ Sorts an array and maintains index association. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/asort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/call_user_func.md b/docs/php/builtins/array/call_user_func.md index 937fc917e6..77e6f8958a 100644 --- a/docs/php/builtins/array/call_user_func.md +++ b/docs/php/builtins/array/call_user_func.md @@ -19,6 +19,11 @@ Calls a callback with the given arguments. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/call_user_func_array.md b/docs/php/builtins/array/call_user_func_array.md index e6cd18b3a5..aaeac63b51 100644 --- a/docs/php/builtins/array/call_user_func_array.md +++ b/docs/php/builtins/array/call_user_func_array.md @@ -19,6 +19,11 @@ Calls a callback with an array of parameters. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/count.md b/docs/php/builtins/array/count.md index a865fb07d7..d443759519 100644 --- a/docs/php/builtins/array/count.md +++ b/docs/php/builtins/array/count.md @@ -19,6 +19,11 @@ Counts all elements in an array or Countable object. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/count.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/in_array.md b/docs/php/builtins/array/in_array.md index 9064877063..7c4e70009b 100644 --- a/docs/php/builtins/array/in_array.md +++ b/docs/php/builtins/array/in_array.md @@ -20,6 +20,11 @@ Checks if a value exists in an array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/krsort.md b/docs/php/builtins/array/krsort.md index 62873b00b3..d013c99cc1 100644 --- a/docs/php/builtins/array/krsort.md +++ b/docs/php/builtins/array/krsort.md @@ -18,6 +18,11 @@ Sorts an array by key in descending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/ksort.md b/docs/php/builtins/array/ksort.md index 558cadc3c5..3a14a476be 100644 --- a/docs/php/builtins/array/ksort.md +++ b/docs/php/builtins/array/ksort.md @@ -18,6 +18,11 @@ Sorts an array by key in ascending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/natcasesort.md b/docs/php/builtins/array/natcasesort.md index b4f4825c81..e2ef2d6c40 100644 --- a/docs/php/builtins/array/natcasesort.md +++ b/docs/php/builtins/array/natcasesort.md @@ -18,6 +18,11 @@ Sorts an array using a case-insensitive natural order algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/natsort.md b/docs/php/builtins/array/natsort.md index 81eb9f565f..3c5090bee1 100644 --- a/docs/php/builtins/array/natsort.md +++ b/docs/php/builtins/array/natsort.md @@ -18,6 +18,11 @@ Sorts an array using a natural order algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/range.md b/docs/php/builtins/array/range.md index 306f693278..f43f4a4e28 100644 --- a/docs/php/builtins/array/range.md +++ b/docs/php/builtins/array/range.md @@ -19,6 +19,11 @@ Create an array containing a range of elements. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/range.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/rsort.md b/docs/php/builtins/array/rsort.md index 9741571ad5..d412aa6826 100644 --- a/docs/php/builtins/array/rsort.md +++ b/docs/php/builtins/array/rsort.md @@ -18,6 +18,11 @@ Sorts an array in descending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/shuffle.md b/docs/php/builtins/array/shuffle.md index 35eeb0a290..557dcc6d31 100644 --- a/docs/php/builtins/array/shuffle.md +++ b/docs/php/builtins/array/shuffle.md @@ -18,6 +18,11 @@ Shuffles an array into random order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/sort.md b/docs/php/builtins/array/sort.md index bd02d22310..1857e4df36 100644 --- a/docs/php/builtins/array/sort.md +++ b/docs/php/builtins/array/sort.md @@ -18,6 +18,11 @@ Sorts an array in ascending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/sort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/uasort.md b/docs/php/builtins/array/uasort.md index 12cd0cbf14..3019bb83e8 100644 --- a/docs/php/builtins/array/uasort.md +++ b/docs/php/builtins/array/uasort.md @@ -19,6 +19,11 @@ Sorts an array with a user-defined comparison function and maintains index assoc **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/uksort.md b/docs/php/builtins/array/uksort.md index 4968295de1..f81b0650df 100644 --- a/docs/php/builtins/array/uksort.md +++ b/docs/php/builtins/array/uksort.md @@ -19,6 +19,11 @@ Sorts an array by keys using a user-defined comparison function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/usort.md b/docs/php/builtins/array/usort.md index 76afdff10f..f90c2e23a0 100644 --- a/docs/php/builtins/array/usort.md +++ b/docs/php/builtins/array/usort.md @@ -19,6 +19,11 @@ Sorts an array by values using a user-defined comparison function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/usort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/buffer.md b/docs/php/builtins/buffer.md index 99cf5e5037..7e4d057561 100644 --- a/docs/php/builtins/buffer.md +++ b/docs/php/builtins/buffer.md @@ -7,7 +7,7 @@ sidebar: ## Buffer builtins -| Function | Signature | Returns | -|---|---|---| -| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | -| [`buffer_len()`](./buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_len()`](./buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/buffer/buffer_free.md b/docs/php/builtins/buffer/buffer_free.md index 136dba1345..88169f317c 100644 --- a/docs/php/builtins/buffer/buffer_free.md +++ b/docs/php/builtins/buffer/buffer_free.md @@ -18,6 +18,11 @@ Lowers `buffer_free()` through the direct buffer opcode helper. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/buffer/buffer_len.md b/docs/php/builtins/buffer/buffer_len.md index 07ff13516b..0b7d6d7d3e 100644 --- a/docs/php/builtins/buffer/buffer_len.md +++ b/docs/php/builtins/buffer/buffer_len.md @@ -18,6 +18,11 @@ Lowers `buffer_len()` through the direct buffer opcode helper. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class.md b/docs/php/builtins/class.md index 38b0b23bd1..0118715697 100644 --- a/docs/php/builtins/class.md +++ b/docs/php/builtins/class.md @@ -7,24 +7,28 @@ sidebar: ## Class builtins -| Function | Signature | Returns | -|---|---|---| -| [`class_alias()`](./class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | -| [`class_attribute_args()`](./class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | -| [`class_attribute_names()`](./class/class_attribute_names.md) | `(string $class_name): array` | `array` | -| [`class_exists()`](./class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | -| [`class_get_attributes()`](./class/class_get_attributes.md) | `(string $class_name): array` | `array` | -| [`class_implements()`](./class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_parents()`](./class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_uses()`](./class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`enum_exists()`](./class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | -| [`function_exists()`](./class/function_exists.md) | `(string $function): bool` | `bool` | -| [`get_class()`](./class/get_class.md) | `(object $object = null): string` | `string` | -| [`get_declared_classes()`](./class/get_declared_classes.md) | `(): array` | `array` | -| [`get_declared_interfaces()`](./class/get_declared_interfaces.md) | `(): array` | `array` | -| [`get_declared_traits()`](./class/get_declared_traits.md) | `(): array` | `array` | -| [`get_parent_class()`](./class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | -| [`interface_exists()`](./class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | -| [`is_a()`](./class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | -| [`is_subclass_of()`](./class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | -| [`trait_exists()`](./class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`class_alias()`](./class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_attribute_args()`](./class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | ✓ | ✓ | +| [`class_attribute_names()`](./class/class_attribute_names.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_exists()`](./class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_get_attributes()`](./class/class_get_attributes.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_implements()`](./class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_parents()`](./class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_uses()`](./class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`enum_exists()`](./class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`function_exists()`](./class/function_exists.md) | `(string $function): bool` | `bool` | ✓ | ✓ | +| [`get_called_class()`](./class/get_called_class.md) | `(): mixed` | `mixed` | — | ✓ | +| [`get_class()`](./class/get_class.md) | `(object $object = null): string` | `string` | ✓ | ✓ | +| [`get_class_methods()`](./class/get_class_methods.md) | `(mixed $object_or_class): mixed` | `mixed` | — | ✓ | +| [`get_class_vars()`](./class/get_class_vars.md) | `(mixed $class): mixed` | `mixed` | — | ✓ | +| [`get_declared_classes()`](./class/get_declared_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_interfaces()`](./class/get_declared_interfaces.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_traits()`](./class/get_declared_traits.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_object_vars()`](./class/get_object_vars.md) | `(mixed $object): mixed` | `mixed` | — | ✓ | +| [`get_parent_class()`](./class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | ✓ | ✓ | +| [`interface_exists()`](./class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`is_a()`](./class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | ✓ | ✓ | +| [`is_subclass_of()`](./class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | ✓ | ✓ | +| [`trait_exists()`](./class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/class/class_alias.md b/docs/php/builtins/class/class_alias.md index 3afbf9fbd2..d08c8cd9e3 100644 --- a/docs/php/builtins/class/class_alias.md +++ b/docs/php/builtins/class/class_alias.md @@ -20,6 +20,11 @@ Creates an alias for a class. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_args.md b/docs/php/builtins/class/class_attribute_args.md index 554d29d965..6302ef4669 100644 --- a/docs/php/builtins/class/class_attribute_args.md +++ b/docs/php/builtins/class/class_attribute_args.md @@ -19,6 +19,11 @@ Returns the constructor arguments of a named attribute applied to a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_names.md b/docs/php/builtins/class/class_attribute_names.md index 78fb12b96c..4d5120b20b 100644 --- a/docs/php/builtins/class/class_attribute_names.md +++ b/docs/php/builtins/class/class_attribute_names.md @@ -18,6 +18,11 @@ Returns the list of attribute names applied to a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_exists.md b/docs/php/builtins/class/class_exists.md index 2fad0f1dcd..e8634808ba 100644 --- a/docs/php/builtins/class/class_exists.md +++ b/docs/php/builtins/class/class_exists.md @@ -19,6 +19,11 @@ Checks whether the given class has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_get_attributes.md b/docs/php/builtins/class/class_get_attributes.md index 1baa768aab..e02fab8ba2 100644 --- a/docs/php/builtins/class/class_get_attributes.md +++ b/docs/php/builtins/class/class_get_attributes.md @@ -18,6 +18,11 @@ Returns an array of ReflectionAttribute objects for all attributes of a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_implements.md b/docs/php/builtins/class/class_implements.md index 138b873524..ae568fc5ed 100644 --- a/docs/php/builtins/class/class_implements.md +++ b/docs/php/builtins/class/class_implements.md @@ -19,6 +19,11 @@ Returns the interfaces which are implemented by the given class or its parents. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_parents.md b/docs/php/builtins/class/class_parents.md index 8bf00fa244..1a180532e7 100644 --- a/docs/php/builtins/class/class_parents.md +++ b/docs/php/builtins/class/class_parents.md @@ -19,6 +19,11 @@ Returns the parent classes of the given class. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_uses.md b/docs/php/builtins/class/class_uses.md index b2da4b4185..b0bb348d89 100644 --- a/docs/php/builtins/class/class_uses.md +++ b/docs/php/builtins/class/class_uses.md @@ -19,6 +19,11 @@ Returns the traits used by the given class. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/enum_exists.md b/docs/php/builtins/class/enum_exists.md index a1389d8667..4d556eedd1 100644 --- a/docs/php/builtins/class/enum_exists.md +++ b/docs/php/builtins/class/enum_exists.md @@ -19,6 +19,11 @@ Checks if the enum has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/function_exists.md b/docs/php/builtins/class/function_exists.md index e358d8e6d0..48a003c47f 100644 --- a/docs/php/builtins/class/function_exists.md +++ b/docs/php/builtins/class/function_exists.md @@ -18,6 +18,11 @@ Returns true if the given function has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_called_class.md b/docs/php/builtins/class/get_called_class.md new file mode 100644 index 0000000000..15b96f8633 --- /dev/null +++ b/docs/php/builtins/class/get_called_class.md @@ -0,0 +1,25 @@ +--- +title: "get_called_class()" +description: "get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 76 +--- + +## get_called_class() + +```php +function get_called_class(): mixed +``` + +get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: none. + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_class.md b/docs/php/builtins/class/get_class.md index 082cda5c3e..ecd0ae69f9 100644 --- a/docs/php/builtins/class/get_class.md +++ b/docs/php/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class()" description: "Returns the name of the class of an object." sidebar: - order: 76 + order: 77 --- ## get_class() @@ -18,6 +18,11 @@ Returns the name of the class of an object. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_class_methods.md b/docs/php/builtins/class/get_class_methods.md new file mode 100644 index 0000000000..77d23d0780 --- /dev/null +++ b/docs/php/builtins/class/get_class_methods.md @@ -0,0 +1,26 @@ +--- +title: "get_class_methods()" +description: "get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 78 +--- + +## get_class_methods() + +```php +function get_class_methods(mixed $object_or_class): mixed +``` + +get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$object_or_class` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_class_vars.md b/docs/php/builtins/class/get_class_vars.md new file mode 100644 index 0000000000..2fe580caf0 --- /dev/null +++ b/docs/php/builtins/class/get_class_vars.md @@ -0,0 +1,26 @@ +--- +title: "get_class_vars()" +description: "get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 79 +--- + +## get_class_vars() + +```php +function get_class_vars(mixed $class): mixed +``` + +get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$class` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_declared_classes.md b/docs/php/builtins/class/get_declared_classes.md index 150f682d7e..0b7833f62b 100644 --- a/docs/php/builtins/class/get_declared_classes.md +++ b/docs/php/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes()" description: "Returns an array of the names of the defined classes." sidebar: - order: 77 + order: 80 --- ## get_declared_classes() @@ -17,6 +17,11 @@ Returns an array of the names of the defined classes. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_declared_interfaces.md b/docs/php/builtins/class/get_declared_interfaces.md index 0994cbbb8c..c70d2fd2fc 100644 --- a/docs/php/builtins/class/get_declared_interfaces.md +++ b/docs/php/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces()" description: "Returns an array of all declared interfaces." sidebar: - order: 78 + order: 81 --- ## get_declared_interfaces() @@ -17,6 +17,11 @@ Returns an array of all declared interfaces. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_declared_traits.md b/docs/php/builtins/class/get_declared_traits.md index 0c4cc105b9..b84fe80b07 100644 --- a/docs/php/builtins/class/get_declared_traits.md +++ b/docs/php/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits()" description: "Returns an array of all declared traits." sidebar: - order: 79 + order: 82 --- ## get_declared_traits() @@ -17,6 +17,11 @@ Returns an array of all declared traits. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_object_vars.md b/docs/php/builtins/class/get_object_vars.md new file mode 100644 index 0000000000..2b4a9ecfc1 --- /dev/null +++ b/docs/php/builtins/class/get_object_vars.md @@ -0,0 +1,26 @@ +--- +title: "get_object_vars()" +description: "get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 83 +--- + +## get_object_vars() + +```php +function get_object_vars(mixed $object): mixed +``` + +get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$object` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_parent_class.md b/docs/php/builtins/class/get_parent_class.md index 35ae146525..ceaa2b2d3f 100644 --- a/docs/php/builtins/class/get_parent_class.md +++ b/docs/php/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class()" description: "Returns the name of the parent class of an object or class." sidebar: - order: 80 + order: 84 --- ## get_parent_class() @@ -18,6 +18,11 @@ Returns the name of the parent class of an object or class. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/interface_exists.md b/docs/php/builtins/class/interface_exists.md index 3e39f7d827..76002a54e7 100644 --- a/docs/php/builtins/class/interface_exists.md +++ b/docs/php/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists()" description: "Checks if the interface has been defined." sidebar: - order: 81 + order: 85 --- ## interface_exists() @@ -19,6 +19,11 @@ Checks if the interface has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/is_a.md b/docs/php/builtins/class/is_a.md index 6eda7d1fac..e1425d8ba8 100644 --- a/docs/php/builtins/class/is_a.md +++ b/docs/php/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a()" description: "Checks whether an object is of a given type or has it as one of its parents." sidebar: - order: 82 + order: 86 --- ## is_a() @@ -20,6 +20,11 @@ Checks whether an object is of a given type or has it as one of its parents. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/is_subclass_of.md b/docs/php/builtins/class/is_subclass_of.md index 72822ee334..a906d499a1 100644 --- a/docs/php/builtins/class/is_subclass_of.md +++ b/docs/php/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of()" description: "Checks if the object has a given class as one of its parents or implements it." sidebar: - order: 83 + order: 87 --- ## is_subclass_of() @@ -20,6 +20,11 @@ Checks if the object has a given class as one of its parents or implements it. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/trait_exists.md b/docs/php/builtins/class/trait_exists.md index de92f3a1c8..df33e8438c 100644 --- a/docs/php/builtins/class/trait_exists.md +++ b/docs/php/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists()" description: "Checks whether the trait exists." sidebar: - order: 84 + order: 88 --- ## trait_exists() @@ -19,6 +19,11 @@ Checks whether the trait exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date.md b/docs/php/builtins/date.md index d5c0b90298..9a157e2d1d 100644 --- a/docs/php/builtins/date.md +++ b/docs/php/builtins/date.md @@ -7,18 +7,18 @@ sidebar: ## Date builtins -| Function | Signature | Returns | -|---|---|---| -| [`checkdate()`](./date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | -| [`date()`](./date/date.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`date_default_timezone_get()`](./date/date_default_timezone_get.md) | `(): string` | `string` | -| [`date_default_timezone_set()`](./date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | -| [`getdate()`](./date/getdate.md) | `(int $timestamp = null): array` | `array` | -| [`gmdate()`](./date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`gmmktime()`](./date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`hrtime()`](./date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | -| [`localtime()`](./date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | -| [`microtime()`](./date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | -| [`mktime()`](./date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`strtotime()`](./date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | -| [`time()`](./date/time.md) | `(): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`checkdate()`](./date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | ✓ | ✓ | +| [`date()`](./date/date.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_get()`](./date/date_default_timezone_get.md) | `(): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_set()`](./date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | ✓ | ✓ | +| [`getdate()`](./date/getdate.md) | `(int $timestamp = null): array` | `array` | ✓ | ✓ | +| [`gmdate()`](./date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`gmmktime()`](./date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`hrtime()`](./date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | ✓ | ✓ | +| [`localtime()`](./date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | ✓ | ✓ | +| [`microtime()`](./date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | ✓ | ✓ | +| [`mktime()`](./date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`strtotime()`](./date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | ✓ | ✓ | +| [`time()`](./date/time.md) | `(): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/date/checkdate.md b/docs/php/builtins/date/checkdate.md index 3f9f33d68f..3338e091e3 100644 --- a/docs/php/builtins/date/checkdate.md +++ b/docs/php/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate()" description: "Validates a Gregorian date." sidebar: - order: 85 + order: 89 --- ## checkdate() @@ -20,6 +20,11 @@ Validates a Gregorian date. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date.md b/docs/php/builtins/date/date.md index 8251d76f38..8f325c551e 100644 --- a/docs/php/builtins/date/date.md +++ b/docs/php/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date()" description: "Formats a local time/date." sidebar: - order: 86 + order: 90 --- ## date() @@ -19,6 +19,11 @@ Formats a local time/date. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date_default_timezone_get.md b/docs/php/builtins/date/date_default_timezone_get.md index 52e9b0f470..3633ce50e7 100644 --- a/docs/php/builtins/date/date_default_timezone_get.md +++ b/docs/php/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get()" description: "Gets the default timezone." sidebar: - order: 87 + order: 91 --- ## date_default_timezone_get() @@ -17,6 +17,11 @@ Gets the default timezone. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date_default_timezone_set.md b/docs/php/builtins/date/date_default_timezone_set.md index 8f83807597..7d9e64756c 100644 --- a/docs/php/builtins/date/date_default_timezone_set.md +++ b/docs/php/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set()" description: "Sets the default timezone." sidebar: - order: 88 + order: 92 --- ## date_default_timezone_set() @@ -18,6 +18,11 @@ Sets the default timezone. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/getdate.md b/docs/php/builtins/date/getdate.md index ac3b17ed08..4df60eec82 100644 --- a/docs/php/builtins/date/getdate.md +++ b/docs/php/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate()" description: "Returns date/time information." sidebar: - order: 89 + order: 93 --- ## getdate() @@ -18,6 +18,11 @@ Returns date/time information. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/gmdate.md b/docs/php/builtins/date/gmdate.md index 58cc570ae0..6752062fc1 100644 --- a/docs/php/builtins/date/gmdate.md +++ b/docs/php/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate()" description: "Formats a GMT/UTC date and time." sidebar: - order: 90 + order: 94 --- ## gmdate() @@ -19,6 +19,11 @@ Formats a GMT/UTC date and time. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/gmmktime.md b/docs/php/builtins/date/gmmktime.md index 69b8f37f89..6fd65dd616 100644 --- a/docs/php/builtins/date/gmmktime.md +++ b/docs/php/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime()" description: "Returns the Unix timestamp for a GMT date." sidebar: - order: 91 + order: 95 --- ## gmmktime() @@ -23,6 +23,11 @@ Returns the Unix timestamp for a GMT date. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/hrtime.md b/docs/php/builtins/date/hrtime.md index 764f21dffc..cd3f8d0e58 100644 --- a/docs/php/builtins/date/hrtime.md +++ b/docs/php/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime()" description: "Returns the current high-resolution time." sidebar: - order: 92 + order: 96 --- ## hrtime() @@ -18,6 +18,11 @@ Returns the current high-resolution time. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/localtime.md b/docs/php/builtins/date/localtime.md index 7afafc1967..cdeed577af 100644 --- a/docs/php/builtins/date/localtime.md +++ b/docs/php/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime()" description: "Returns the local time." sidebar: - order: 93 + order: 97 --- ## localtime() @@ -19,6 +19,11 @@ Returns the local time. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/microtime.md b/docs/php/builtins/date/microtime.md index 22abe5ed1f..295d1c0029 100644 --- a/docs/php/builtins/date/microtime.md +++ b/docs/php/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime()" description: "Returns the current Unix timestamp with microseconds." sidebar: - order: 94 + order: 98 --- ## microtime() @@ -18,6 +18,11 @@ Returns the current Unix timestamp with microseconds. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/mktime.md b/docs/php/builtins/date/mktime.md index 109320d678..93f4c14133 100644 --- a/docs/php/builtins/date/mktime.md +++ b/docs/php/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime()" description: "Returns the Unix timestamp for a date." sidebar: - order: 95 + order: 99 --- ## mktime() @@ -23,6 +23,11 @@ Returns the Unix timestamp for a date. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/strtotime.md b/docs/php/builtins/date/strtotime.md index b367f34f5c..3dc4815f48 100644 --- a/docs/php/builtins/date/strtotime.md +++ b/docs/php/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime()" description: "Parses an English textual datetime description into a Unix timestamp." sidebar: - order: 96 + order: 100 --- ## strtotime() @@ -19,6 +19,11 @@ Parses an English textual datetime description into a Unix timestamp. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/time.md b/docs/php/builtins/date/time.md index a100865cf0..2d852df176 100644 --- a/docs/php/builtins/date/time.md +++ b/docs/php/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time()" description: "Returns the current Unix timestamp." sidebar: - order: 97 + order: 101 --- ## time() @@ -17,6 +17,11 @@ Returns the current Unix timestamp. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/time.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/time.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem.md b/docs/php/builtins/filesystem.md index 1edc01cb60..d8c9af2cf3 100644 --- a/docs/php/builtins/filesystem.md +++ b/docs/php/builtins/filesystem.md @@ -7,60 +7,60 @@ sidebar: ## Filesystem builtins -| Function | Signature | Returns | -|---|---|---| -| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | -| [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | -| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`chmod()`](./filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | -| [`chown()`](./filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | -| [`clearstatcache()`](./filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | -| [`copy()`](./filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | -| [`dirname()`](./filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | -| [`disk_free_space()`](./filesystem/disk_free_space.md) | `(string $directory): float` | `float` | -| [`disk_total_space()`](./filesystem/disk_total_space.md) | `(string $directory): float` | `float` | -| [`file_exists()`](./filesystem/file_exists.md) | `(string $filename): bool` | `bool` | -| [`fileatime()`](./filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | -| [`filectime()`](./filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | -| [`filegroup()`](./filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | -| [`fileinode()`](./filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | -| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): int` | `int` | -| [`fileowner()`](./filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | -| [`fileperms()`](./filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | -| [`filesize()`](./filesystem/filesize.md) | `(string $filename): int` | `int` | -| [`filetype()`](./filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | -| [`fnmatch()`](./filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | -| [`getcwd()`](./filesystem/getcwd.md) | `(): string` | `string` | -| [`getenv()`](./filesystem/getenv.md) | `(string $name): mixed` | `mixed` | -| [`glob()`](./filesystem/glob.md) | `(string $pattern): array` | `array` | -| [`is_dir()`](./filesystem/is_dir.md) | `(string $filename): bool` | `bool` | -| [`is_executable()`](./filesystem/is_executable.md) | `(string $filename): bool` | `bool` | -| [`is_file()`](./filesystem/is_file.md) | `(string $filename): bool` | `bool` | -| [`is_link()`](./filesystem/is_link.md) | `(string $filename): bool` | `bool` | -| [`is_readable()`](./filesystem/is_readable.md) | `(string $filename): bool` | `bool` | -| [`is_writable()`](./filesystem/is_writable.md) | `(string $filename): bool` | `bool` | -| [`is_writeable()`](./filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | -| [`lchgrp()`](./filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`lchown()`](./filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | -| [`link()`](./filesystem/link.md) | `(string $target, string $link): bool` | `bool` | -| [`linkinfo()`](./filesystem/linkinfo.md) | `(string $path): int` | `int` | -| [`lstat()`](./filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | -| [`mkdir()`](./filesystem/mkdir.md) | `(string $directory): bool` | `bool` | -| [`pathinfo()`](./filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | -| [`putenv()`](./filesystem/putenv.md) | `(string $assignment): bool` | `bool` | -| [`readfile()`](./filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | -| [`readlink()`](./filesystem/readlink.md) | `(string $path): mixed` | `mixed` | -| [`realpath()`](./filesystem/realpath.md) | `(string $path): mixed` | `mixed` | -| [`realpath_cache_get()`](./filesystem/realpath_cache_get.md) | `(): array` | `array` | -| [`realpath_cache_size()`](./filesystem/realpath_cache_size.md) | `(): int` | `int` | -| [`rename()`](./filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | -| [`rmdir()`](./filesystem/rmdir.md) | `(string $directory): bool` | `bool` | -| [`scandir()`](./filesystem/scandir.md) | `(string $directory): array` | `array` | -| [`stat()`](./filesystem/stat.md) | `(string $filename): mixed` | `mixed` | -| [`symlink()`](./filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | -| [`sys_get_temp_dir()`](./filesystem/sys_get_temp_dir.md) | `(): string` | `string` | -| [`tempnam()`](./filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | -| [`tmpfile()`](./filesystem/tmpfile.md) | `(): mixed` | `mixed` | -| [`touch()`](./filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | -| [`umask()`](./filesystem/umask.md) | `(int $mask = null): int` | `int` | -| [`unlink()`](./filesystem/unlink.md) | `(string $filename): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | +| [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chmod()`](./filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`clearstatcache()`](./filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | ✓ | ✓ | +| [`copy()`](./filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`dirname()`](./filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | ✓ | ✓ | +| [`disk_free_space()`](./filesystem/disk_free_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`disk_total_space()`](./filesystem/disk_total_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`file_exists()`](./filesystem/file_exists.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`fileatime()`](./filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filectime()`](./filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filegroup()`](./filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileinode()`](./filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`fileowner()`](./filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileperms()`](./filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filesize()`](./filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filetype()`](./filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fnmatch()`](./filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`getcwd()`](./filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | +| [`getenv()`](./filesystem/getenv.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | +| [`glob()`](./filesystem/glob.md) | `(string $pattern): array` | `array` | ✓ | ✓ | +| [`is_dir()`](./filesystem/is_dir.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_executable()`](./filesystem/is_executable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_file()`](./filesystem/is_file.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_link()`](./filesystem/is_link.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_readable()`](./filesystem/is_readable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writable()`](./filesystem/is_writable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writeable()`](./filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`link()`](./filesystem/link.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`linkinfo()`](./filesystem/linkinfo.md) | `(string $path): int` | `int` | ✓ | ✓ | +| [`lstat()`](./filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`mkdir()`](./filesystem/mkdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`pathinfo()`](./filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | ✓ | ✓ | +| [`putenv()`](./filesystem/putenv.md) | `(string $assignment): bool` | `bool` | ✓ | ✓ | +| [`readfile()`](./filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`readlink()`](./filesystem/readlink.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath()`](./filesystem/realpath.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath_cache_get()`](./filesystem/realpath_cache_get.md) | `(): array` | `array` | ✓ | ✓ | +| [`realpath_cache_size()`](./filesystem/realpath_cache_size.md) | `(): int` | `int` | ✓ | ✓ | +| [`rename()`](./filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`rmdir()`](./filesystem/rmdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`scandir()`](./filesystem/scandir.md) | `(string $directory): array` | `array` | ✓ | ✓ | +| [`stat()`](./filesystem/stat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`symlink()`](./filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`sys_get_temp_dir()`](./filesystem/sys_get_temp_dir.md) | `(): string` | `string` | ✓ | ✓ | +| [`tempnam()`](./filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | ✓ | ✓ | +| [`tmpfile()`](./filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`touch()`](./filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | +| [`umask()`](./filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | +| [`unlink()`](./filesystem/unlink.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/filesystem/basename.md b/docs/php/builtins/filesystem/basename.md index b7fcae60c7..b7ede7c35d 100644 --- a/docs/php/builtins/filesystem/basename.md +++ b/docs/php/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename()" description: "Returns the trailing name component of a path." sidebar: - order: 98 + order: 102 --- ## basename() @@ -19,6 +19,11 @@ Returns the trailing name component of a path. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chdir.md b/docs/php/builtins/filesystem/chdir.md index 79eb4bd9bb..1b04de9d4e 100644 --- a/docs/php/builtins/filesystem/chdir.md +++ b/docs/php/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir()" description: "Changes the current directory." sidebar: - order: 99 + order: 103 --- ## chdir() @@ -18,6 +18,11 @@ Changes the current directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chgrp.md b/docs/php/builtins/filesystem/chgrp.md index a5f7d23632..17bddd4e80 100644 --- a/docs/php/builtins/filesystem/chgrp.md +++ b/docs/php/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp()" description: "Changes file group." sidebar: - order: 100 + order: 104 --- ## chgrp() @@ -19,6 +19,11 @@ Changes file group. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chmod.md b/docs/php/builtins/filesystem/chmod.md index 23a5012e9e..dbb1dfd042 100644 --- a/docs/php/builtins/filesystem/chmod.md +++ b/docs/php/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod()" description: "Changes file mode." sidebar: - order: 101 + order: 105 --- ## chmod() @@ -19,6 +19,11 @@ Changes file mode. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chown.md b/docs/php/builtins/filesystem/chown.md index 26d53c1398..6321a5fa86 100644 --- a/docs/php/builtins/filesystem/chown.md +++ b/docs/php/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown()" description: "Changes file owner." sidebar: - order: 102 + order: 106 --- ## chown() @@ -19,6 +19,11 @@ Changes file owner. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/clearstatcache.md b/docs/php/builtins/filesystem/clearstatcache.md index 47f99f901c..57ad08f8d6 100644 --- a/docs/php/builtins/filesystem/clearstatcache.md +++ b/docs/php/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache()" description: "Clears file status cache." sidebar: - order: 103 + order: 107 --- ## clearstatcache() @@ -19,6 +19,11 @@ Clears file status cache. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/copy.md b/docs/php/builtins/filesystem/copy.md index 3d08aa2588..fe60932396 100644 --- a/docs/php/builtins/filesystem/copy.md +++ b/docs/php/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy()" description: "Copies a file." sidebar: - order: 104 + order: 108 --- ## copy() @@ -19,6 +19,11 @@ Copies a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/dirname.md b/docs/php/builtins/filesystem/dirname.md index 4fe76dc5f5..be2ffd47f6 100644 --- a/docs/php/builtins/filesystem/dirname.md +++ b/docs/php/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname()" description: "Returns a parent directory's path." sidebar: - order: 105 + order: 109 --- ## dirname() @@ -19,6 +19,11 @@ Returns a parent directory's path. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/disk_free_space.md b/docs/php/builtins/filesystem/disk_free_space.md index a13b08e44d..d845244797 100644 --- a/docs/php/builtins/filesystem/disk_free_space.md +++ b/docs/php/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space()" description: "Returns available space on filesystem or disk partition." sidebar: - order: 106 + order: 110 --- ## disk_free_space() @@ -18,6 +18,11 @@ Returns available space on filesystem or disk partition. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/disk_total_space.md b/docs/php/builtins/filesystem/disk_total_space.md index b774d51383..690fa6a8ff 100644 --- a/docs/php/builtins/filesystem/disk_total_space.md +++ b/docs/php/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space()" description: "Returns the total size of a filesystem or disk partition." sidebar: - order: 107 + order: 111 --- ## disk_total_space() @@ -18,6 +18,11 @@ Returns the total size of a filesystem or disk partition. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/file_exists.md b/docs/php/builtins/filesystem/file_exists.md index 526d596428..4f0abad943 100644 --- a/docs/php/builtins/filesystem/file_exists.md +++ b/docs/php/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists()" description: "Checks whether a file or directory exists." sidebar: - order: 108 + order: 112 --- ## file_exists() @@ -18,6 +18,11 @@ Checks whether a file or directory exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileatime.md b/docs/php/builtins/filesystem/fileatime.md index 61b1f09e2d..e4abc65c70 100644 --- a/docs/php/builtins/filesystem/fileatime.md +++ b/docs/php/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime()" description: "Gets last access time of file." sidebar: - order: 109 + order: 113 --- ## fileatime() @@ -18,6 +18,11 @@ Gets last access time of file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filectime.md b/docs/php/builtins/filesystem/filectime.md index ea697c09f0..ab4424dc60 100644 --- a/docs/php/builtins/filesystem/filectime.md +++ b/docs/php/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime()" description: "Gets inode change time of file." sidebar: - order: 110 + order: 114 --- ## filectime() @@ -18,6 +18,11 @@ Gets inode change time of file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filegroup.md b/docs/php/builtins/filesystem/filegroup.md index 18bfc38b86..b56e3f1e92 100644 --- a/docs/php/builtins/filesystem/filegroup.md +++ b/docs/php/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup()" description: "Gets file group." sidebar: - order: 111 + order: 115 --- ## filegroup() @@ -18,6 +18,11 @@ Gets file group. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileinode.md b/docs/php/builtins/filesystem/fileinode.md index 2110a291ef..3862fc18c5 100644 --- a/docs/php/builtins/filesystem/fileinode.md +++ b/docs/php/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode()" description: "Gets file inode." sidebar: - order: 112 + order: 116 --- ## fileinode() @@ -18,6 +18,11 @@ Gets file inode. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filemtime.md b/docs/php/builtins/filesystem/filemtime.md index dfd9aba90e..103b540ccc 100644 --- a/docs/php/builtins/filesystem/filemtime.md +++ b/docs/php/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime()" description: "Gets file modification time." sidebar: - order: 113 + order: 117 --- ## filemtime() @@ -18,6 +18,11 @@ Gets file modification time. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileowner.md b/docs/php/builtins/filesystem/fileowner.md index b49e713523..109fd3b59f 100644 --- a/docs/php/builtins/filesystem/fileowner.md +++ b/docs/php/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner()" description: "Gets file owner." sidebar: - order: 114 + order: 118 --- ## fileowner() @@ -18,6 +18,11 @@ Gets file owner. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileperms.md b/docs/php/builtins/filesystem/fileperms.md index ebb7453ba1..6c62b27d78 100644 --- a/docs/php/builtins/filesystem/fileperms.md +++ b/docs/php/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms()" description: "Gets file permissions." sidebar: - order: 115 + order: 119 --- ## fileperms() @@ -18,6 +18,11 @@ Gets file permissions. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filesize.md b/docs/php/builtins/filesystem/filesize.md index c655357e10..4fed54516d 100644 --- a/docs/php/builtins/filesystem/filesize.md +++ b/docs/php/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize()" description: "Gets file size." sidebar: - order: 116 + order: 120 --- ## filesize() @@ -18,6 +18,11 @@ Gets file size. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filetype.md b/docs/php/builtins/filesystem/filetype.md index 61027757ed..e2e67300ac 100644 --- a/docs/php/builtins/filesystem/filetype.md +++ b/docs/php/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype()" description: "Gets file type." sidebar: - order: 117 + order: 121 --- ## filetype() @@ -18,6 +18,11 @@ Gets file type. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fnmatch.md b/docs/php/builtins/filesystem/fnmatch.md index 0f8791cc5f..584accdf41 100644 --- a/docs/php/builtins/filesystem/fnmatch.md +++ b/docs/php/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch()" description: "Matches a filename against a pattern." sidebar: - order: 118 + order: 122 --- ## fnmatch() @@ -20,6 +20,11 @@ Matches a filename against a pattern. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/getcwd.md b/docs/php/builtins/filesystem/getcwd.md index 330eabaefa..9924acfb7b 100644 --- a/docs/php/builtins/filesystem/getcwd.md +++ b/docs/php/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd()" description: "Gets the current working directory." sidebar: - order: 119 + order: 123 --- ## getcwd() @@ -17,6 +17,11 @@ Gets the current working directory. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/getenv.md b/docs/php/builtins/filesystem/getenv.md index 4d5a9b30e5..7f13d2fd2c 100644 --- a/docs/php/builtins/filesystem/getenv.md +++ b/docs/php/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv()" description: "Gets the value of an environment variable." sidebar: - order: 120 + order: 124 --- ## getenv() @@ -18,6 +18,11 @@ Gets the value of an environment variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/glob.md b/docs/php/builtins/filesystem/glob.md index af23fa2cf9..8155b89884 100644 --- a/docs/php/builtins/filesystem/glob.md +++ b/docs/php/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob()" description: "Finds pathnames matching a pattern." sidebar: - order: 121 + order: 125 --- ## glob() @@ -18,6 +18,11 @@ Finds pathnames matching a pattern. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_dir.md b/docs/php/builtins/filesystem/is_dir.md index 967dd2819a..aca56bddee 100644 --- a/docs/php/builtins/filesystem/is_dir.md +++ b/docs/php/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir()" description: "Tells whether the filename is a directory." sidebar: - order: 122 + order: 126 --- ## is_dir() @@ -18,6 +18,11 @@ Tells whether the filename is a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_executable.md b/docs/php/builtins/filesystem/is_executable.md index 08b532dac1..6b185798cb 100644 --- a/docs/php/builtins/filesystem/is_executable.md +++ b/docs/php/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable()" description: "Tells whether the filename is executable." sidebar: - order: 123 + order: 127 --- ## is_executable() @@ -18,6 +18,11 @@ Tells whether the filename is executable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_file.md b/docs/php/builtins/filesystem/is_file.md index b54871fded..53279f1690 100644 --- a/docs/php/builtins/filesystem/is_file.md +++ b/docs/php/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file()" description: "Tells whether the filename is a regular file." sidebar: - order: 124 + order: 128 --- ## is_file() @@ -18,6 +18,11 @@ Tells whether the filename is a regular file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_link.md b/docs/php/builtins/filesystem/is_link.md index 96a1ee53ae..98e113c928 100644 --- a/docs/php/builtins/filesystem/is_link.md +++ b/docs/php/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link()" description: "Tells whether the filename is a symbolic link." sidebar: - order: 125 + order: 129 --- ## is_link() @@ -18,6 +18,11 @@ Tells whether the filename is a symbolic link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_readable.md b/docs/php/builtins/filesystem/is_readable.md index c024a11243..03347f76f1 100644 --- a/docs/php/builtins/filesystem/is_readable.md +++ b/docs/php/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable()" description: "Tells whether the filename is readable." sidebar: - order: 126 + order: 130 --- ## is_readable() @@ -18,6 +18,11 @@ Tells whether the filename is readable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_writable.md b/docs/php/builtins/filesystem/is_writable.md index 2d121f3082..21b3826a43 100644 --- a/docs/php/builtins/filesystem/is_writable.md +++ b/docs/php/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable()" description: "Tells whether the filename is writable." sidebar: - order: 127 + order: 131 --- ## is_writable() @@ -18,6 +18,11 @@ Tells whether the filename is writable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_writeable.md b/docs/php/builtins/filesystem/is_writeable.md index d5c732bdcb..3f3a1cdd51 100644 --- a/docs/php/builtins/filesystem/is_writeable.md +++ b/docs/php/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable()" description: "Tells whether the filename is writable (alias of is_writable)." sidebar: - order: 128 + order: 132 --- ## is_writeable() @@ -18,6 +18,11 @@ Tells whether the filename is writable (alias of is_writable). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lchgrp.md b/docs/php/builtins/filesystem/lchgrp.md index 60157747b1..ea6dccf835 100644 --- a/docs/php/builtins/filesystem/lchgrp.md +++ b/docs/php/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp()" description: "Changes group ownership of a symlink." sidebar: - order: 129 + order: 133 --- ## lchgrp() @@ -19,6 +19,11 @@ Changes group ownership of a symlink. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lchown.md b/docs/php/builtins/filesystem/lchown.md index 7e1b51edc2..6558c95ffb 100644 --- a/docs/php/builtins/filesystem/lchown.md +++ b/docs/php/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown()" description: "Changes user ownership of a symlink." sidebar: - order: 130 + order: 134 --- ## lchown() @@ -19,6 +19,11 @@ Changes user ownership of a symlink. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/link.md b/docs/php/builtins/filesystem/link.md index a91aa299a4..c57013fd0c 100644 --- a/docs/php/builtins/filesystem/link.md +++ b/docs/php/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link()" description: "Creates a hard link." sidebar: - order: 131 + order: 135 --- ## link() @@ -19,6 +19,11 @@ Creates a hard link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/linkinfo.md b/docs/php/builtins/filesystem/linkinfo.md index dbf0e821e3..45b36f1de0 100644 --- a/docs/php/builtins/filesystem/linkinfo.md +++ b/docs/php/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo()" description: "Gets information about a link." sidebar: - order: 132 + order: 136 --- ## linkinfo() @@ -18,6 +18,11 @@ Gets information about a link. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lstat.md b/docs/php/builtins/filesystem/lstat.md index 9872189521..1791c5ebed 100644 --- a/docs/php/builtins/filesystem/lstat.md +++ b/docs/php/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat()" description: "Gives information about a file or symbolic link." sidebar: - order: 133 + order: 137 --- ## lstat() @@ -18,6 +18,11 @@ Gives information about a file or symbolic link. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/mkdir.md b/docs/php/builtins/filesystem/mkdir.md index b10b78219f..e8a7ce95fd 100644 --- a/docs/php/builtins/filesystem/mkdir.md +++ b/docs/php/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir()" description: "Makes a directory." sidebar: - order: 134 + order: 138 --- ## mkdir() @@ -18,6 +18,11 @@ Makes a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/pathinfo.md b/docs/php/builtins/filesystem/pathinfo.md index 6f048c5f92..8cde8f248b 100644 --- a/docs/php/builtins/filesystem/pathinfo.md +++ b/docs/php/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo()" description: "Returns information about a file path." sidebar: - order: 135 + order: 139 --- ## pathinfo() @@ -19,6 +19,11 @@ Returns information about a file path. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/putenv.md b/docs/php/builtins/filesystem/putenv.md index 3be34a079a..7937bc9403 100644 --- a/docs/php/builtins/filesystem/putenv.md +++ b/docs/php/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv()" description: "Sets an environment variable." sidebar: - order: 136 + order: 140 --- ## putenv() @@ -18,6 +18,11 @@ Sets an environment variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/readfile.md b/docs/php/builtins/filesystem/readfile.md index 4ae8854ae2..627e657fe6 100644 --- a/docs/php/builtins/filesystem/readfile.md +++ b/docs/php/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile()" description: "Outputs a file." sidebar: - order: 137 + order: 141 --- ## readfile() @@ -18,6 +18,11 @@ Outputs a file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/readlink.md b/docs/php/builtins/filesystem/readlink.md index 3225ce379b..644eb5f2bb 100644 --- a/docs/php/builtins/filesystem/readlink.md +++ b/docs/php/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink()" description: "Returns the target of a symbolic link." sidebar: - order: 138 + order: 142 --- ## readlink() @@ -18,6 +18,11 @@ Returns the target of a symbolic link. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath.md b/docs/php/builtins/filesystem/realpath.md index ad0d795625..5e32979978 100644 --- a/docs/php/builtins/filesystem/realpath.md +++ b/docs/php/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath()" description: "Returns canonicalized absolute pathname." sidebar: - order: 139 + order: 143 --- ## realpath() @@ -18,6 +18,11 @@ Returns canonicalized absolute pathname. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath_cache_get.md b/docs/php/builtins/filesystem/realpath_cache_get.md index 0780ccfffa..bc92aa2e19 100644 --- a/docs/php/builtins/filesystem/realpath_cache_get.md +++ b/docs/php/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get()" description: "Returns realpath cache entries." sidebar: - order: 140 + order: 144 --- ## realpath_cache_get() @@ -17,6 +17,11 @@ Returns realpath cache entries. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath_cache_size.md b/docs/php/builtins/filesystem/realpath_cache_size.md index 7c7b7796e6..030e9a39df 100644 --- a/docs/php/builtins/filesystem/realpath_cache_size.md +++ b/docs/php/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size()" description: "Returns the amount of memory used by the realpath cache." sidebar: - order: 141 + order: 145 --- ## realpath_cache_size() @@ -17,6 +17,11 @@ Returns the amount of memory used by the realpath cache. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/rename.md b/docs/php/builtins/filesystem/rename.md index 0075032d5d..cfbcef30ac 100644 --- a/docs/php/builtins/filesystem/rename.md +++ b/docs/php/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename()" description: "Renames a file or directory." sidebar: - order: 142 + order: 146 --- ## rename() @@ -19,6 +19,11 @@ Renames a file or directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/rmdir.md b/docs/php/builtins/filesystem/rmdir.md index ac6c8e1a93..3d86c09321 100644 --- a/docs/php/builtins/filesystem/rmdir.md +++ b/docs/php/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir()" description: "Removes a directory." sidebar: - order: 143 + order: 147 --- ## rmdir() @@ -18,6 +18,11 @@ Removes a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/scandir.md b/docs/php/builtins/filesystem/scandir.md index 8540afa3a0..03edbdfd5c 100644 --- a/docs/php/builtins/filesystem/scandir.md +++ b/docs/php/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir()" description: "Lists files and directories inside the specified path." sidebar: - order: 144 + order: 148 --- ## scandir() @@ -18,6 +18,11 @@ Lists files and directories inside the specified path. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/stat.md b/docs/php/builtins/filesystem/stat.md index 3179e14368..39286104cc 100644 --- a/docs/php/builtins/filesystem/stat.md +++ b/docs/php/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat()" description: "Gives information about a file." sidebar: - order: 145 + order: 149 --- ## stat() @@ -18,6 +18,11 @@ Gives information about a file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/symlink.md b/docs/php/builtins/filesystem/symlink.md index 7bd1e3a55c..dfd1abdfa9 100644 --- a/docs/php/builtins/filesystem/symlink.md +++ b/docs/php/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink()" description: "Creates a symbolic link." sidebar: - order: 146 + order: 150 --- ## symlink() @@ -19,6 +19,11 @@ Creates a symbolic link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/sys_get_temp_dir.md b/docs/php/builtins/filesystem/sys_get_temp_dir.md index e910e45349..fa0168fa5e 100644 --- a/docs/php/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/php/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir()" description: "Returns the directory path used for temporary files." sidebar: - order: 147 + order: 151 --- ## sys_get_temp_dir() @@ -17,6 +17,11 @@ Returns the directory path used for temporary files. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/tempnam.md b/docs/php/builtins/filesystem/tempnam.md index a8ac18a230..4293223b49 100644 --- a/docs/php/builtins/filesystem/tempnam.md +++ b/docs/php/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam()" description: "Creates a file with a unique filename." sidebar: - order: 148 + order: 152 --- ## tempnam() @@ -19,6 +19,11 @@ Creates a file with a unique filename. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/tmpfile.md b/docs/php/builtins/filesystem/tmpfile.md index 9919570752..2061075d49 100644 --- a/docs/php/builtins/filesystem/tmpfile.md +++ b/docs/php/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile()" description: "Creates a temporary file." sidebar: - order: 149 + order: 153 --- ## tmpfile() @@ -17,6 +17,11 @@ Creates a temporary file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/touch.md b/docs/php/builtins/filesystem/touch.md index 7bd652ac00..2773a7a68c 100644 --- a/docs/php/builtins/filesystem/touch.md +++ b/docs/php/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch()" description: "Sets access and modification time of a file." sidebar: - order: 150 + order: 154 --- ## touch() @@ -20,6 +20,11 @@ Sets access and modification time of a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/umask.md b/docs/php/builtins/filesystem/umask.md index c11ca2d250..f6249bd537 100644 --- a/docs/php/builtins/filesystem/umask.md +++ b/docs/php/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask()" description: "Changes the current umask." sidebar: - order: 151 + order: 155 --- ## umask() @@ -18,6 +18,11 @@ Changes the current umask. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/unlink.md b/docs/php/builtins/filesystem/unlink.md index 1910b9daa3..6b825ae4b9 100644 --- a/docs/php/builtins/filesystem/unlink.md +++ b/docs/php/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink()" description: "Deletes a file." sidebar: - order: 152 + order: 156 --- ## unlink() @@ -18,6 +18,11 @@ Deletes a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io.md b/docs/php/builtins/io.md index 135caf50f7..94afe286a0 100644 --- a/docs/php/builtins/io.md +++ b/docs/php/builtins/io.md @@ -7,82 +7,82 @@ sidebar: ## IO builtins -| Function | Signature | Returns | -|---|---|---| -| [`closedir()`](./io/closedir.md) | `(resource $dir_handle): void` | `void` | -| [`fclose()`](./io/fclose.md) | `(resource $stream): bool` | `bool` | -| [`fdatasync()`](./io/fdatasync.md) | `(resource $stream): bool` | `bool` | -| [`feof()`](./io/feof.md) | `(resource $stream): bool` | `bool` | -| [`fflush()`](./io/fflush.md) | `(resource $stream): bool` | `bool` | -| [`fgetc()`](./io/fgetc.md) | `(resource $stream): mixed` | `mixed` | -| [`fgetcsv()`](./io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | -| [`fgets()`](./io/fgets.md) | `(resource $stream): mixed` | `mixed` | -| [`file()`](./io/file.md) | `(string $filename): array` | `array` | -| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | -| [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | -| [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | -| [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | -| [`fpassthru()`](./io/fpassthru.md) | `(resource $stream): int` | `int` | -| [`fprintf()`](./io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | -| [`fputcsv()`](./io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | -| [`fread()`](./io/fread.md) | `(resource $stream, int $length): string` | `string` | -| [`fscanf()`](./io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | -| [`fseek()`](./io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | -| [`fstat()`](./io/fstat.md) | `(resource $stream): mixed` | `mixed` | -| [`fsync()`](./io/fsync.md) | `(resource $stream): bool` | `bool` | -| [`ftell()`](./io/ftell.md) | `(resource $stream): int` | `int` | -| [`ftruncate()`](./io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | -| [`fwrite()`](./io/fwrite.md) | `(resource $stream, string $data): int` | `int` | -| [`gethostbyaddr()`](./io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | -| [`gethostbyname()`](./io/gethostbyname.md) | `(string $hostname): string` | `string` | -| [`gethostname()`](./io/gethostname.md) | `(): string` | `string` | -| [`getprotobyname()`](./io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | -| [`getprotobynumber()`](./io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | -| [`getservbyname()`](./io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | -| [`getservbyport()`](./io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | -| [`hash_file()`](./io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | -| [`opendir()`](./io/opendir.md) | `(string $directory): mixed` | `mixed` | -| [`readdir()`](./io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | -| [`rewind()`](./io/rewind.md) | `(resource $stream): bool` | `bool` | -| [`rewinddir()`](./io/rewinddir.md) | `(resource $dir_handle): void` | `void` | -| [`stream_bucket_make_writeable()`](./io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | -| [`stream_bucket_new()`](./io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | -| [`stream_context_create()`](./io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | -| [`stream_context_get_default()`](./io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | -| [`stream_context_get_options()`](./io/stream_context_get_options.md) | `(resource $context): array` | `array` | -| [`stream_context_get_params()`](./io/stream_context_get_params.md) | `(resource $context): array` | `array` | -| [`stream_context_set_default()`](./io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_set_option()`](./io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | -| [`stream_context_set_params()`](./io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | -| [`stream_copy_to_stream()`](./io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_filter_register()`](./io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | -| [`stream_filter_remove()`](./io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | -| [`stream_get_contents()`](./io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_get_filters()`](./io/stream_get_filters.md) | `(): array` | `array` | -| [`stream_get_line()`](./io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | -| [`stream_get_meta_data()`](./io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | -| [`stream_get_transports()`](./io/stream_get_transports.md) | `(): array` | `array` | -| [`stream_get_wrappers()`](./io/stream_get_wrappers.md) | `(): array` | `array` | -| [`stream_is_local()`](./io/stream_is_local.md) | `(resource $stream): bool` | `bool` | -| [`stream_isatty()`](./io/stream_isatty.md) | `(resource $stream): bool` | `bool` | -| [`stream_resolve_include_path()`](./io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | -| [`stream_select()`](./io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | -| [`stream_set_blocking()`](./io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | -| [`stream_set_chunk_size()`](./io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_read_buffer()`](./io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_timeout()`](./io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | -| [`stream_set_write_buffer()`](./io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_socket_accept()`](./io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | -| [`stream_socket_client()`](./io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | -| [`stream_socket_get_name()`](./io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | -| [`stream_socket_pair()`](./io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | -| [`stream_socket_recvfrom()`](./io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_sendto()`](./io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_server()`](./io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_shutdown()`](./io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | -| [`stream_supports_lock()`](./io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | -| [`stream_wrapper_register()`](./io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | -| [`stream_wrapper_restore()`](./io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | -| [`stream_wrapper_unregister()`](./io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | -| [`vfprintf()`](./io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`closedir()`](./io/closedir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`fclose()`](./io/fclose.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fdatasync()`](./io/fdatasync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`feof()`](./io/feof.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fflush()`](./io/fflush.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fgetc()`](./io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fgetcsv()`](./io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | +| [`fgets()`](./io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | +| [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | +| [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | +| [`fpassthru()`](./io/fpassthru.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`fprintf()`](./io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`fputcsv()`](./io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | ✓ | ✓ | +| [`fread()`](./io/fread.md) | `(resource $stream, int $length): string` | `string` | ✓ | ✓ | +| [`fscanf()`](./io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`fseek()`](./io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | ✓ | ✓ | +| [`fstat()`](./io/fstat.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fsync()`](./io/fsync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`ftell()`](./io/ftell.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`ftruncate()`](./io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | ✓ | ✓ | +| [`fwrite()`](./io/fwrite.md) | `(resource $stream, string $data): int` | `int` | ✓ | ✓ | +| [`gethostbyaddr()`](./io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`gethostbyname()`](./io/gethostbyname.md) | `(string $hostname): string` | `string` | ✓ | ✓ | +| [`gethostname()`](./io/gethostname.md) | `(): string` | `string` | ✓ | ✓ | +| [`getprotobyname()`](./io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getprotobynumber()`](./io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyname()`](./io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyport()`](./io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`hash_file()`](./io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | ✓ | ✓ | +| [`opendir()`](./io/opendir.md) | `(string $directory): mixed` | `mixed` | ✓ | ✓ | +| [`readdir()`](./io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | ✓ | ✓ | +| [`rewind()`](./io/rewind.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`rewinddir()`](./io/rewinddir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`stream_bucket_make_writeable()`](./io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_new()`](./io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_create()`](./io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_default()`](./io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_options()`](./io/stream_context_get_options.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_get_params()`](./io/stream_context_get_params.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_set_default()`](./io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_set_option()`](./io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | ✓ | ✓ | +| [`stream_context_set_params()`](./io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | ✓ | ✓ | +| [`stream_copy_to_stream()`](./io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_register()`](./io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | ✓ | ✓ | +| [`stream_filter_remove()`](./io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | ✓ | ✓ | +| [`stream_get_contents()`](./io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_get_filters()`](./io/stream_get_filters.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_line()`](./io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | ✓ | ✓ | +| [`stream_get_meta_data()`](./io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | ✓ | ✓ | +| [`stream_get_transports()`](./io/stream_get_transports.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_wrappers()`](./io/stream_get_wrappers.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_is_local()`](./io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_isatty()`](./io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_resolve_include_path()`](./io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`stream_select()`](./io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_set_blocking()`](./io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | +| [`stream_set_chunk_size()`](./io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_read_buffer()`](./io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_timeout()`](./io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_set_write_buffer()`](./io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_socket_accept()`](./io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_client()`](./io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | ✓ | ✓ | +| [`stream_socket_get_name()`](./io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_pair()`](./io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_recvfrom()`](./io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_sendto()`](./io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_server()`](./io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_shutdown()`](./io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | ✓ | ✓ | +| [`stream_supports_lock()`](./io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_register()`](./io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_restore()`](./io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_unregister()`](./io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`vfprintf()`](./io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/io/closedir.md b/docs/php/builtins/io/closedir.md index d2f0c88f2a..01d1651e8e 100644 --- a/docs/php/builtins/io/closedir.md +++ b/docs/php/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir()" description: "Closes directory handle." sidebar: - order: 153 + order: 157 --- ## closedir() @@ -18,6 +18,11 @@ Closes directory handle. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fclose.md b/docs/php/builtins/io/fclose.md index d9c8c44990..ad568cb4b5 100644 --- a/docs/php/builtins/io/fclose.md +++ b/docs/php/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose()" description: "Closes an open file pointer." sidebar: - order: 154 + order: 158 --- ## fclose() @@ -18,6 +18,11 @@ Closes an open file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fdatasync.md b/docs/php/builtins/io/fdatasync.md index 26b0b5562e..6f98421731 100644 --- a/docs/php/builtins/io/fdatasync.md +++ b/docs/php/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync()" description: "Synchronizes data (but not meta-data) to file." sidebar: - order: 155 + order: 159 --- ## fdatasync() @@ -18,6 +18,11 @@ Synchronizes data (but not meta-data) to file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/feof.md b/docs/php/builtins/io/feof.md index fcb60ca74e..890d32aab2 100644 --- a/docs/php/builtins/io/feof.md +++ b/docs/php/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof()" description: "Tests for end-of-file on a file pointer." sidebar: - order: 156 + order: 160 --- ## feof() @@ -18,6 +18,11 @@ Tests for end-of-file on a file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fflush.md b/docs/php/builtins/io/fflush.md index ba052577bc..f86c775ae8 100644 --- a/docs/php/builtins/io/fflush.md +++ b/docs/php/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush()" description: "Flushes the output to a file." sidebar: - order: 157 + order: 161 --- ## fflush() @@ -18,6 +18,11 @@ Flushes the output to a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgetc.md b/docs/php/builtins/io/fgetc.md index f69c10d508..29f28a270d 100644 --- a/docs/php/builtins/io/fgetc.md +++ b/docs/php/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc()" description: "Gets a character from the given file pointer." sidebar: - order: 158 + order: 162 --- ## fgetc() @@ -18,6 +18,11 @@ Gets a character from the given file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgetcsv.md b/docs/php/builtins/io/fgetcsv.md index 4baf3d3a40..cf29ce57e9 100644 --- a/docs/php/builtins/io/fgetcsv.md +++ b/docs/php/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv()" description: "Gets line from file pointer and parse for CSV fields." sidebar: - order: 159 + order: 163 --- ## fgetcsv() @@ -20,6 +20,11 @@ Gets line from file pointer and parse for CSV fields. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgets.md b/docs/php/builtins/io/fgets.md index 2150096f36..2d5d14744c 100644 --- a/docs/php/builtins/io/fgets.md +++ b/docs/php/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets()" description: "Gets line from file pointer." sidebar: - order: 160 + order: 164 --- ## fgets() @@ -18,6 +18,11 @@ Gets line from file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file.md b/docs/php/builtins/io/file.md index b6176accd9..b4882e0b63 100644 --- a/docs/php/builtins/io/file.md +++ b/docs/php/builtins/io/file.md @@ -2,7 +2,7 @@ title: "file()" description: "Reads an entire file into an array." sidebar: - order: 161 + order: 165 --- ## file() @@ -18,6 +18,11 @@ Reads an entire file into an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file_get_contents.md b/docs/php/builtins/io/file_get_contents.md index e8d8a6fc69..9a74b62e5b 100644 --- a/docs/php/builtins/io/file_get_contents.md +++ b/docs/php/builtins/io/file_get_contents.md @@ -2,7 +2,7 @@ title: "file_get_contents()" description: "Reads an entire file into a string." sidebar: - order: 162 + order: 166 --- ## file_get_contents() @@ -18,6 +18,11 @@ Reads an entire file into a string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file_put_contents.md b/docs/php/builtins/io/file_put_contents.md index 0cebb841a2..9d7d8eef53 100644 --- a/docs/php/builtins/io/file_put_contents.md +++ b/docs/php/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents()" description: "Writes data to a file." sidebar: - order: 163 + order: 167 --- ## file_put_contents() @@ -19,6 +19,11 @@ Writes data to a file. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/flock.md b/docs/php/builtins/io/flock.md index efd46e23e8..661c300ab4 100644 --- a/docs/php/builtins/io/flock.md +++ b/docs/php/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock()" description: "Portable advisory file locking." sidebar: - order: 164 + order: 168 --- ## flock() @@ -20,6 +20,11 @@ Portable advisory file locking. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fopen.md b/docs/php/builtins/io/fopen.md index 706973d49d..1ec50f76b0 100644 --- a/docs/php/builtins/io/fopen.md +++ b/docs/php/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen()" description: "Opens file or URL." sidebar: - order: 165 + order: 169 --- ## fopen() @@ -21,6 +21,11 @@ Opens file or URL. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fpassthru.md b/docs/php/builtins/io/fpassthru.md index 676aa59290..7f32dbfde4 100644 --- a/docs/php/builtins/io/fpassthru.md +++ b/docs/php/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru()" description: "Output all remaining data on a file pointer." sidebar: - order: 166 + order: 170 --- ## fpassthru() @@ -18,6 +18,11 @@ Output all remaining data on a file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fprintf.md b/docs/php/builtins/io/fprintf.md index f9ceafb81e..c365eb2c72 100644 --- a/docs/php/builtins/io/fprintf.md +++ b/docs/php/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf()" description: "Write a formatted string to a stream." sidebar: - order: 167 + order: 171 --- ## fprintf() @@ -20,6 +20,11 @@ Write a formatted string to a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fputcsv.md b/docs/php/builtins/io/fputcsv.md index 3f004533f4..49f251076c 100644 --- a/docs/php/builtins/io/fputcsv.md +++ b/docs/php/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv()" description: "Format line as CSV and write to file pointer." sidebar: - order: 168 + order: 172 --- ## fputcsv() @@ -21,6 +21,11 @@ Format line as CSV and write to file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fread.md b/docs/php/builtins/io/fread.md index 17dbb2afc9..ac66807844 100644 --- a/docs/php/builtins/io/fread.md +++ b/docs/php/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread()" description: "Binary-safe file read." sidebar: - order: 169 + order: 173 --- ## fread() @@ -19,6 +19,11 @@ Binary-safe file read. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fscanf.md b/docs/php/builtins/io/fscanf.md index 67d2cd9551..45ebe07e1d 100644 --- a/docs/php/builtins/io/fscanf.md +++ b/docs/php/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf()" description: "Parses input from a file according to a format." sidebar: - order: 170 + order: 174 --- ## fscanf() @@ -20,6 +20,11 @@ Parses input from a file according to a format. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fseek.md b/docs/php/builtins/io/fseek.md index 66fea389be..8e7732bdc6 100644 --- a/docs/php/builtins/io/fseek.md +++ b/docs/php/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek()" description: "Seeks on a file pointer." sidebar: - order: 171 + order: 175 --- ## fseek() @@ -20,6 +20,11 @@ Seeks on a file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fstat.md b/docs/php/builtins/io/fstat.md index 673d0c0302..e2b1d2c1f9 100644 --- a/docs/php/builtins/io/fstat.md +++ b/docs/php/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat()" description: "Gets information about a file using an open file pointer." sidebar: - order: 172 + order: 176 --- ## fstat() @@ -18,6 +18,11 @@ Gets information about a file using an open file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fsync.md b/docs/php/builtins/io/fsync.md index 58a001a4b6..5e7a7cbfe0 100644 --- a/docs/php/builtins/io/fsync.md +++ b/docs/php/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync()" description: "Synchronizes changes to the file (including meta-data)." sidebar: - order: 173 + order: 177 --- ## fsync() @@ -18,6 +18,11 @@ Synchronizes changes to the file (including meta-data). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/ftell.md b/docs/php/builtins/io/ftell.md index 5fc9fee511..0b9aa3c22c 100644 --- a/docs/php/builtins/io/ftell.md +++ b/docs/php/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell()" description: "Returns the current position of the file read/write pointer." sidebar: - order: 174 + order: 178 --- ## ftell() @@ -18,6 +18,11 @@ Returns the current position of the file read/write pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/ftruncate.md b/docs/php/builtins/io/ftruncate.md index cfa0d10725..8b4c53ba33 100644 --- a/docs/php/builtins/io/ftruncate.md +++ b/docs/php/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate()" description: "Truncates a file to a given length." sidebar: - order: 175 + order: 179 --- ## ftruncate() @@ -19,6 +19,11 @@ Truncates a file to a given length. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fwrite.md b/docs/php/builtins/io/fwrite.md index 3edc50c485..aa8f9ddb21 100644 --- a/docs/php/builtins/io/fwrite.md +++ b/docs/php/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite()" description: "Binary-safe file write." sidebar: - order: 176 + order: 180 --- ## fwrite() @@ -19,6 +19,11 @@ Binary-safe file write. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostbyaddr.md b/docs/php/builtins/io/gethostbyaddr.md index 740bd536b8..653683960d 100644 --- a/docs/php/builtins/io/gethostbyaddr.md +++ b/docs/php/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr()" description: "Gets the Internet host name corresponding to a given IP address." sidebar: - order: 177 + order: 181 --- ## gethostbyaddr() @@ -18,6 +18,11 @@ Gets the Internet host name corresponding to a given IP address. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostbyname.md b/docs/php/builtins/io/gethostbyname.md index 669006764a..a9af4c3852 100644 --- a/docs/php/builtins/io/gethostbyname.md +++ b/docs/php/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname()" description: "Gets the IPv4 address corresponding to the given Internet host name." sidebar: - order: 178 + order: 182 --- ## gethostbyname() @@ -18,6 +18,11 @@ Gets the IPv4 address corresponding to the given Internet host name. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostname.md b/docs/php/builtins/io/gethostname.md index 102c48204e..9f8e85f1bf 100644 --- a/docs/php/builtins/io/gethostname.md +++ b/docs/php/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname()" description: "Gets the standard host name for the local machine." sidebar: - order: 179 + order: 183 --- ## gethostname() @@ -17,6 +17,11 @@ Gets the standard host name for the local machine. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getprotobyname.md b/docs/php/builtins/io/getprotobyname.md index bddcbae93a..5c1461d66a 100644 --- a/docs/php/builtins/io/getprotobyname.md +++ b/docs/php/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname()" description: "Gets the protocol number associated with the given protocol name." sidebar: - order: 180 + order: 184 --- ## getprotobyname() @@ -18,6 +18,11 @@ Gets the protocol number associated with the given protocol name. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getprotobynumber.md b/docs/php/builtins/io/getprotobynumber.md index 14de680e4a..3cc918aaf6 100644 --- a/docs/php/builtins/io/getprotobynumber.md +++ b/docs/php/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber()" description: "Gets the protocol name associated with the given protocol number." sidebar: - order: 181 + order: 185 --- ## getprotobynumber() @@ -18,6 +18,11 @@ Gets the protocol name associated with the given protocol number. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getservbyname.md b/docs/php/builtins/io/getservbyname.md index 92114158d2..4eb2bce1ed 100644 --- a/docs/php/builtins/io/getservbyname.md +++ b/docs/php/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname()" description: "Gets port number associated with an Internet service and protocol." sidebar: - order: 182 + order: 186 --- ## getservbyname() @@ -19,6 +19,11 @@ Gets port number associated with an Internet service and protocol. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getservbyport.md b/docs/php/builtins/io/getservbyport.md index ac10941245..9974f92837 100644 --- a/docs/php/builtins/io/getservbyport.md +++ b/docs/php/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport()" description: "Gets the Internet service that corresponds to a port and protocol." sidebar: - order: 183 + order: 187 --- ## getservbyport() @@ -19,6 +19,11 @@ Gets the Internet service that corresponds to a port and protocol. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/hash_file.md b/docs/php/builtins/io/hash_file.md index 7307465076..a0825f6f0c 100644 --- a/docs/php/builtins/io/hash_file.md +++ b/docs/php/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file()" description: "Generates a hash value using the contents of a given file." sidebar: - order: 184 + order: 188 --- ## hash_file() @@ -20,6 +20,11 @@ Generates a hash value using the contents of a given file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/opendir.md b/docs/php/builtins/io/opendir.md index 8666aaa8ec..b3b1b7a49e 100644 --- a/docs/php/builtins/io/opendir.md +++ b/docs/php/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir()" description: "Open directory handle." sidebar: - order: 185 + order: 189 --- ## opendir() @@ -18,6 +18,11 @@ Open directory handle. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/readdir.md b/docs/php/builtins/io/readdir.md index 2626d69f9e..cbb08f1bd0 100644 --- a/docs/php/builtins/io/readdir.md +++ b/docs/php/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir()" description: "Read entry from directory handle." sidebar: - order: 186 + order: 190 --- ## readdir() @@ -18,6 +18,11 @@ Read entry from directory handle. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/rewind.md b/docs/php/builtins/io/rewind.md index af56e86a73..3296ae8cba 100644 --- a/docs/php/builtins/io/rewind.md +++ b/docs/php/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind()" description: "Rewind the position of a file pointer." sidebar: - order: 187 + order: 191 --- ## rewind() @@ -18,6 +18,11 @@ Rewind the position of a file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/rewinddir.md b/docs/php/builtins/io/rewinddir.md index 379f10be46..2221a233e8 100644 --- a/docs/php/builtins/io/rewinddir.md +++ b/docs/php/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir()" description: "Rewind directory handle." sidebar: - order: 188 + order: 192 --- ## rewinddir() @@ -18,6 +18,11 @@ Rewind directory handle. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_bucket_make_writeable.md b/docs/php/builtins/io/stream_bucket_make_writeable.md index 9fd73afea8..7859d968c2 100644 --- a/docs/php/builtins/io/stream_bucket_make_writeable.md +++ b/docs/php/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable()" description: "Returns a bucket object from the brigade for use in a stream filter." sidebar: - order: 189 + order: 193 --- ## stream_bucket_make_writeable() @@ -18,6 +18,11 @@ Returns a bucket object from the brigade for use in a stream filter. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_bucket_new.md b/docs/php/builtins/io/stream_bucket_new.md index ebea411b29..495565d932 100644 --- a/docs/php/builtins/io/stream_bucket_new.md +++ b/docs/php/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new()" description: "Creates a new bucket for use in a stream filter." sidebar: - order: 190 + order: 194 --- ## stream_bucket_new() @@ -19,6 +19,11 @@ Creates a new bucket for use in a stream filter. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_create.md b/docs/php/builtins/io/stream_context_create.md index 7e556f6436..c9fab9f744 100644 --- a/docs/php/builtins/io/stream_context_create.md +++ b/docs/php/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create()" description: "Creates a stream context." sidebar: - order: 191 + order: 195 --- ## stream_context_create() @@ -19,6 +19,11 @@ Creates a stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_default.md b/docs/php/builtins/io/stream_context_get_default.md index ad032c2764..dbb8115b22 100644 --- a/docs/php/builtins/io/stream_context_get_default.md +++ b/docs/php/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default()" description: "Retrieves the default stream context." sidebar: - order: 192 + order: 196 --- ## stream_context_get_default() @@ -18,6 +18,11 @@ Retrieves the default stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_options.md b/docs/php/builtins/io/stream_context_get_options.md index ffca97723d..fa6f5edd97 100644 --- a/docs/php/builtins/io/stream_context_get_options.md +++ b/docs/php/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options()" description: "Retrieves options for the specified stream context." sidebar: - order: 193 + order: 197 --- ## stream_context_get_options() @@ -18,6 +18,11 @@ Retrieves options for the specified stream context. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_params.md b/docs/php/builtins/io/stream_context_get_params.md index 836973a127..9aae40c46c 100644 --- a/docs/php/builtins/io/stream_context_get_params.md +++ b/docs/php/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params()" description: "Retrieves parameters from the specified stream context." sidebar: - order: 194 + order: 198 --- ## stream_context_get_params() @@ -18,6 +18,11 @@ Retrieves parameters from the specified stream context. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_default.md b/docs/php/builtins/io/stream_context_set_default.md index afab538c14..d0bb794088 100644 --- a/docs/php/builtins/io/stream_context_set_default.md +++ b/docs/php/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default()" description: "Sets the default stream context." sidebar: - order: 195 + order: 199 --- ## stream_context_set_default() @@ -18,6 +18,11 @@ Sets the default stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_option.md b/docs/php/builtins/io/stream_context_set_option.md index 2178ce7484..fb3f50d3e2 100644 --- a/docs/php/builtins/io/stream_context_set_option.md +++ b/docs/php/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option()" description: "Sets an option on the specified context." sidebar: - order: 196 + order: 200 --- ## stream_context_set_option() @@ -21,6 +21,11 @@ Sets an option on the specified context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_params.md b/docs/php/builtins/io/stream_context_set_params.md index da7dcc12fa..eaf336381f 100644 --- a/docs/php/builtins/io/stream_context_set_params.md +++ b/docs/php/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params()" description: "Sets parameters on the specified context." sidebar: - order: 197 + order: 201 --- ## stream_context_set_params() @@ -19,6 +19,11 @@ Sets parameters on the specified context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_copy_to_stream.md b/docs/php/builtins/io/stream_copy_to_stream.md index 1083006e1d..6a9294b801 100644 --- a/docs/php/builtins/io/stream_copy_to_stream.md +++ b/docs/php/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream()" description: "Copies data from one stream to another." sidebar: - order: 198 + order: 202 --- ## stream_copy_to_stream() @@ -21,6 +21,11 @@ Copies data from one stream to another. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_filter_register.md b/docs/php/builtins/io/stream_filter_register.md index 055527fdcc..3f60b09a8d 100644 --- a/docs/php/builtins/io/stream_filter_register.md +++ b/docs/php/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register()" description: "Registers a user-defined stream filter." sidebar: - order: 199 + order: 203 --- ## stream_filter_register() @@ -19,6 +19,11 @@ Registers a user-defined stream filter. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_filter_remove.md b/docs/php/builtins/io/stream_filter_remove.md index b03bbe6b8a..d4d9fa4715 100644 --- a/docs/php/builtins/io/stream_filter_remove.md +++ b/docs/php/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove()" description: "Removes a filter from a stream." sidebar: - order: 200 + order: 204 --- ## stream_filter_remove() @@ -18,6 +18,11 @@ Removes a filter from a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_contents.md b/docs/php/builtins/io/stream_get_contents.md index 6e41982c9a..bebe7d5ae9 100644 --- a/docs/php/builtins/io/stream_get_contents.md +++ b/docs/php/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents()" description: "Reads remainder of a stream into a string." sidebar: - order: 201 + order: 205 --- ## stream_get_contents() @@ -20,6 +20,11 @@ Reads remainder of a stream into a string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_filters.md b/docs/php/builtins/io/stream_get_filters.md index 813204a4ec..b306f050dc 100644 --- a/docs/php/builtins/io/stream_get_filters.md +++ b/docs/php/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters()" description: "Retrieves list of registered filters." sidebar: - order: 202 + order: 206 --- ## stream_get_filters() @@ -17,6 +17,11 @@ Retrieves list of registered filters. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_line.md b/docs/php/builtins/io/stream_get_line.md index ed332ef462..b328090567 100644 --- a/docs/php/builtins/io/stream_get_line.md +++ b/docs/php/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line()" description: "Gets line from stream resource up to a given delimiter." sidebar: - order: 203 + order: 207 --- ## stream_get_line() @@ -20,6 +20,11 @@ Gets line from stream resource up to a given delimiter. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_meta_data.md b/docs/php/builtins/io/stream_get_meta_data.md index 4f8a0c5105..11b796e7e2 100644 --- a/docs/php/builtins/io/stream_get_meta_data.md +++ b/docs/php/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data()" description: "Retrieves metadata from streams/file pointers." sidebar: - order: 204 + order: 208 --- ## stream_get_meta_data() @@ -18,6 +18,11 @@ Retrieves metadata from streams/file pointers. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_transports.md b/docs/php/builtins/io/stream_get_transports.md index 3e306c33b7..ae6e6635c3 100644 --- a/docs/php/builtins/io/stream_get_transports.md +++ b/docs/php/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports()" description: "Retrieves list of registered socket transports." sidebar: - order: 205 + order: 209 --- ## stream_get_transports() @@ -17,6 +17,11 @@ Retrieves list of registered socket transports. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_wrappers.md b/docs/php/builtins/io/stream_get_wrappers.md index c87abb259c..dc8ba7fca2 100644 --- a/docs/php/builtins/io/stream_get_wrappers.md +++ b/docs/php/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers()" description: "Retrieves list of registered streams." sidebar: - order: 206 + order: 210 --- ## stream_get_wrappers() @@ -17,6 +17,11 @@ Retrieves list of registered streams. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_is_local.md b/docs/php/builtins/io/stream_is_local.md index b30b1f8b69..62fcee0f6e 100644 --- a/docs/php/builtins/io/stream_is_local.md +++ b/docs/php/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local()" description: "Checks if a stream is a local stream." sidebar: - order: 207 + order: 211 --- ## stream_is_local() @@ -18,6 +18,11 @@ Checks if a stream is a local stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_isatty.md b/docs/php/builtins/io/stream_isatty.md index 59e169fb0a..693793775a 100644 --- a/docs/php/builtins/io/stream_isatty.md +++ b/docs/php/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty()" description: "Checks if a stream is a TTY." sidebar: - order: 208 + order: 212 --- ## stream_isatty() @@ -18,6 +18,11 @@ Checks if a stream is a TTY. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_resolve_include_path.md b/docs/php/builtins/io/stream_resolve_include_path.md index 079a7c35b6..f7be71820d 100644 --- a/docs/php/builtins/io/stream_resolve_include_path.md +++ b/docs/php/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path()" description: "Resolves filename against the include path." sidebar: - order: 209 + order: 213 --- ## stream_resolve_include_path() @@ -18,6 +18,11 @@ Resolves filename against the include path. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_select.md b/docs/php/builtins/io/stream_select.md index b5681c50ab..ca029ef007 100644 --- a/docs/php/builtins/io/stream_select.md +++ b/docs/php/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select()" description: "Runs the equivalent of the select() system call on the given arrays of streams." sidebar: - order: 210 + order: 214 --- ## stream_select() @@ -22,6 +22,11 @@ Runs the equivalent of the select() system call on the given arrays of streams. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_blocking.md b/docs/php/builtins/io/stream_set_blocking.md index 37866fddaf..d4200e093f 100644 --- a/docs/php/builtins/io/stream_set_blocking.md +++ b/docs/php/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking()" description: "Sets blocking/non-blocking mode on a stream." sidebar: - order: 211 + order: 215 --- ## stream_set_blocking() @@ -19,6 +19,11 @@ Sets blocking/non-blocking mode on a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_chunk_size.md b/docs/php/builtins/io/stream_set_chunk_size.md index 1a8db790b5..466afa57e0 100644 --- a/docs/php/builtins/io/stream_set_chunk_size.md +++ b/docs/php/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size()" description: "Sets the read chunk size on a stream." sidebar: - order: 212 + order: 216 --- ## stream_set_chunk_size() @@ -19,6 +19,11 @@ Sets the read chunk size on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_read_buffer.md b/docs/php/builtins/io/stream_set_read_buffer.md index fcbfcaa434..abb460af02 100644 --- a/docs/php/builtins/io/stream_set_read_buffer.md +++ b/docs/php/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer()" description: "Sets the read file buffering on a stream." sidebar: - order: 213 + order: 217 --- ## stream_set_read_buffer() @@ -19,6 +19,11 @@ Sets the read file buffering on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_timeout.md b/docs/php/builtins/io/stream_set_timeout.md index 990624b8c6..df3f0e17df 100644 --- a/docs/php/builtins/io/stream_set_timeout.md +++ b/docs/php/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout()" description: "Sets timeout period on a stream." sidebar: - order: 214 + order: 218 --- ## stream_set_timeout() @@ -20,6 +20,11 @@ Sets timeout period on a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_write_buffer.md b/docs/php/builtins/io/stream_set_write_buffer.md index 3ffbc69c95..8d6d84430d 100644 --- a/docs/php/builtins/io/stream_set_write_buffer.md +++ b/docs/php/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer()" description: "Sets the write file buffering on a stream." sidebar: - order: 215 + order: 219 --- ## stream_set_write_buffer() @@ -19,6 +19,11 @@ Sets the write file buffering on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_accept.md b/docs/php/builtins/io/stream_socket_accept.md index 4f49662b6e..f5cc8fd698 100644 --- a/docs/php/builtins/io/stream_socket_accept.md +++ b/docs/php/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept()" description: "Accept a connection on a socket created by stream_socket_server()." sidebar: - order: 216 + order: 220 --- ## stream_socket_accept() @@ -20,6 +20,11 @@ Accept a connection on a socket created by stream_socket_server(). **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_client.md b/docs/php/builtins/io/stream_socket_client.md index d8133a2e64..75fa1d76e5 100644 --- a/docs/php/builtins/io/stream_socket_client.md +++ b/docs/php/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 217 + order: 221 --- ## stream_socket_client() @@ -18,6 +18,11 @@ Open Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_enable_crypto.md b/docs/php/builtins/io/stream_socket_enable_crypto.md index c3a720b3b4..c176e9a060 100644 --- a/docs/php/builtins/io/stream_socket_enable_crypto.md +++ b/docs/php/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto()" description: "Turns encryption on/off on an already connected socket." sidebar: - order: 218 + order: 222 --- ## stream_socket_enable_crypto() @@ -21,6 +21,11 @@ Turns encryption on/off on an already connected socket. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_get_name.md b/docs/php/builtins/io/stream_socket_get_name.md index 66a9f9833d..de69154afc 100644 --- a/docs/php/builtins/io/stream_socket_get_name.md +++ b/docs/php/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name()" description: "Retrieve the name of the local or remote sockets." sidebar: - order: 219 + order: 223 --- ## stream_socket_get_name() @@ -19,6 +19,11 @@ Retrieve the name of the local or remote sockets. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_pair.md b/docs/php/builtins/io/stream_socket_pair.md index 678755e505..cc66e95eab 100644 --- a/docs/php/builtins/io/stream_socket_pair.md +++ b/docs/php/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair()" description: "Creates a pair of connected, indistinguishable socket streams." sidebar: - order: 220 + order: 224 --- ## stream_socket_pair() @@ -20,6 +20,11 @@ Creates a pair of connected, indistinguishable socket streams. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_recvfrom.md b/docs/php/builtins/io/stream_socket_recvfrom.md index 3b549eae0e..a148ea1503 100644 --- a/docs/php/builtins/io/stream_socket_recvfrom.md +++ b/docs/php/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom()" description: "Receives data from a socket, connected or not." sidebar: - order: 221 + order: 225 --- ## stream_socket_recvfrom() @@ -21,6 +21,11 @@ Receives data from a socket, connected or not. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_sendto.md b/docs/php/builtins/io/stream_socket_sendto.md index 2ae31eb084..d5dfdeab5a 100644 --- a/docs/php/builtins/io/stream_socket_sendto.md +++ b/docs/php/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto()" description: "Sends a message to a socket, whether it is connected or not." sidebar: - order: 222 + order: 226 --- ## stream_socket_sendto() @@ -21,6 +21,11 @@ Sends a message to a socket, whether it is connected or not. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_server.md b/docs/php/builtins/io/stream_socket_server.md index 658e6bb5c5..5ba8a540ec 100644 --- a/docs/php/builtins/io/stream_socket_server.md +++ b/docs/php/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server()" description: "Create an Internet or Unix domain server socket." sidebar: - order: 223 + order: 227 --- ## stream_socket_server() @@ -18,6 +18,11 @@ Create an Internet or Unix domain server socket. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_shutdown.md b/docs/php/builtins/io/stream_socket_shutdown.md index e36aaa1fe5..8c25fa13f3 100644 --- a/docs/php/builtins/io/stream_socket_shutdown.md +++ b/docs/php/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown()" description: "Shutdown a full-duplex connection." sidebar: - order: 224 + order: 228 --- ## stream_socket_shutdown() @@ -19,6 +19,11 @@ Shutdown a full-duplex connection. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_supports_lock.md b/docs/php/builtins/io/stream_supports_lock.md index 0cde57ca47..0a23c097ab 100644 --- a/docs/php/builtins/io/stream_supports_lock.md +++ b/docs/php/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock()" description: "Tells whether the stream supports locking." sidebar: - order: 225 + order: 229 --- ## stream_supports_lock() @@ -18,6 +18,11 @@ Tells whether the stream supports locking. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_register.md b/docs/php/builtins/io/stream_wrapper_register.md index b1eb6e6363..c6dada2f1f 100644 --- a/docs/php/builtins/io/stream_wrapper_register.md +++ b/docs/php/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register()" description: "Registers a URL wrapper implemented as a PHP class." sidebar: - order: 226 + order: 230 --- ## stream_wrapper_register() @@ -20,6 +20,11 @@ Registers a URL wrapper implemented as a PHP class. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_restore.md b/docs/php/builtins/io/stream_wrapper_restore.md index b3909c2564..1624e8bfd7 100644 --- a/docs/php/builtins/io/stream_wrapper_restore.md +++ b/docs/php/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore()" description: "Restores a previously unregistered built-in wrapper." sidebar: - order: 227 + order: 231 --- ## stream_wrapper_restore() @@ -18,6 +18,11 @@ Restores a previously unregistered built-in wrapper. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_unregister.md b/docs/php/builtins/io/stream_wrapper_unregister.md index 0b96c1b0ea..a8f0d919c5 100644 --- a/docs/php/builtins/io/stream_wrapper_unregister.md +++ b/docs/php/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister()" description: "Unregisters a previously registered URL wrapper." sidebar: - order: 228 + order: 232 --- ## stream_wrapper_unregister() @@ -18,6 +18,11 @@ Unregisters a previously registered URL wrapper. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/vfprintf.md b/docs/php/builtins/io/vfprintf.md index faef5f65ab..01fdc3b468 100644 --- a/docs/php/builtins/io/vfprintf.md +++ b/docs/php/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf()" description: "Write a formatted string to a stream." sidebar: - order: 229 + order: 233 --- ## vfprintf() @@ -20,6 +20,11 @@ Write a formatted string to a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json.md b/docs/php/builtins/json.md index 09a9e16654..4f84bcb3af 100644 --- a/docs/php/builtins/json.md +++ b/docs/php/builtins/json.md @@ -7,10 +7,10 @@ sidebar: ## JSON builtins -| Function | Signature | Returns | -|---|---|---| -| [`json_decode()`](./json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | -| [`json_encode()`](./json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | -| [`json_last_error()`](./json/json_last_error.md) | `(): int` | `int` | -| [`json_last_error_msg()`](./json/json_last_error_msg.md) | `(): string` | `string` | -| [`json_validate()`](./json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`json_decode()`](./json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | ✓ | ✓ | +| [`json_encode()`](./json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | ✓ | ✓ | +| [`json_last_error()`](./json/json_last_error.md) | `(): int` | `int` | ✓ | ✓ | +| [`json_last_error_msg()`](./json/json_last_error_msg.md) | `(): string` | `string` | ✓ | ✓ | +| [`json_validate()`](./json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/json/json_decode.md b/docs/php/builtins/json/json_decode.md index d3ff351578..ab07c77c52 100644 --- a/docs/php/builtins/json/json_decode.md +++ b/docs/php/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode()" description: "Decodes a JSON string." sidebar: - order: 230 + order: 234 --- ## json_decode() @@ -21,6 +21,11 @@ Decodes a JSON string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_encode.md b/docs/php/builtins/json/json_encode.md index 183c7517f9..b6e896d02a 100644 --- a/docs/php/builtins/json/json_encode.md +++ b/docs/php/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode()" description: "Returns the JSON representation of a value." sidebar: - order: 231 + order: 235 --- ## json_encode() @@ -20,6 +20,11 @@ Returns the JSON representation of a value. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_last_error.md b/docs/php/builtins/json/json_last_error.md index 4873936518..76f567c7ec 100644 --- a/docs/php/builtins/json/json_last_error.md +++ b/docs/php/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error()" description: "Returns the last error (if any) occurred during the last JSON encoding/decoding." sidebar: - order: 232 + order: 236 --- ## json_last_error() @@ -17,6 +17,11 @@ Returns the last error (if any) occurred during the last JSON encoding/decoding. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_last_error_msg.md b/docs/php/builtins/json/json_last_error_msg.md index fc3d0d284e..ea693822ef 100644 --- a/docs/php/builtins/json/json_last_error_msg.md +++ b/docs/php/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg()" description: "Returns the error string of the last json_encode() or json_decode() call." sidebar: - order: 233 + order: 237 --- ## json_last_error_msg() @@ -17,6 +17,11 @@ Returns the error string of the last json_encode() or json_decode() call. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_validate.md b/docs/php/builtins/json/json_validate.md index fbcd6e4743..690ba416d0 100644 --- a/docs/php/builtins/json/json_validate.md +++ b/docs/php/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate()" description: "Checks if a string contains valid JSON." sidebar: - order: 234 + order: 238 --- ## json_validate() @@ -20,6 +20,11 @@ Checks if a string contains valid JSON. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index c4b4f8457b..39b0a972a3 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -7,41 +7,41 @@ sidebar: ## Math builtins -| Function | Signature | Returns | -|---|---|---| -| [`abs()`](./math/abs.md) | `(int $num): mixed` | `mixed` | -| [`acos()`](./math/acos.md) | `(float $num): float` | `float` | -| [`asin()`](./math/asin.md) | `(float $num): float` | `float` | -| [`atan()`](./math/atan.md) | `(float $num): float` | `float` | -| [`atan2()`](./math/atan2.md) | `(float $y, float $x): float` | `float` | -| [`ceil()`](./math/ceil.md) | `(float $num): float` | `float` | -| [`clamp()`](./math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | -| [`cos()`](./math/cos.md) | `(float $num): float` | `float` | -| [`cosh()`](./math/cosh.md) | `(float $num): float` | `float` | -| [`deg2rad()`](./math/deg2rad.md) | `(float $num): float` | `float` | -| [`exp()`](./math/exp.md) | `(float $num): float` | `float` | -| [`fdiv()`](./math/fdiv.md) | `(float $num1, float $num2): float` | `float` | -| [`floor()`](./math/floor.md) | `(float $num): float` | `float` | -| [`fmod()`](./math/fmod.md) | `(float $num1, float $num2): float` | `float` | -| [`hypot()`](./math/hypot.md) | `(float $x, float $y): float` | `float` | -| [`intdiv()`](./math/intdiv.md) | `(int $num1, int $num2): int` | `int` | -| [`is_finite()`](./math/is_finite.md) | `(float $num): bool` | `bool` | -| [`is_infinite()`](./math/is_infinite.md) | `(float $num): bool` | `bool` | -| [`is_nan()`](./math/is_nan.md) | `(float $num): bool` | `bool` | -| [`log()`](./math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | -| [`log10()`](./math/log10.md) | `(float $num): float` | `float` | -| [`log2()`](./math/log2.md) | `(float $num): float` | `float` | -| [`max()`](./math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`min()`](./math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`mt_rand()`](./math/mt_rand.md) | `(int $min, int $max): int` | `int` | -| [`pi()`](./math/pi.md) | `(): float` | `float` | -| [`pow()`](./math/pow.md) | `(float $num, float $exponent): float` | `float` | -| [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | -| [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | -| [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | -| [`sin()`](./math/sin.md) | `(float $num): float` | `float` | -| [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | -| [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | -| [`tan()`](./math/tan.md) | `(float $num): float` | `float` | -| [`tanh()`](./math/tanh.md) | `(float $num): float` | `float` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`abs()`](./math/abs.md) | `(int $num): mixed` | `mixed` | ✓ | ✓ | +| [`acos()`](./math/acos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`asin()`](./math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan()`](./math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan2()`](./math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`ceil()`](./math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`clamp()`](./math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | +| [`cos()`](./math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`cosh()`](./math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`deg2rad()`](./math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`exp()`](./math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fdiv()`](./math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`floor()`](./math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fmod()`](./math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hypot()`](./math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | +| [`intdiv()`](./math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | +| [`is_finite()`](./math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_infinite()`](./math/is_infinite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_nan()`](./math/is_nan.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`log()`](./math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | ✓ | ✓ | +| [`log10()`](./math/log10.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`log2()`](./math/log2.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`max()`](./math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`min()`](./math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`mt_rand()`](./math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`pi()`](./math/pi.md) | `(): float` | `float` | ✓ | ✓ | +| [`pow()`](./math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | +| [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`sin()`](./math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tan()`](./math/tan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tanh()`](./math/tanh.md) | `(float $num): float` | `float` | ✓ | ✓ | diff --git a/docs/php/builtins/math/abs.md b/docs/php/builtins/math/abs.md index a1bd48d104..f5d3642755 100644 --- a/docs/php/builtins/math/abs.md +++ b/docs/php/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs()" description: "Absolute value." sidebar: - order: 235 + order: 239 --- ## abs() @@ -18,6 +18,11 @@ Absolute value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/abs.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/acos.md b/docs/php/builtins/math/acos.md index acf751064a..de003253a4 100644 --- a/docs/php/builtins/math/acos.md +++ b/docs/php/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos()" description: "Returns the arccosine of a number in radians." sidebar: - order: 236 + order: 240 --- ## acos() @@ -18,6 +18,11 @@ Returns the arccosine of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/acos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/asin.md b/docs/php/builtins/math/asin.md index e8a5038ca1..6b2ece7366 100644 --- a/docs/php/builtins/math/asin.md +++ b/docs/php/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin()" description: "Returns the arcsine of a number in radians." sidebar: - order: 237 + order: 241 --- ## asin() @@ -18,6 +18,11 @@ Returns the arcsine of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/asin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/atan.md b/docs/php/builtins/math/atan.md index f93ba54f99..adedd086b6 100644 --- a/docs/php/builtins/math/atan.md +++ b/docs/php/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan()" description: "Returns the arctangent of a number in radians." sidebar: - order: 238 + order: 242 --- ## atan() @@ -18,6 +18,11 @@ Returns the arctangent of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/atan2.md b/docs/php/builtins/math/atan2.md index f52df945f5..ae9a819286 100644 --- a/docs/php/builtins/math/atan2.md +++ b/docs/php/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2()" description: "Returns the arc tangent of two variables." sidebar: - order: 239 + order: 243 --- ## atan2() @@ -19,6 +19,11 @@ Returns the arc tangent of two variables. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/ceil.md b/docs/php/builtins/math/ceil.md index 0f5564bbd9..6db76f5a3a 100644 --- a/docs/php/builtins/math/ceil.md +++ b/docs/php/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil()" description: "Rounds a number up to the nearest integer." sidebar: - order: 240 + order: 244 --- ## ceil() @@ -18,6 +18,11 @@ Rounds a number up to the nearest integer. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/clamp.md b/docs/php/builtins/math/clamp.md index 1dc2122491..7a1fb45b89 100644 --- a/docs/php/builtins/math/clamp.md +++ b/docs/php/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp()" description: "Clamps a value to be within a specified range." sidebar: - order: 241 + order: 245 --- ## clamp() @@ -20,6 +20,11 @@ Clamps a value to be within a specified range. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/cos.md b/docs/php/builtins/math/cos.md index a07542ec1f..598c775655 100644 --- a/docs/php/builtins/math/cos.md +++ b/docs/php/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos()" description: "Returns the cosine of a number (radians)." sidebar: - order: 242 + order: 246 --- ## cos() @@ -18,6 +18,11 @@ Returns the cosine of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/cosh.md b/docs/php/builtins/math/cosh.md index ea9da090fb..8cccefa5da 100644 --- a/docs/php/builtins/math/cosh.md +++ b/docs/php/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh()" description: "Returns the hyperbolic cosine of a number." sidebar: - order: 243 + order: 247 --- ## cosh() @@ -18,6 +18,11 @@ Returns the hyperbolic cosine of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/deg2rad.md b/docs/php/builtins/math/deg2rad.md index 4ad81c1f2f..4f4ad04d85 100644 --- a/docs/php/builtins/math/deg2rad.md +++ b/docs/php/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad()" description: "Converts a degree value to radians." sidebar: - order: 244 + order: 248 --- ## deg2rad() @@ -18,6 +18,11 @@ Converts a degree value to radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/exp.md b/docs/php/builtins/math/exp.md index bb38c14642..0f812b4022 100644 --- a/docs/php/builtins/math/exp.md +++ b/docs/php/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp()" description: "Returns e raised to the power of a number." sidebar: - order: 245 + order: 249 --- ## exp() @@ -18,6 +18,11 @@ Returns e raised to the power of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/exp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/fdiv.md b/docs/php/builtins/math/fdiv.md index cb296e066c..939cf5f706 100644 --- a/docs/php/builtins/math/fdiv.md +++ b/docs/php/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv()" description: "Divides two numbers, according to IEEE 754." sidebar: - order: 246 + order: 250 --- ## fdiv() @@ -19,6 +19,11 @@ Divides two numbers, according to IEEE 754. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/floor.md b/docs/php/builtins/math/floor.md index c920dc07b5..8a44286492 100644 --- a/docs/php/builtins/math/floor.md +++ b/docs/php/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor()" description: "Rounds a number down to the nearest integer." sidebar: - order: 247 + order: 251 --- ## floor() @@ -18,6 +18,11 @@ Rounds a number down to the nearest integer. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/floor.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/fmod.md b/docs/php/builtins/math/fmod.md index 78a2c94254..983c7344c8 100644 --- a/docs/php/builtins/math/fmod.md +++ b/docs/php/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod()" description: "Returns the floating point remainder of the division of the arguments." sidebar: - order: 248 + order: 252 --- ## fmod() @@ -19,6 +19,11 @@ Returns the floating point remainder of the division of the arguments. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/hypot.md b/docs/php/builtins/math/hypot.md index 6b97393f53..f2b4feb806 100644 --- a/docs/php/builtins/math/hypot.md +++ b/docs/php/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot()" description: "Calculates the length of the hypotenuse of a right-angle triangle." sidebar: - order: 249 + order: 253 --- ## hypot() @@ -19,6 +19,11 @@ Calculates the length of the hypotenuse of a right-angle triangle. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/intdiv.md b/docs/php/builtins/math/intdiv.md index 06740dddfb..076ae1bb0d 100644 --- a/docs/php/builtins/math/intdiv.md +++ b/docs/php/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv()" description: "Integer division." sidebar: - order: 250 + order: 254 --- ## intdiv() @@ -19,6 +19,11 @@ Integer division. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_finite.md b/docs/php/builtins/math/is_finite.md index eed5a5021e..04b87e5ae2 100644 --- a/docs/php/builtins/math/is_finite.md +++ b/docs/php/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite()" description: "Checks whether a float is finite." sidebar: - order: 251 + order: 255 --- ## is_finite() @@ -18,6 +18,11 @@ Checks whether a float is finite. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_infinite.md b/docs/php/builtins/math/is_infinite.md index f2927a37a5..04e78a6023 100644 --- a/docs/php/builtins/math/is_infinite.md +++ b/docs/php/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite()" description: "Checks whether a float is infinite." sidebar: - order: 252 + order: 256 --- ## is_infinite() @@ -18,6 +18,11 @@ Checks whether a float is infinite. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_nan.md b/docs/php/builtins/math/is_nan.md index 7b5ab79a73..fbfce3be15 100644 --- a/docs/php/builtins/math/is_nan.md +++ b/docs/php/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan()" description: "Checks whether a float is NAN." sidebar: - order: 253 + order: 257 --- ## is_nan() @@ -18,6 +18,11 @@ Checks whether a float is NAN. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log.md b/docs/php/builtins/math/log.md index 8973c6a012..3633a3a032 100644 --- a/docs/php/builtins/math/log.md +++ b/docs/php/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log()" description: "Natural logarithm." sidebar: - order: 254 + order: 258 --- ## log() @@ -19,6 +19,11 @@ Natural logarithm. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log10.md b/docs/php/builtins/math/log10.md index ac21b08007..aaeec288d9 100644 --- a/docs/php/builtins/math/log10.md +++ b/docs/php/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10()" description: "Returns the base-10 logarithm of a number." sidebar: - order: 255 + order: 259 --- ## log10() @@ -18,6 +18,11 @@ Returns the base-10 logarithm of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log10.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log2.md b/docs/php/builtins/math/log2.md index 0401ccc162..f5d672ec27 100644 --- a/docs/php/builtins/math/log2.md +++ b/docs/php/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2()" description: "Returns the base-2 logarithm of a number." sidebar: - order: 256 + order: 260 --- ## log2() @@ -18,6 +18,11 @@ Returns the base-2 logarithm of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log2.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/max.md b/docs/php/builtins/math/max.md index c92f69734a..6df160c2fe 100644 --- a/docs/php/builtins/math/max.md +++ b/docs/php/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max()" description: "Find highest value." sidebar: - order: 257 + order: 261 --- ## max() @@ -19,6 +19,11 @@ Find highest value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/max.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/min.md b/docs/php/builtins/math/min.md index bb959c9892..8e1ad04b2f 100644 --- a/docs/php/builtins/math/min.md +++ b/docs/php/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min()" description: "Find lowest value." sidebar: - order: 258 + order: 262 --- ## min() @@ -19,6 +19,11 @@ Find lowest value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/min.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/mt_rand.md b/docs/php/builtins/math/mt_rand.md index 83d6dd9791..81776a4bf8 100644 --- a/docs/php/builtins/math/mt_rand.md +++ b/docs/php/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand()" description: "Generate a random value via the Mersenne Twister Random Number Generator." sidebar: - order: 259 + order: 263 --- ## mt_rand() @@ -19,6 +19,11 @@ Generate a random value via the Mersenne Twister Random Number Generator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/pi.md b/docs/php/builtins/math/pi.md index 231447b7b1..1d8992ba07 100644 --- a/docs/php/builtins/math/pi.md +++ b/docs/php/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi()" description: "Gets value of pi." sidebar: - order: 260 + order: 264 --- ## pi() @@ -17,6 +17,11 @@ Gets value of pi. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pi.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/pow.md b/docs/php/builtins/math/pow.md index 9fd3f5b18c..96c2717103 100644 --- a/docs/php/builtins/math/pow.md +++ b/docs/php/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow()" description: "Exponential expression." sidebar: - order: 261 + order: 265 --- ## pow() @@ -19,6 +19,11 @@ Exponential expression. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pow.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/rad2deg.md b/docs/php/builtins/math/rad2deg.md index a0382b92c2..afa45fbbdc 100644 --- a/docs/php/builtins/math/rad2deg.md +++ b/docs/php/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg()" description: "Converts a radian value to degrees." sidebar: - order: 262 + order: 266 --- ## rad2deg() @@ -18,6 +18,11 @@ Converts a radian value to degrees. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/rand.md b/docs/php/builtins/math/rand.md index 264d5e63e5..a03166aa1b 100644 --- a/docs/php/builtins/math/rand.md +++ b/docs/php/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand()" description: "Generate a random integer." sidebar: - order: 263 + order: 267 --- ## rand() @@ -19,6 +19,11 @@ Generate a random integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/random_int.md b/docs/php/builtins/math/random_int.md index 86f381fad7..b1952da4d4 100644 --- a/docs/php/builtins/math/random_int.md +++ b/docs/php/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int()" description: "Get a cryptographically secure, uniformly selected integer." sidebar: - order: 264 + order: 268 --- ## random_int() @@ -19,6 +19,11 @@ Get a cryptographically secure, uniformly selected integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index 3364d0c5fc..67b1868d98 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round()" description: "Rounds a float." sidebar: - order: 265 + order: 269 --- ## round() @@ -19,6 +19,11 @@ Rounds a float. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/round.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sin.md b/docs/php/builtins/math/sin.md index b0a33ec7fc..4b8f6ab992 100644 --- a/docs/php/builtins/math/sin.md +++ b/docs/php/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin()" description: "Returns the sine of a number (radians)." sidebar: - order: 266 + order: 270 --- ## sin() @@ -18,6 +18,11 @@ Returns the sine of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sinh.md b/docs/php/builtins/math/sinh.md index 8bed69d8d1..806e1dc782 100644 --- a/docs/php/builtins/math/sinh.md +++ b/docs/php/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh()" description: "Returns the hyperbolic sine of a number." sidebar: - order: 267 + order: 271 --- ## sinh() @@ -18,6 +18,11 @@ Returns the hyperbolic sine of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sqrt.md b/docs/php/builtins/math/sqrt.md index f6e648cb6b..8fde18fa98 100644 --- a/docs/php/builtins/math/sqrt.md +++ b/docs/php/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt()" description: "Returns the square root of a number." sidebar: - order: 268 + order: 272 --- ## sqrt() @@ -18,6 +18,11 @@ Returns the square root of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/tan.md b/docs/php/builtins/math/tan.md index b54db263a8..3f88a46f9b 100644 --- a/docs/php/builtins/math/tan.md +++ b/docs/php/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan()" description: "Returns the tangent of a number (radians)." sidebar: - order: 269 + order: 273 --- ## tan() @@ -18,6 +18,11 @@ Returns the tangent of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/tanh.md b/docs/php/builtins/math/tanh.md index b3d2c1bdbf..eda998619a 100644 --- a/docs/php/builtins/math/tanh.md +++ b/docs/php/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh()" description: "Returns the hyperbolic tangent of a number." sidebar: - order: 270 + order: 274 --- ## tanh() @@ -18,6 +18,11 @@ Returns the hyperbolic tangent of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc.md b/docs/php/builtins/misc.md index 91c54afd83..b14cfc6808 100644 --- a/docs/php/builtins/misc.md +++ b/docs/php/builtins/misc.md @@ -7,19 +7,19 @@ sidebar: ## Misc builtins -| Function | Signature | Returns | -|---|---|---| -| [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | -| [`define()`](./misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | -| [`defined()`](./misc/defined.md) | `(string $constant_name): bool` | `bool` | -| [`empty()`](./misc/empty.md) | `(mixed $value): bool` | `bool` | -| [`header()`](./misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | -| [`http_response_code()`](./misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | -| [`isset()`](./misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | -| [`php_uname()`](./misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | -| [`phpversion()`](./misc/phpversion.md) | `(): string` | `string` | -| [`print_r()`](./misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | -| [`serialize()`](./misc/serialize.md) | `(mixed $value): string` | `string` | -| [`unserialize()`](./misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | -| [`unset()`](./misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | -| [`var_dump()`](./misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`define()`](./misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | +| [`defined()`](./misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | +| [`empty()`](./misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`header()`](./misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | ✓ | ✓ | +| [`http_response_code()`](./misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | ✓ | ✓ | +| [`isset()`](./misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | ✓ | ✓ | +| [`php_uname()`](./misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | ✓ | ✓ | +| [`phpversion()`](./misc/phpversion.md) | `(): string` | `string` | ✓ | ✓ | +| [`print_r()`](./misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | ✓ | ✓ | +| [`serialize()`](./misc/serialize.md) | `(mixed $value): string` | `string` | ✓ | — | +| [`unserialize()`](./misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | ✓ | — | +| [`unset()`](./misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | ✓ | ✓ | +| [`var_dump()`](./misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | ✓ | ✓ | diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index 5245eebc5e..ca48729c20 100644 --- a/docs/php/builtins/misc/buffer_new.md +++ b/docs/php/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new()" description: "buffer_new() — misc builtin supported by Elephc." sidebar: - order: 271 + order: 275 --- ## buffer_new() @@ -18,11 +18,9 @@ function buffer_new(int $length): mixed **Returns**: `mixed` -_No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - +## Availability +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs)). +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/define.md b/docs/php/builtins/misc/define.md index b478f07a63..a9b336b8bb 100644 --- a/docs/php/builtins/misc/define.md +++ b/docs/php/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define()" description: "Defines a named constant at runtime." sidebar: - order: 272 + order: 276 --- ## define() @@ -19,6 +19,11 @@ Defines a named constant at runtime. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/define.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/define.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/defined.md b/docs/php/builtins/misc/defined.md index 7aaa83bb77..daff838a3e 100644 --- a/docs/php/builtins/misc/defined.md +++ b/docs/php/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined()" description: "Checks whether a given named constant exists." sidebar: - order: 273 + order: 277 --- ## defined() @@ -18,6 +18,11 @@ Checks whether a given named constant exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/defined.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/empty.md b/docs/php/builtins/misc/empty.md index d39bb13d6e..66b92ebd33 100644 --- a/docs/php/builtins/misc/empty.md +++ b/docs/php/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty()" description: "Determines whether a variable is considered empty." sidebar: - order: 274 + order: 278 --- ## empty() @@ -18,6 +18,11 @@ Determines whether a variable is considered empty. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/header.md b/docs/php/builtins/misc/header.md index 2c2e1e4382..a80983e367 100644 --- a/docs/php/builtins/misc/header.md +++ b/docs/php/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header()" description: "Sends a raw HTTP header." sidebar: - order: 275 + order: 279 --- ## header() @@ -20,6 +20,11 @@ Sends a raw HTTP header. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/header.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/header.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/http_response_code.md b/docs/php/builtins/misc/http_response_code.md index 000188dc4a..f6791efac3 100644 --- a/docs/php/builtins/misc/http_response_code.md +++ b/docs/php/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code()" description: "Gets or sets the HTTP response code." sidebar: - order: 276 + order: 280 --- ## http_response_code() @@ -18,6 +18,11 @@ Gets or sets the HTTP response code. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/isset.md b/docs/php/builtins/misc/isset.md index 52494d3ba2..fb402cf27f 100644 --- a/docs/php/builtins/misc/isset.md +++ b/docs/php/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset()" description: "Determines whether a variable is set and is not null." sidebar: - order: 277 + order: 281 --- ## isset() @@ -19,6 +19,11 @@ Determines whether a variable is set and is not null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/php_uname.md b/docs/php/builtins/misc/php_uname.md index 7f84895d09..248bad3bd2 100644 --- a/docs/php/builtins/misc/php_uname.md +++ b/docs/php/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname()" description: "Returns information about the operating system PHP is running on." sidebar: - order: 278 + order: 282 --- ## php_uname() @@ -18,6 +18,11 @@ Returns information about the operating system PHP is running on. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/phpversion.md b/docs/php/builtins/misc/phpversion.md index ec9ae47c43..1a79779777 100644 --- a/docs/php/builtins/misc/phpversion.md +++ b/docs/php/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion()" description: "Returns the current PHP version information." sidebar: - order: 279 + order: 283 --- ## phpversion() @@ -17,6 +17,11 @@ Returns the current PHP version information. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/print_r.md b/docs/php/builtins/misc/print_r.md index 5d45f0a5f4..f850db9f64 100644 --- a/docs/php/builtins/misc/print_r.md +++ b/docs/php/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r()" description: "Prints human-readable information about a variable." sidebar: - order: 280 + order: 284 --- ## print_r() @@ -19,6 +19,11 @@ Prints human-readable information about a variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/serialize.md b/docs/php/builtins/misc/serialize.md index 254d3e27e1..6e737a93d8 100644 --- a/docs/php/builtins/misc/serialize.md +++ b/docs/php/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize()" description: "Generates a storable representation of a value." sidebar: - order: 281 + order: 285 --- ## serialize() @@ -18,6 +18,11 @@ Generates a storable representation of a value. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/unserialize.md b/docs/php/builtins/misc/unserialize.md index d1d43ca6cd..fdb83fe98f 100644 --- a/docs/php/builtins/misc/unserialize.md +++ b/docs/php/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize()" description: "Creates a PHP value from a stored representation." sidebar: - order: 282 + order: 286 --- ## unserialize() @@ -19,6 +19,11 @@ Creates a PHP value from a stored representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/unset.md b/docs/php/builtins/misc/unset.md index b80b20fdff..10f96dff1a 100644 --- a/docs/php/builtins/misc/unset.md +++ b/docs/php/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset()" description: "Unsets the given variables." sidebar: - order: 283 + order: 287 --- ## unset() @@ -19,6 +19,11 @@ Unsets the given variables. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/var_dump.md b/docs/php/builtins/misc/var_dump.md index a3767a9f1c..8cefb76d21 100644 --- a/docs/php/builtins/misc/var_dump.md +++ b/docs/php/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump()" description: "Dumps information about a variable, including its type and value." sidebar: - order: 284 + order: 288 --- ## var_dump() @@ -19,6 +19,11 @@ Dumps information about a variable, including its type and value. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer.md b/docs/php/builtins/pointer.md index 1753862977..7a69dc20b2 100644 --- a/docs/php/builtins/pointer.md +++ b/docs/php/builtins/pointer.md @@ -7,24 +7,24 @@ sidebar: ## Pointer builtins -| Function | Signature | Returns | -|---|---|---| -| [`ptr()`](./pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | -| [`ptr_get()`](./pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | -| [`ptr_is_null()`](./pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | -| [`ptr_null()`](./pointer/ptr_null.md) | `(): mixed` | `mixed` | -| [`ptr_offset()`](./pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | -| [`ptr_read16()`](./pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read32()`](./pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read8()`](./pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read_string()`](./pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | -| [`ptr_set()`](./pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | -| [`ptr_sizeof()`](./pointer/ptr_sizeof.md) | `(string $type): int` | `int` | -| [`ptr_write16()`](./pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write32()`](./pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write8()`](./pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write_string()`](./pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | -| [`zval_free()`](./pointer/zval_free.md) | `(pointer $zval): void` | `void` | -| [`zval_pack()`](./pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | -| [`zval_type()`](./pointer/zval_type.md) | `(pointer $zval): int` | `int` | -| [`zval_unpack()`](./pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`ptr()`](./pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_get()`](./pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_is_null()`](./pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | ✓ | ✓ | +| [`ptr_null()`](./pointer/ptr_null.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_offset()`](./pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_read16()`](./pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read32()`](./pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read8()`](./pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read_string()`](./pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | ✓ | ✓ | +| [`ptr_set()`](./pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | ✓ | ✓ | +| [`ptr_sizeof()`](./pointer/ptr_sizeof.md) | `(string $type): int` | `int` | ✓ | ✓ | +| [`ptr_write16()`](./pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write32()`](./pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write8()`](./pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write_string()`](./pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | ✓ | ✓ | +| [`zval_free()`](./pointer/zval_free.md) | `(pointer $zval): void` | `void` | ✓ | — | +| [`zval_pack()`](./pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | ✓ | — | +| [`zval_type()`](./pointer/zval_type.md) | `(pointer $zval): int` | `int` | ✓ | — | +| [`zval_unpack()`](./pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | ✓ | — | diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index b9dea500b8..9508c7a85c 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr()" description: "Returns a raw pointer to the given variable." sidebar: - order: 285 + order: 289 --- ## ptr() @@ -18,6 +18,11 @@ Returns a raw pointer to the given variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index e42836d62e..bf295807b1 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get()" description: "Reads one machine word through a raw pointer and returns it as an integer." sidebar: - order: 286 + order: 290 --- ## ptr_get() @@ -18,6 +18,11 @@ Reads one machine word through a raw pointer and returns it as an integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 63a5dd4828..7c1484e556 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null()" description: "Returns true if the pointer is null." sidebar: - order: 287 + order: 291 --- ## ptr_is_null() @@ -18,6 +18,11 @@ Returns true if the pointer is null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index b3756ec496..ed2b38d16f 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null()" description: "Returns a null raw pointer." sidebar: - order: 288 + order: 292 --- ## ptr_null() @@ -17,6 +17,11 @@ Returns a null raw pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index 54b7b95c7c..d1fe66c738 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset()" description: "Returns a new pointer offset from the given pointer by the given byte count." sidebar: - order: 289 + order: 293 --- ## ptr_offset() @@ -19,6 +19,11 @@ Returns a new pointer offset from the given pointer by the given byte count. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index e1caeb7e71..01fb96f0de 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16()" description: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer." sidebar: - order: 290 + order: 294 --- ## ptr_read16() @@ -18,6 +18,11 @@ Reads one unsigned 16-bit word through a raw pointer and returns it as an intege **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index 351806a9d0..4bb3023d17 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32()" description: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer." sidebar: - order: 291 + order: 295 --- ## ptr_read32() @@ -18,6 +18,11 @@ Reads one unsigned 32-bit word through a raw pointer and returns it as an intege **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index c6325cbc2d..6f0906e47f 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8()" description: "Reads one unsigned byte through a raw pointer and returns it as an integer." sidebar: - order: 292 + order: 296 --- ## ptr_read8() @@ -18,6 +18,11 @@ Reads one unsigned byte through a raw pointer and returns it as an integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index 8240e0cd5a..52f27f4d03 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string()" description: "Copies raw bytes from a pointer into a PHP string of the given length." sidebar: - order: 293 + order: 297 --- ## ptr_read_string() @@ -19,6 +19,11 @@ Copies raw bytes from a pointer into a PHP string of the given length. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index 546adc0403..155d0c8060 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set()" description: "Writes one machine word through a raw pointer." sidebar: - order: 294 + order: 298 --- ## ptr_set() @@ -19,6 +19,11 @@ Writes one machine word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index b6c17eb104..f6a53fa4a2 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof()" description: "Returns the byte size of the named pointer target type." sidebar: - order: 295 + order: 299 --- ## ptr_sizeof() @@ -18,6 +18,11 @@ Returns the byte size of the named pointer target type. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write16.md b/docs/php/builtins/pointer/ptr_write16.md index 0c1f5f6711..f4f7c45bdd 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16()" description: "Writes one 16-bit word through a raw pointer." sidebar: - order: 296 + order: 300 --- ## ptr_write16() @@ -19,6 +19,11 @@ Writes one 16-bit word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index 6b456effc9..bd624bb964 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32()" description: "Writes one 32-bit word through a raw pointer." sidebar: - order: 297 + order: 301 --- ## ptr_write32() @@ -19,6 +19,11 @@ Writes one 32-bit word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index 8dd2dd86bb..46d8170bf7 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8()" description: "Writes one byte through a raw pointer." sidebar: - order: 298 + order: 302 --- ## ptr_write8() @@ -19,6 +19,11 @@ Writes one byte through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index b76409106e..970f2dd3d4 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string()" description: "Copies PHP string bytes into raw memory at the given pointer." sidebar: - order: 299 + order: 303 --- ## ptr_write_string() @@ -19,6 +19,11 @@ Copies PHP string bytes into raw memory at the given pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_free.md b/docs/php/builtins/pointer/zval_free.md index 36937da4d2..cd046be29d 100644 --- a/docs/php/builtins/pointer/zval_free.md +++ b/docs/php/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free()" description: "Frees a PHP zval pointer allocated by `zval_pack`." sidebar: - order: 300 + order: 304 --- ## zval_free() @@ -18,6 +18,11 @@ Frees a PHP zval pointer allocated by `zval_pack`. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_pack.md b/docs/php/builtins/pointer/zval_pack.md index 64d39a7f95..ae3b82705f 100644 --- a/docs/php/builtins/pointer/zval_pack.md +++ b/docs/php/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack()" description: "Packs an elephc runtime value into a heap-allocated PHP zval pointer." sidebar: - order: 301 + order: 305 --- ## zval_pack() @@ -18,6 +18,11 @@ Packs an elephc runtime value into a heap-allocated PHP zval pointer. **Returns**: `pointer` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_type.md b/docs/php/builtins/pointer/zval_type.md index c025ec8f3d..6f8351dfb0 100644 --- a/docs/php/builtins/pointer/zval_type.md +++ b/docs/php/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type()" description: "Returns the PHP zval type byte for a zval pointer." sidebar: - order: 302 + order: 306 --- ## zval_type() @@ -18,6 +18,11 @@ Returns the PHP zval type byte for a zval pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_unpack.md b/docs/php/builtins/pointer/zval_unpack.md index 72a3ea8d34..3d0d4c6aab 100644 --- a/docs/php/builtins/pointer/zval_unpack.md +++ b/docs/php/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack()" description: "Unpacks a PHP zval pointer into an owned elephc Mixed value." sidebar: - order: 303 + order: 307 --- ## zval_unpack() @@ -18,6 +18,11 @@ Unpacks a PHP zval pointer into an owned elephc Mixed value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process.md b/docs/php/builtins/process.md index dacbecf105..d1d1f080bc 100644 --- a/docs/php/builtins/process.md +++ b/docs/php/builtins/process.md @@ -7,16 +7,16 @@ sidebar: ## Process builtins -| Function | Signature | Returns | -|---|---|---| -| [`die()`](./process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./process/exec.md) | `(string $command): string` | `string` | -| [`exit()`](./process/exit.md) | `(int $status): void` | `void` | -| [`passthru()`](./process/passthru.md) | `(string $command): void` | `void` | -| [`pclose()`](./process/pclose.md) | `(resource $handle): int` | `int` | -| [`popen()`](./process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | -| [`readline()`](./process/readline.md) | `(string $prompt = null): mixed` | `mixed` | -| [`shell_exec()`](./process/shell_exec.md) | `(string $command): string` | `string` | -| [`sleep()`](./process/sleep.md) | `(int $seconds): int` | `int` | -| [`system()`](./process/system.md) | `(string $command): string` | `string` | -| [`usleep()`](./process/usleep.md) | `(int $microseconds): void` | `void` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`die()`](./process/die.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`exec()`](./process/exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`exit()`](./process/exit.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`passthru()`](./process/passthru.md) | `(string $command): void` | `void` | ✓ | ✓ | +| [`pclose()`](./process/pclose.md) | `(resource $handle): int` | `int` | ✓ | ✓ | +| [`popen()`](./process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | ✓ | ✓ | +| [`readline()`](./process/readline.md) | `(string $prompt = null): mixed` | `mixed` | ✓ | ✓ | +| [`shell_exec()`](./process/shell_exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`sleep()`](./process/sleep.md) | `(int $seconds): int` | `int` | ✓ | ✓ | +| [`system()`](./process/system.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`usleep()`](./process/usleep.md) | `(int $microseconds): void` | `void` | ✓ | ✓ | diff --git a/docs/php/builtins/process/die.md b/docs/php/builtins/process/die.md index e306fd41bd..0dfb3e0148 100644 --- a/docs/php/builtins/process/die.md +++ b/docs/php/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die()" description: "die() — process builtin supported by Elephc." sidebar: - order: 304 + order: 308 --- ## die() @@ -18,11 +18,9 @@ function die(int $status): void **Returns**: `void` -_No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - +## Availability +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/die.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/die.rs)). +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/exec.md b/docs/php/builtins/process/exec.md index 0daa23a6ed..84c1a4f231 100644 --- a/docs/php/builtins/process/exec.md +++ b/docs/php/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec()" description: "Executes an external program and returns the last line of output." sidebar: - order: 305 + order: 309 --- ## exec() @@ -18,6 +18,11 @@ Executes an external program and returns the last line of output. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 5b645f94e2..d29169c8cf 100644 --- a/docs/php/builtins/process/exit.md +++ b/docs/php/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit()" description: "exit() — process builtin supported by Elephc." sidebar: - order: 306 + order: 310 --- ## exit() @@ -18,11 +18,9 @@ function exit(int $status): void **Returns**: `void` -_No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - +## Availability +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/exit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/exit.rs)). +_No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/passthru.md b/docs/php/builtins/process/passthru.md index d23873fd09..52a25996ec 100644 --- a/docs/php/builtins/process/passthru.md +++ b/docs/php/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru()" description: "Executes an external program and passes its output directly." sidebar: - order: 307 + order: 311 --- ## passthru() @@ -18,6 +18,11 @@ Executes an external program and passes its output directly. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/pclose.md b/docs/php/builtins/process/pclose.md index 9f45bc241e..beff9e05bc 100644 --- a/docs/php/builtins/process/pclose.md +++ b/docs/php/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose()" description: "Closes process file pointer." sidebar: - order: 308 + order: 312 --- ## pclose() @@ -18,6 +18,11 @@ Closes process file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/popen.md b/docs/php/builtins/process/popen.md index bc6fdf943d..78c84729e6 100644 --- a/docs/php/builtins/process/popen.md +++ b/docs/php/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen()" description: "Opens process file pointer." sidebar: - order: 309 + order: 313 --- ## popen() @@ -19,6 +19,11 @@ Opens process file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/readline.md b/docs/php/builtins/process/readline.md index 0c836f938c..0a6c4a7861 100644 --- a/docs/php/builtins/process/readline.md +++ b/docs/php/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline()" description: "Reads a line from the user's terminal." sidebar: - order: 310 + order: 314 --- ## readline() @@ -18,6 +18,11 @@ Reads a line from the user's terminal. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/shell_exec.md b/docs/php/builtins/process/shell_exec.md index 240cf5800d..d3d69c4cf7 100644 --- a/docs/php/builtins/process/shell_exec.md +++ b/docs/php/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec()" description: "Executes a command via the shell and returns the complete output as a string." sidebar: - order: 311 + order: 315 --- ## shell_exec() @@ -18,6 +18,11 @@ Executes a command via the shell and returns the complete output as a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/sleep.md b/docs/php/builtins/process/sleep.md index 40fe11aa4c..7810d127af 100644 --- a/docs/php/builtins/process/sleep.md +++ b/docs/php/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep()" description: "Delays execution for a number of seconds." sidebar: - order: 312 + order: 316 --- ## sleep() @@ -18,6 +18,11 @@ Delays execution for a number of seconds. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/system.md b/docs/php/builtins/process/system.md index bfb15f8368..234eb89169 100644 --- a/docs/php/builtins/process/system.md +++ b/docs/php/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system()" description: "Executes an external program and displays the output." sidebar: - order: 313 + order: 317 --- ## system() @@ -18,6 +18,11 @@ Executes an external program and displays the output. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/system.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/usleep.md b/docs/php/builtins/process/usleep.md index c1bb02b9d8..caff5a8f59 100644 --- a/docs/php/builtins/process/usleep.md +++ b/docs/php/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep()" description: "Delays execution for a number of microseconds." sidebar: - order: 314 + order: 318 --- ## usleep() @@ -18,6 +18,11 @@ Delays execution for a number of microseconds. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex.md b/docs/php/builtins/regex.md index d0a58ab919..266750173f 100644 --- a/docs/php/builtins/regex.md +++ b/docs/php/builtins/regex.md @@ -7,11 +7,11 @@ sidebar: ## Regex builtins -| Function | Signature | Returns | -|---|---|---| -| [`mb_ereg_match()`](./regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | -| [`preg_match()`](./regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | -| [`preg_match_all()`](./regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | -| [`preg_replace()`](./regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | -| [`preg_replace_callback()`](./regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | -| [`preg_split()`](./regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`mb_ereg_match()`](./regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | +| [`preg_match()`](./regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match_all()`](./regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | ✓ | ✓ | +| [`preg_replace()`](./regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_replace_callback()`](./regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_split()`](./regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | ✓ | ✓ | diff --git a/docs/php/builtins/regex/mb_ereg_match.md b/docs/php/builtins/regex/mb_ereg_match.md index d8363dda77..85abfa21bf 100644 --- a/docs/php/builtins/regex/mb_ereg_match.md +++ b/docs/php/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match()" description: "Tests whether a regex pattern matches the beginning of a string (multibyte)." sidebar: - order: 315 + order: 319 --- ## mb_ereg_match() @@ -20,6 +20,11 @@ Tests whether a regex pattern matches the beginning of a string (multibyte). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_match.md b/docs/php/builtins/regex/preg_match.md index 92ba577e78..4821ef06da 100644 --- a/docs/php/builtins/regex/preg_match.md +++ b/docs/php/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match()" description: "Performs a regular expression match." sidebar: - order: 316 + order: 320 --- ## preg_match() @@ -20,6 +20,11 @@ Performs a regular expression match. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_match_all.md b/docs/php/builtins/regex/preg_match_all.md index 064c0bbd83..d286472894 100644 --- a/docs/php/builtins/regex/preg_match_all.md +++ b/docs/php/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all()" description: "Performs a global regular expression match and returns the number of matches." sidebar: - order: 317 + order: 321 --- ## preg_match_all() @@ -19,6 +19,11 @@ Performs a global regular expression match and returns the number of matches. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_replace.md b/docs/php/builtins/regex/preg_replace.md index 804e96c2d9..06e7330961 100644 --- a/docs/php/builtins/regex/preg_replace.md +++ b/docs/php/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace()" description: "Performs a regular expression search and replace." sidebar: - order: 318 + order: 322 --- ## preg_replace() @@ -20,6 +20,11 @@ Performs a regular expression search and replace. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_replace_callback.md b/docs/php/builtins/regex/preg_replace_callback.md index 3d0200b2b5..a54f565707 100644 --- a/docs/php/builtins/regex/preg_replace_callback.md +++ b/docs/php/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback()" description: "Performs a regular expression search and replace using a callback." sidebar: - order: 319 + order: 323 --- ## preg_replace_callback() @@ -20,6 +20,11 @@ Performs a regular expression search and replace using a callback. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_split.md b/docs/php/builtins/regex/preg_split.md index c135f843dd..3cc2a0ce20 100644 --- a/docs/php/builtins/regex/preg_split.md +++ b/docs/php/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split()" description: "Splits a string by a regular expression." sidebar: - order: 320 + order: 324 --- ## preg_split() @@ -21,6 +21,11 @@ Splits a string by a regular expression. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl.md b/docs/php/builtins/spl.md index c94612f328..3adfe281c2 100644 --- a/docs/php/builtins/spl.md +++ b/docs/php/builtins/spl.md @@ -7,17 +7,17 @@ sidebar: ## SPL builtins -| Function | Signature | Returns | -|---|---|---| -| [`iterator_apply()`](./spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | -| [`iterator_count()`](./spl/iterator_count.md) | `(traversable $iterator): int` | `int` | -| [`iterator_to_array()`](./spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | -| [`spl_autoload()`](./spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | -| [`spl_autoload_call()`](./spl/spl_autoload_call.md) | `(string $class): void` | `void` | -| [`spl_autoload_extensions()`](./spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | -| [`spl_autoload_functions()`](./spl/spl_autoload_functions.md) | `(): array` | `array` | -| [`spl_autoload_register()`](./spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | -| [`spl_autoload_unregister()`](./spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | -| [`spl_classes()`](./spl/spl_classes.md) | `(): array` | `array` | -| [`spl_object_hash()`](./spl/spl_object_hash.md) | `(object $object): string` | `string` | -| [`spl_object_id()`](./spl/spl_object_id.md) | `(object $object): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`iterator_apply()`](./spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | ✓ | ✓ | +| [`iterator_count()`](./spl/iterator_count.md) | `(traversable $iterator): int` | `int` | ✓ | ✓ | +| [`iterator_to_array()`](./spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | ✓ | ✓ | +| [`spl_autoload()`](./spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | ✓ | ✓ | +| [`spl_autoload_call()`](./spl/spl_autoload_call.md) | `(string $class): void` | `void` | ✓ | ✓ | +| [`spl_autoload_extensions()`](./spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | ✓ | ✓ | +| [`spl_autoload_functions()`](./spl/spl_autoload_functions.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_autoload_register()`](./spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | ✓ | ✓ | +| [`spl_autoload_unregister()`](./spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | ✓ | ✓ | +| [`spl_classes()`](./spl/spl_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_object_hash()`](./spl/spl_object_hash.md) | `(object $object): string` | `string` | ✓ | ✓ | +| [`spl_object_id()`](./spl/spl_object_id.md) | `(object $object): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/spl/iterator_apply.md b/docs/php/builtins/spl/iterator_apply.md index 437b93161e..a4f3bbc5b8 100644 --- a/docs/php/builtins/spl/iterator_apply.md +++ b/docs/php/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply()" description: "Call a function for every element in an iterator." sidebar: - order: 321 + order: 325 --- ## iterator_apply() @@ -20,6 +20,11 @@ Call a function for every element in an iterator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/iterator_count.md b/docs/php/builtins/spl/iterator_count.md index 42a005db0a..18ea84aeed 100644 --- a/docs/php/builtins/spl/iterator_count.md +++ b/docs/php/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count()" description: "Count the elements in an iterator." sidebar: - order: 322 + order: 326 --- ## iterator_count() @@ -18,6 +18,11 @@ Count the elements in an iterator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/iterator_to_array.md b/docs/php/builtins/spl/iterator_to_array.md index 40bf7bfb8a..c0e42e1e4b 100644 --- a/docs/php/builtins/spl/iterator_to_array.md +++ b/docs/php/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array()" description: "Copy the iterator into an array." sidebar: - order: 323 + order: 327 --- ## iterator_to_array() @@ -19,6 +19,11 @@ Copy the iterator into an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload.md b/docs/php/builtins/spl/spl_autoload.md index 7e1c44fd2b..5d1a916d74 100644 --- a/docs/php/builtins/spl/spl_autoload.md +++ b/docs/php/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload()" description: "Default implementation for __autoload()." sidebar: - order: 324 + order: 328 --- ## spl_autoload() @@ -19,6 +19,11 @@ Default implementation for __autoload(). **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_call.md b/docs/php/builtins/spl/spl_autoload_call.md index 7ebd9a9414..713933c9b9 100644 --- a/docs/php/builtins/spl/spl_autoload_call.md +++ b/docs/php/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call()" description: "Try all registered __autoload() functions to load the requested class." sidebar: - order: 325 + order: 329 --- ## spl_autoload_call() @@ -18,6 +18,11 @@ Try all registered __autoload() functions to load the requested class. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_extensions.md b/docs/php/builtins/spl/spl_autoload_extensions.md index 7d3d32a2f6..28a6ee3e12 100644 --- a/docs/php/builtins/spl/spl_autoload_extensions.md +++ b/docs/php/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions()" description: "Register and return default file extensions for spl_autoload." sidebar: - order: 326 + order: 330 --- ## spl_autoload_extensions() @@ -18,6 +18,11 @@ Register and return default file extensions for spl_autoload. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_functions.md b/docs/php/builtins/spl/spl_autoload_functions.md index 634305ff1f..67192fcf18 100644 --- a/docs/php/builtins/spl/spl_autoload_functions.md +++ b/docs/php/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions()" description: "Return all registered __autoload() functions." sidebar: - order: 327 + order: 331 --- ## spl_autoload_functions() @@ -17,6 +17,11 @@ Return all registered __autoload() functions. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_register.md b/docs/php/builtins/spl/spl_autoload_register.md index 61df76125f..425aee9d66 100644 --- a/docs/php/builtins/spl/spl_autoload_register.md +++ b/docs/php/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register()" description: "Register given function as __autoload() implementation." sidebar: - order: 328 + order: 332 --- ## spl_autoload_register() @@ -20,6 +20,11 @@ Register given function as __autoload() implementation. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_unregister.md b/docs/php/builtins/spl/spl_autoload_unregister.md index 2db76a7b6f..e50dc26c5b 100644 --- a/docs/php/builtins/spl/spl_autoload_unregister.md +++ b/docs/php/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister()" description: "Unregister given function as __autoload() implementation." sidebar: - order: 329 + order: 333 --- ## spl_autoload_unregister() @@ -18,6 +18,11 @@ Unregister given function as __autoload() implementation. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_classes.md b/docs/php/builtins/spl/spl_classes.md index 177d75646a..e817f411c0 100644 --- a/docs/php/builtins/spl/spl_classes.md +++ b/docs/php/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes()" description: "Return available SPL classes." sidebar: - order: 330 + order: 334 --- ## spl_classes() @@ -17,6 +17,11 @@ Return available SPL classes. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_object_hash.md b/docs/php/builtins/spl/spl_object_hash.md index f236da8bc7..4ca666e110 100644 --- a/docs/php/builtins/spl/spl_object_hash.md +++ b/docs/php/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash()" description: "Return hash id for given object." sidebar: - order: 331 + order: 335 --- ## spl_object_hash() @@ -18,6 +18,11 @@ Return hash id for given object. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_object_id.md b/docs/php/builtins/spl/spl_object_id.md index 868fe18b3d..652eeb3af8 100644 --- a/docs/php/builtins/spl/spl_object_id.md +++ b/docs/php/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id()" description: "Return the integer object handle for given object." sidebar: - order: 332 + order: 336 --- ## spl_object_id() @@ -18,6 +18,11 @@ Return the integer object handle for given object. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams.md b/docs/php/builtins/streams.md index a2ab662d99..4bf6d7b2a6 100644 --- a/docs/php/builtins/streams.md +++ b/docs/php/builtins/streams.md @@ -7,11 +7,11 @@ sidebar: ## Streams builtins -| Function | Signature | Returns | -|---|---|---| -| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`stream_bucket_append()`](./streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_bucket_prepend()`](./streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_filter_append()`](./streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`stream_filter_prepend()`](./streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_append()`](./streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_bucket_prepend()`](./streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_filter_append()`](./streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_prepend()`](./streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | diff --git a/docs/php/builtins/streams/fsockopen.md b/docs/php/builtins/streams/fsockopen.md index 3fef1b56db..f70cc8c754 100644 --- a/docs/php/builtins/streams/fsockopen.md +++ b/docs/php/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 333 + order: 337 --- ## fsockopen() @@ -22,6 +22,11 @@ Open Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/pfsockopen.md b/docs/php/builtins/streams/pfsockopen.md index cc4ca4f3db..0df585e2e2 100644 --- a/docs/php/builtins/streams/pfsockopen.md +++ b/docs/php/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen()" description: "Open persistent Internet or Unix domain socket connection." sidebar: - order: 334 + order: 338 --- ## pfsockopen() @@ -22,6 +22,11 @@ Open persistent Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_bucket_append.md b/docs/php/builtins/streams/stream_bucket_append.md index 65a0eb6647..ba7b364912 100644 --- a/docs/php/builtins/streams/stream_bucket_append.md +++ b/docs/php/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append()" description: "Appends a bucket to the brigade." sidebar: - order: 335 + order: 339 --- ## stream_bucket_append() @@ -19,6 +19,11 @@ Appends a bucket to the brigade. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_bucket_prepend.md b/docs/php/builtins/streams/stream_bucket_prepend.md index 7974675c19..ad19b9ae75 100644 --- a/docs/php/builtins/streams/stream_bucket_prepend.md +++ b/docs/php/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend()" description: "Prepends a bucket to the brigade." sidebar: - order: 336 + order: 340 --- ## stream_bucket_prepend() @@ -19,6 +19,11 @@ Prepends a bucket to the brigade. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_filter_append.md b/docs/php/builtins/streams/stream_filter_append.md index de3a459159..5ee98ec4c1 100644 --- a/docs/php/builtins/streams/stream_filter_append.md +++ b/docs/php/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append()" description: "Attaches a filter to a stream." sidebar: - order: 337 + order: 341 --- ## stream_filter_append() @@ -21,6 +21,11 @@ Attaches a filter to a stream. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_filter_prepend.md b/docs/php/builtins/streams/stream_filter_prepend.md index 891981ef58..f0b2544ad3 100644 --- a/docs/php/builtins/streams/stream_filter_prepend.md +++ b/docs/php/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend()" description: "Attaches a filter to a stream (prepend)." sidebar: - order: 338 + order: 342 --- ## stream_filter_prepend() @@ -21,6 +21,11 @@ Attaches a filter to a stream (prepend). **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index 15d7ceb547..9f54a0e732 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -7,76 +7,76 @@ sidebar: ## String builtins -| Function | Signature | Returns | -|---|---|---| -| [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | -| [`base64_decode()`](./string/base64_decode.md) | `(string $string): string` | `string` | -| [`base64_encode()`](./string/base64_encode.md) | `(string $string): string` | `string` | -| [`bin2hex()`](./string/bin2hex.md) | `(string $string): string` | `string` | -| [`chop()`](./string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`chr()`](./string/chr.md) | `(int $codepoint): string` | `string` | -| [`crc32()`](./string/crc32.md) | `(string $string): int` | `int` | -| [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | -| [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | -| [`gzcompress()`](./string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzdeflate()`](./string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzinflate()`](./string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`gzuncompress()`](./string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`hash()`](./string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | -| [`hash_algos()`](./string/hash_algos.md) | `(): array` | `array` | -| [`hash_copy()`](./string/hash_copy.md) | `(resource $context): mixed` | `mixed` | -| [`hash_equals()`](./string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | -| [`hash_final()`](./string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | -| [`hash_hmac()`](./string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | -| [`hash_init()`](./string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | -| [`hash_update()`](./string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | -| [`hex2bin()`](./string/hex2bin.md) | `(string $string): string` | `string` | -| [`html_entity_decode()`](./string/html_entity_decode.md) | `(string $string): string` | `string` | -| [`htmlentities()`](./string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`implode()`](./string/implode.md) | `(string $separator, array $array = null): string` | `string` | -| [`inet_ntop()`](./string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | -| [`inet_pton()`](./string/inet_pton.md) | `(string $ip): mixed` | `mixed` | -| [`ip2long()`](./string/ip2long.md) | `(string $ip): mixed` | `mixed` | -| [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | -| [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | -| [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`md5()`](./string/md5.md) | `(string $string, bool $binary = false): string` | `string` | -| [`nl2br()`](./string/nl2br.md) | `(string $string): string` | `string` | -| [`number_format()`](./string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | -| [`ord()`](./string/ord.md) | `(string $character): int` | `int` | -| [`printf()`](./string/printf.md) | `(string $format, ...$values): int` | `int` | -| [`rawurldecode()`](./string/rawurldecode.md) | `(string $string): string` | `string` | -| [`rawurlencode()`](./string/rawurlencode.md) | `(string $string): string` | `string` | -| [`rtrim()`](./string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`sha1()`](./string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | -| [`sprintf()`](./string/sprintf.md) | `(string $format, ...$values): string` | `string` | -| [`sscanf()`](./string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | -| [`str_contains()`](./string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ends_with()`](./string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ireplace()`](./string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_pad()`](./string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | -| [`str_repeat()`](./string/str_repeat.md) | `(string $string, int $times): string` | `string` | -| [`str_replace()`](./string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_split()`](./string/str_split.md) | `(string $string, int $length = 1): array` | `array` | -| [`str_starts_with()`](./string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`strcasecmp()`](./string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | -| [`strcmp()`](./string/strcmp.md) | `(string $string1, string $string2): int` | `int` | -| [`stripslashes()`](./string/stripslashes.md) | `(string $string): string` | `string` | -| [`strlen()`](./string/strlen.md) | `(string $string): int` | `int` | -| [`strpos()`](./string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strrev()`](./string/strrev.md) | `(string $string): string` | `string` | -| [`strrpos()`](./string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | -| [`strtolower()`](./string/strtolower.md) | `(string $string): string` | `string` | -| [`strtoupper()`](./string/strtoupper.md) | `(string $string): string` | `string` | -| [`substr()`](./string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | -| [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | -| [`trim()`](./string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`ucfirst()`](./string/ucfirst.md) | `(string $string): string` | `string` | -| [`ucwords()`](./string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | -| [`urldecode()`](./string/urldecode.md) | `(string $string): string` | `string` | -| [`urlencode()`](./string/urlencode.md) | `(string $string): string` | `string` | -| [`vprintf()`](./string/vprintf.md) | `(string $format, array $values): int` | `int` | -| [`vsprintf()`](./string/vsprintf.md) | `(string $format, array $values): string` | `string` | -| [`wordwrap()`](./string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_encode()`](./string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`bin2hex()`](./string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`chop()`](./string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`chr()`](./string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`crc32()`](./string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | +| [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | +| [`gzcompress()`](./string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzdeflate()`](./string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzinflate()`](./string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`gzuncompress()`](./string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`hash()`](./string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_algos()`](./string/hash_algos.md) | `(): array` | `array` | ✓ | ✓ | +| [`hash_copy()`](./string/hash_copy.md) | `(resource $context): mixed` | `mixed` | ✓ | ✓ | +| [`hash_equals()`](./string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | ✓ | ✓ | +| [`hash_final()`](./string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_hmac()`](./string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_init()`](./string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | ✓ | ✓ | +| [`hash_update()`](./string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | ✓ | ✓ | +| [`hex2bin()`](./string/hex2bin.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`html_entity_decode()`](./string/html_entity_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`htmlentities()`](./string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`implode()`](./string/implode.md) | `(string $separator, array $array = null): string` | `string` | ✓ | ✓ | +| [`inet_ntop()`](./string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`inet_pton()`](./string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`ip2long()`](./string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | +| [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`md5()`](./string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`nl2br()`](./string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`number_format()`](./string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | +| [`ord()`](./string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | +| [`printf()`](./string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`rawurldecode()`](./string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rawurlencode()`](./string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rtrim()`](./string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`sha1()`](./string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`sprintf()`](./string/sprintf.md) | `(string $format, ...$values): string` | `string` | ✓ | ✓ | +| [`sscanf()`](./string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`str_contains()`](./string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ends_with()`](./string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ireplace()`](./string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_pad()`](./string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | ✓ | ✓ | +| [`str_repeat()`](./string/str_repeat.md) | `(string $string, int $times): string` | `string` | ✓ | ✓ | +| [`str_replace()`](./string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_split()`](./string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | +| [`str_starts_with()`](./string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`strcasecmp()`](./string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`strcmp()`](./string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripslashes()`](./string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strlen()`](./string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strpos()`](./string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strrev()`](./string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strrpos()`](./string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | ✓ | ✓ | +| [`strtolower()`](./string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtoupper()`](./string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`substr()`](./string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`trim()`](./string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`ucfirst()`](./string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`ucwords()`](./string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | ✓ | ✓ | +| [`urldecode()`](./string/urldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`urlencode()`](./string/urlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`vprintf()`](./string/vprintf.md) | `(string $format, array $values): int` | `int` | ✓ | ✓ | +| [`vsprintf()`](./string/vsprintf.md) | `(string $format, array $values): string` | `string` | ✓ | ✓ | +| [`wordwrap()`](./string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/string/addslashes.md b/docs/php/builtins/string/addslashes.md index 04191523b7..6546d1ec1e 100644 --- a/docs/php/builtins/string/addslashes.md +++ b/docs/php/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes()" description: "Adds backslashes before characters that need to be escaped." sidebar: - order: 339 + order: 343 --- ## addslashes() @@ -18,6 +18,11 @@ Adds backslashes before characters that need to be escaped. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/base64_decode.md b/docs/php/builtins/string/base64_decode.md index 6349f09018..1a0c846992 100644 --- a/docs/php/builtins/string/base64_decode.md +++ b/docs/php/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode()" description: "Decodes a Base64-encoded string back into its original data." sidebar: - order: 340 + order: 344 --- ## base64_decode() @@ -18,6 +18,11 @@ Decodes a Base64-encoded string back into its original data. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/base64_encode.md b/docs/php/builtins/string/base64_encode.md index c9305f8bd4..437423d72d 100644 --- a/docs/php/builtins/string/base64_encode.md +++ b/docs/php/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode()" description: "Encodes binary data into a Base64 string." sidebar: - order: 341 + order: 345 --- ## base64_encode() @@ -18,6 +18,11 @@ Encodes binary data into a Base64 string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/bin2hex.md b/docs/php/builtins/string/bin2hex.md index 70bbb66f79..2fa6ceeca1 100644 --- a/docs/php/builtins/string/bin2hex.md +++ b/docs/php/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex()" description: "Converts binary data into its hexadecimal string representation." sidebar: - order: 342 + order: 346 --- ## bin2hex() @@ -18,6 +18,11 @@ Converts binary data into its hexadecimal string representation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/chop.md b/docs/php/builtins/string/chop.md index a5f81e8e05..cf3cbd551d 100644 --- a/docs/php/builtins/string/chop.md +++ b/docs/php/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop()" description: "Alias of rtrim: strips whitespace (or other characters) from the end of a string." sidebar: - order: 343 + order: 347 --- ## chop() @@ -19,6 +19,11 @@ Alias of rtrim: strips whitespace (or other characters) from the end of a string **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/chr.md b/docs/php/builtins/string/chr.md index 2a49122231..2dcff0cd7a 100644 --- a/docs/php/builtins/string/chr.md +++ b/docs/php/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr()" description: "Returns a one-character string from the given byte code point." sidebar: - order: 344 + order: 348 --- ## chr() @@ -18,6 +18,11 @@ Returns a one-character string from the given byte code point. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/crc32.md b/docs/php/builtins/string/crc32.md index 22219e2145..7a657e9ddc 100644 --- a/docs/php/builtins/string/crc32.md +++ b/docs/php/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32()" description: "Calculates the CRC32 polynomial of a string." sidebar: - order: 345 + order: 349 --- ## crc32() @@ -18,6 +18,11 @@ Calculates the CRC32 polynomial of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/explode.md b/docs/php/builtins/string/explode.md index 0e09835b20..922676c675 100644 --- a/docs/php/builtins/string/explode.md +++ b/docs/php/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode()" description: "Splits a string by a separator into an array of substrings." sidebar: - order: 346 + order: 350 --- ## explode() @@ -20,6 +20,11 @@ Splits a string by a separator into an array of substrings. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/explode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/grapheme_strrev.md b/docs/php/builtins/string/grapheme_strrev.md index 9964a742d0..707d554932 100644 --- a/docs/php/builtins/string/grapheme_strrev.md +++ b/docs/php/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev()" description: "Reverses a string by grapheme cluster, returning false on failure." sidebar: - order: 347 + order: 351 --- ## grapheme_strrev() @@ -18,6 +18,11 @@ Reverses a string by grapheme cluster, returning false on failure. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzcompress.md b/docs/php/builtins/string/gzcompress.md index 56438dda09..e9ba96537a 100644 --- a/docs/php/builtins/string/gzcompress.md +++ b/docs/php/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress()" description: "Compress a string using the ZLIB data format." sidebar: - order: 348 + order: 352 --- ## gzcompress() @@ -19,6 +19,11 @@ Compress a string using the ZLIB data format. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzdeflate.md b/docs/php/builtins/string/gzdeflate.md index 65a4d21349..6aacc75a90 100644 --- a/docs/php/builtins/string/gzdeflate.md +++ b/docs/php/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate()" description: "Deflate a string using the DEFLATE data format." sidebar: - order: 349 + order: 353 --- ## gzdeflate() @@ -19,6 +19,11 @@ Deflate a string using the DEFLATE data format. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzinflate.md b/docs/php/builtins/string/gzinflate.md index 8de121202a..599d2ab523 100644 --- a/docs/php/builtins/string/gzinflate.md +++ b/docs/php/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate()" description: "Inflate a deflated string." sidebar: - order: 350 + order: 354 --- ## gzinflate() @@ -19,6 +19,11 @@ Inflate a deflated string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzuncompress.md b/docs/php/builtins/string/gzuncompress.md index 9643a7d864..58482dc2c3 100644 --- a/docs/php/builtins/string/gzuncompress.md +++ b/docs/php/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress()" description: "Uncompress a compressed string." sidebar: - order: 351 + order: 355 --- ## gzuncompress() @@ -19,6 +19,11 @@ Uncompress a compressed string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash.md b/docs/php/builtins/string/hash.md index 66507e2540..adf4a3aad0 100644 --- a/docs/php/builtins/string/hash.md +++ b/docs/php/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash()" description: "Generates a hash value using the given algorithm." sidebar: - order: 352 + order: 356 --- ## hash() @@ -20,6 +20,11 @@ Generates a hash value using the given algorithm. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_algos.md b/docs/php/builtins/string/hash_algos.md index c125ab2d99..eb305c68c1 100644 --- a/docs/php/builtins/string/hash_algos.md +++ b/docs/php/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos()" description: "Returns an array of supported hashing algorithm names." sidebar: - order: 353 + order: 357 --- ## hash_algos() @@ -17,6 +17,11 @@ Returns an array of supported hashing algorithm names. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_copy.md b/docs/php/builtins/string/hash_copy.md index 77bb7266d5..a4162d7380 100644 --- a/docs/php/builtins/string/hash_copy.md +++ b/docs/php/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy()" description: "Copies the state of an incremental hashing context." sidebar: - order: 354 + order: 358 --- ## hash_copy() @@ -18,6 +18,11 @@ Copies the state of an incremental hashing context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_equals.md b/docs/php/builtins/string/hash_equals.md index 9ab6e0b6a9..4a26b9b313 100644 --- a/docs/php/builtins/string/hash_equals.md +++ b/docs/php/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals()" description: "Compares two strings using a constant-time algorithm." sidebar: - order: 355 + order: 359 --- ## hash_equals() @@ -19,6 +19,11 @@ Compares two strings using a constant-time algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_final.md b/docs/php/builtins/string/hash_final.md index f3551f6983..a843884520 100644 --- a/docs/php/builtins/string/hash_final.md +++ b/docs/php/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final()" description: "Finalizes an incremental hash and returns the digest string." sidebar: - order: 356 + order: 360 --- ## hash_final() @@ -19,6 +19,11 @@ Finalizes an incremental hash and returns the digest string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_hmac.md b/docs/php/builtins/string/hash_hmac.md index 10006bc301..d96486a98e 100644 --- a/docs/php/builtins/string/hash_hmac.md +++ b/docs/php/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac()" description: "Generates a keyed hash value using the HMAC method." sidebar: - order: 357 + order: 361 --- ## hash_hmac() @@ -21,6 +21,11 @@ Generates a keyed hash value using the HMAC method. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_init.md b/docs/php/builtins/string/hash_init.md index 79b66685b6..1d5bfde0a9 100644 --- a/docs/php/builtins/string/hash_init.md +++ b/docs/php/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init()" description: "Initialize an incremental hashing context." sidebar: - order: 358 + order: 362 --- ## hash_init() @@ -20,6 +20,11 @@ Initialize an incremental hashing context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_update.md b/docs/php/builtins/string/hash_update.md index f069e597be..16b02a2144 100644 --- a/docs/php/builtins/string/hash_update.md +++ b/docs/php/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update()" description: "Pumps data into an active incremental hashing context." sidebar: - order: 359 + order: 363 --- ## hash_update() @@ -19,6 +19,11 @@ Pumps data into an active incremental hashing context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hex2bin.md b/docs/php/builtins/string/hex2bin.md index 39cb3881cb..d5510ff8e2 100644 --- a/docs/php/builtins/string/hex2bin.md +++ b/docs/php/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin()" description: "Decodes a hexadecimal string back into its binary representation." sidebar: - order: 360 + order: 364 --- ## hex2bin() @@ -18,6 +18,11 @@ Decodes a hexadecimal string back into its binary representation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/html_entity_decode.md b/docs/php/builtins/string/html_entity_decode.md index 3a35b9080d..75b76d0d68 100644 --- a/docs/php/builtins/string/html_entity_decode.md +++ b/docs/php/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode()" description: "Converts HTML entities in a string back into their corresponding characters." sidebar: - order: 361 + order: 365 --- ## html_entity_decode() @@ -18,6 +18,11 @@ Converts HTML entities in a string back into their corresponding characters. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/htmlentities.md b/docs/php/builtins/string/htmlentities.md index 69b4d0b5b0..a3ef3f5b16 100644 --- a/docs/php/builtins/string/htmlentities.md +++ b/docs/php/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities()" description: "Converts all applicable characters in a string into their HTML entities." sidebar: - order: 362 + order: 366 --- ## htmlentities() @@ -20,6 +20,11 @@ Converts all applicable characters in a string into their HTML entities. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/htmlspecialchars.md b/docs/php/builtins/string/htmlspecialchars.md index 32b107fa75..874fb33cd9 100644 --- a/docs/php/builtins/string/htmlspecialchars.md +++ b/docs/php/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars()" description: "Converts the HTML special characters in a string into their entities." sidebar: - order: 363 + order: 367 --- ## htmlspecialchars() @@ -20,6 +20,11 @@ Converts the HTML special characters in a string into their entities. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/implode.md b/docs/php/builtins/string/implode.md index d243faa749..d234c66765 100644 --- a/docs/php/builtins/string/implode.md +++ b/docs/php/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode()" description: "Joins array elements into a single string using a separator." sidebar: - order: 364 + order: 368 --- ## implode() @@ -19,6 +19,11 @@ Joins array elements into a single string using a separator. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/implode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/inet_ntop.md b/docs/php/builtins/string/inet_ntop.md index 1c2a971a06..614b66d70f 100644 --- a/docs/php/builtins/string/inet_ntop.md +++ b/docs/php/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop()" description: "Converts a packed internet address to a human-readable representation." sidebar: - order: 365 + order: 369 --- ## inet_ntop() @@ -18,6 +18,11 @@ Converts a packed internet address to a human-readable representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/inet_pton.md b/docs/php/builtins/string/inet_pton.md index 84d7b7e2a7..add9ca9617 100644 --- a/docs/php/builtins/string/inet_pton.md +++ b/docs/php/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton()" description: "Converts a human-readable IP address to its packed in_addr representation." sidebar: - order: 366 + order: 370 --- ## inet_pton() @@ -18,6 +18,11 @@ Converts a human-readable IP address to its packed in_addr representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ip2long.md b/docs/php/builtins/string/ip2long.md index a7e935b880..33f40216d5 100644 --- a/docs/php/builtins/string/ip2long.md +++ b/docs/php/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long()" description: "Converts a string containing an IPv4 address into a long integer." sidebar: - order: 367 + order: 371 --- ## ip2long() @@ -18,6 +18,11 @@ Converts a string containing an IPv4 address into a long integer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/lcfirst.md b/docs/php/builtins/string/lcfirst.md index f52a1f4848..7281b99b3b 100644 --- a/docs/php/builtins/string/lcfirst.md +++ b/docs/php/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst()" description: "Lowercases the first character of a string." sidebar: - order: 368 + order: 372 --- ## lcfirst() @@ -18,6 +18,11 @@ Lowercases the first character of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/long2ip.md b/docs/php/builtins/string/long2ip.md index 264925d6c4..ff5fa8ece3 100644 --- a/docs/php/builtins/string/long2ip.md +++ b/docs/php/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip()" description: "Converts an IPv4 address from long integer to dotted string notation." sidebar: - order: 369 + order: 373 --- ## long2ip() @@ -18,6 +18,11 @@ Converts an IPv4 address from long integer to dotted string notation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ltrim.md b/docs/php/builtins/string/ltrim.md index 77f600bd7a..d09ed8a96f 100644 --- a/docs/php/builtins/string/ltrim.md +++ b/docs/php/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim()" description: "Strips whitespace (or other characters) from the beginning of a string." sidebar: - order: 370 + order: 374 --- ## ltrim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the beginning of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index fab1475d71..78afd7989c 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5()" description: "Calculates the MD5 hash of a string." sidebar: - order: 371 + order: 375 --- ## md5() @@ -19,6 +19,11 @@ Calculates the MD5 hash of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/md5.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index 525ed09f13..d42522a437 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br()" description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 372 + order: 376 --- ## nl2br() @@ -18,6 +18,11 @@ Inserts HTML line breaks before newlines in a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index 19e5c3eed3..e662c0d367 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format()" description: "Formats a number with grouped thousands." sidebar: - order: 373 + order: 377 --- ## number_format() @@ -21,6 +21,11 @@ Formats a number with grouped thousands. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index a645846562..32e73ff2ac 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord()" description: "Returns the ASCII value of the first character of a string." sidebar: - order: 374 + order: 378 --- ## ord() @@ -18,6 +18,11 @@ Returns the ASCII value of the first character of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ord.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index 156332b12c..c0cf42b38d 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf()" description: "Outputs a formatted string." sidebar: - order: 375 + order: 379 --- ## printf() @@ -19,6 +19,11 @@ Outputs a formatted string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 4202a3459b..6b283f1d50 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode()" description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 376 + order: 380 --- ## rawurldecode() @@ -18,6 +18,11 @@ Decodes an RFC 3986 percent-encoded string without treating '+' as a space. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index 52f27ea5cb..ba0bbe5bb9 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode()" description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 377 + order: 381 --- ## rawurlencode() @@ -18,6 +18,11 @@ URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces). **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index e8cc673ded..028e86cda6 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim()" description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 378 + order: 382 --- ## rtrim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the end of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index aa07710d1d..5a374b0e21 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1()" description: "Calculates the SHA-1 hash of a string." sidebar: - order: 379 + order: 383 --- ## sha1() @@ -19,6 +19,11 @@ Calculates the SHA-1 hash of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index d4f6da2cf2..25eb470c99 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf()" description: "Returns a formatted string." sidebar: - order: 380 + order: 384 --- ## sprintf() @@ -19,6 +19,11 @@ Returns a formatted string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 7f3206257d..785fb4b3c7 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf()" description: "Parses a string according to a format." sidebar: - order: 381 + order: 385 --- ## sscanf() @@ -20,6 +20,11 @@ Parses a string according to a format. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index 7f64d495eb..de8fd5a766 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains()" description: "Determines if a string contains a given substring." sidebar: - order: 382 + order: 386 --- ## str_contains() @@ -19,6 +19,11 @@ Determines if a string contains a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index 7a05ef2b75..164376bb04 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with()" description: "Checks if a string ends with a given substring." sidebar: - order: 383 + order: 387 --- ## str_ends_with() @@ -19,6 +19,11 @@ Checks if a string ends with a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index 75706424ad..e9bba3b29c 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace()" description: "Case-insensitive version of str_replace()." sidebar: - order: 384 + order: 388 --- ## str_ireplace() @@ -21,6 +21,11 @@ Case-insensitive version of str_replace(). **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 5abc77c431..49d4be8770 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad()" description: "Pads a string to a certain length with another string." sidebar: - order: 385 + order: 389 --- ## str_pad() @@ -21,6 +21,11 @@ Pads a string to a certain length with another string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index a3f9a4c0d9..a8ce358b4c 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat()" description: "Repeats a string a given number of times." sidebar: - order: 386 + order: 390 --- ## str_repeat() @@ -19,6 +19,11 @@ Repeats a string a given number of times. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index 6d21c42e23..d018238fd8 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace()" description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 387 + order: 391 --- ## str_replace() @@ -21,6 +21,11 @@ Replaces all occurrences of a search string with a replacement string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_split.md b/docs/php/builtins/string/str_split.md index 9e121f6f6d..3d6d9432f5 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split()" description: "Converts a string into an array of chunks of the given length." sidebar: - order: 388 + order: 392 --- ## str_split() @@ -19,6 +19,11 @@ Converts a string into an array of chunks of the given length. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index 9b653fa120..7e4c58141a 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with()" description: "Checks if a string starts with a given substring." sidebar: - order: 389 + order: 393 --- ## str_starts_with() @@ -19,6 +19,11 @@ Checks if a string starts with a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index fbfeeaa353..8cf921556e 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp()" description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 390 + order: 394 --- ## strcasecmp() @@ -19,6 +19,11 @@ Binary safe case-insensitive string comparison. Returns negative, zero, or posit **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index 279f8625bb..04ba651fe6 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp()" description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 391 + order: 395 --- ## strcmp() @@ -19,6 +19,11 @@ Binary safe string comparison. Returns negative, zero, or positive. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index 2a22f56a34..e75dee9b8a 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes()" description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 392 + order: 396 --- ## stripslashes() @@ -18,6 +18,11 @@ Removes backslashes from a string previously escaped by addslashes. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index 27592d4259..2447dc287c 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen()" description: "Returns the length of a string." sidebar: - order: 393 + order: 397 --- ## strlen() @@ -18,6 +18,11 @@ Returns the length of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index 6c5e3ef75b..8d25bdb870 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos()" description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 394 + order: 398 --- ## strpos() @@ -20,6 +20,11 @@ Finds the numeric position of the first occurrence of a substring. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index d867d1be94..cc53004b0a 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev()" description: "Reverses a string." sidebar: - order: 395 + order: 399 --- ## strrev() @@ -18,6 +18,11 @@ Reverses a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index 06cca6fde3..a3e1d06704 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos()" description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 396 + order: 400 --- ## strrpos() @@ -20,6 +20,11 @@ Finds the numeric position of the last occurrence of a substring. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index 94ff9bc781..02a3374550 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr()" description: "Returns the portion of a string starting at the first occurrence of a substring." sidebar: - order: 397 + order: 401 --- ## strstr() @@ -20,6 +20,11 @@ Returns the portion of a string starting at the first occurrence of a substring. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index 0f3edc503b..2a10ffbd44 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower()" description: "Converts a string to lowercase." sidebar: - order: 398 + order: 402 --- ## strtolower() @@ -18,6 +18,11 @@ Converts a string to lowercase. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index e61f7b42c1..931bdc74d2 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper()" description: "Converts a string to uppercase." sidebar: - order: 399 + order: 403 --- ## strtoupper() @@ -18,6 +18,11 @@ Converts a string to uppercase. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index e85c4bcf62..f59589b1fb 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr()" description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 400 + order: 404 --- ## substr() @@ -20,6 +20,11 @@ Returns a portion of a string specified by the offset and length. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index 687dde47e5..93fbe68cc9 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace()" description: "Replaces text within a portion of a string." sidebar: - order: 401 + order: 405 --- ## substr_replace() @@ -21,6 +21,11 @@ Replaces text within a portion of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 6466fbe3e8..8a7b53e39a 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim()" description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 402 + order: 406 --- ## trim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the beginning and end of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/trim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index 5ea51801e0..761cf864f3 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst()" description: "Uppercases the first character of a string." sidebar: - order: 403 + order: 407 --- ## ucfirst() @@ -18,6 +18,11 @@ Uppercases the first character of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index 28ae2cf3d8..e19ff6ae0f 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords()" description: "Uppercases the first character of each word in a string." sidebar: - order: 404 + order: 408 --- ## ucwords() @@ -19,6 +19,11 @@ Uppercases the first character of each word in a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index 61018c9334..c62400f0dd 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode()" description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 405 + order: 409 --- ## urldecode() @@ -18,6 +18,11 @@ Decodes a URL-encoded string, including '+' as a space. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index 2130cb43b5..f851695738 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode()" description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 406 + order: 410 --- ## urlencode() @@ -18,6 +18,11 @@ URL-encodes a string using application/x-www-form-urlencoded rules. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index 57b8496cff..ac88bfa994 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf()" description: "Outputs a formatted string using an array of values." sidebar: - order: 407 + order: 411 --- ## vprintf() @@ -19,6 +19,11 @@ Outputs a formatted string using an array of values. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index 3f180b06ed..7aec19ae51 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf()" description: "Returns a formatted string using an array of values." sidebar: - order: 408 + order: 412 --- ## vsprintf() @@ -19,6 +19,11 @@ Returns a formatted string using an array of values. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index 765a9f2ce3..afae05ae8d 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap()" description: "Wraps a string to a given number of characters." sidebar: - order: 409 + order: 413 --- ## wordwrap() @@ -21,6 +21,11 @@ Wraps a string to a given number of characters. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type.md b/docs/php/builtins/type.md index 75386c75ec..49c9af6645 100644 --- a/docs/php/builtins/type.md +++ b/docs/php/builtins/type.md @@ -7,28 +7,28 @@ sidebar: ## Type builtins -| Function | Signature | Returns | -|---|---|---| -| [`boolval()`](./type/boolval.md) | `(mixed $value): bool` | `bool` | -| [`ctype_alnum()`](./type/ctype_alnum.md) | `(string $text): bool` | `bool` | -| [`ctype_alpha()`](./type/ctype_alpha.md) | `(string $text): bool` | `bool` | -| [`ctype_digit()`](./type/ctype_digit.md) | `(string $text): bool` | `bool` | -| [`ctype_space()`](./type/ctype_space.md) | `(string $text): bool` | `bool` | -| [`floatval()`](./type/floatval.md) | `(mixed $value): float` | `float` | -| [`get_resource_id()`](./type/get_resource_id.md) | `(resource $resource): int` | `int` | -| [`get_resource_type()`](./type/get_resource_type.md) | `(resource $resource): string` | `string` | -| [`gettype()`](./type/gettype.md) | `(mixed $value): string` | `string` | -| [`intval()`](./type/intval.md) | `(mixed $value): int` | `int` | -| [`is_array()`](./type/is_array.md) | `(mixed $value): bool` | `bool` | -| [`is_bool()`](./type/is_bool.md) | `(mixed $value): bool` | `bool` | -| [`is_callable()`](./type/is_callable.md) | `(mixed $value): bool` | `bool` | -| [`is_float()`](./type/is_float.md) | `(mixed $value): bool` | `bool` | -| [`is_int()`](./type/is_int.md) | `(mixed $value): bool` | `bool` | -| [`is_iterable()`](./type/is_iterable.md) | `(mixed $value): bool` | `bool` | -| [`is_null()`](./type/is_null.md) | `(mixed $value): bool` | `bool` | -| [`is_numeric()`](./type/is_numeric.md) | `(mixed $value): bool` | `bool` | -| [`is_object()`](./type/is_object.md) | `(mixed $value): bool` | `bool` | -| [`is_resource()`](./type/is_resource.md) | `(mixed $value): bool` | `bool` | -| [`is_scalar()`](./type/is_scalar.md) | `(mixed $value): bool` | `bool` | -| [`is_string()`](./type/is_string.md) | `(mixed $value): bool` | `bool` | -| [`settype()`](./type/settype.md) | `(mixed $var, string $type): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`boolval()`](./type/boolval.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`ctype_alnum()`](./type/ctype_alnum.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_alpha()`](./type/ctype_alpha.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_digit()`](./type/ctype_digit.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_space()`](./type/ctype_space.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`floatval()`](./type/floatval.md) | `(mixed $value): float` | `float` | ✓ | ✓ | +| [`get_resource_id()`](./type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | +| [`get_resource_type()`](./type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | +| [`gettype()`](./type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | +| [`intval()`](./type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`is_array()`](./type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_bool()`](./type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_callable()`](./type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_float()`](./type/is_float.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_int()`](./type/is_int.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_iterable()`](./type/is_iterable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_null()`](./type/is_null.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_numeric()`](./type/is_numeric.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_object()`](./type/is_object.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_resource()`](./type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_scalar()`](./type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_string()`](./type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index b51be25e0e..c708d4f738 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval()" description: "Returns the boolean value of a variable." sidebar: - order: 410 + order: 414 --- ## boolval() @@ -18,6 +18,11 @@ Returns the boolean value of a variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index 3b0f4ea08e..e0009bd063 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum()" description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 411 + order: 415 --- ## ctype_alnum() @@ -18,6 +18,11 @@ Checks if all characters in the string are alphanumeric. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index dcec8ca7e7..1c78690cc1 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha()" description: "Checks if all characters in the string are alphabetic." sidebar: - order: 412 + order: 416 --- ## ctype_alpha() @@ -18,6 +18,11 @@ Checks if all characters in the string are alphabetic. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index 57dd9ec6df..0639b02ebc 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit()" description: "Checks if all characters in the string are digits." sidebar: - order: 413 + order: 417 --- ## ctype_digit() @@ -18,6 +18,11 @@ Checks if all characters in the string are digits. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index ed18d53168..3825e43c1c 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space()" description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 414 + order: 418 --- ## ctype_space() @@ -18,6 +18,11 @@ Checks if all characters in the string are whitespace characters. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index 3bf86190c7..089f094671 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval()" description: "Returns the float value of a variable." sidebar: - order: 415 + order: 419 --- ## floatval() @@ -18,6 +18,11 @@ Returns the float value of a variable. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index b6e5f9609b..1a2d929fd5 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id()" description: "Returns an integer identifier for the given resource." sidebar: - order: 416 + order: 420 --- ## get_resource_id() @@ -18,6 +18,11 @@ Returns an integer identifier for the given resource. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index f9ae6868cc..62c4d71e3a 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type()" description: "Returns the type of a resource." sidebar: - order: 417 + order: 421 --- ## get_resource_type() @@ -18,6 +18,11 @@ Returns the type of a resource. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index ddc8b75548..fd840fb77d 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype()" description: "Returns the type of a variable as a string." sidebar: - order: 418 + order: 422 --- ## gettype() @@ -18,6 +18,11 @@ Returns the type of a variable as a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index d6f8629483..8c584777c9 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval()" description: "Returns the integer value of a variable." sidebar: - order: 419 + order: 423 --- ## intval() @@ -18,6 +18,11 @@ Returns the integer value of a variable. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/intval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index f096891afb..1fdb21c808 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array()" description: "Checks whether a variable is an array." sidebar: - order: 420 + order: 424 --- ## is_array() @@ -18,6 +18,11 @@ Checks whether a variable is an array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index fddf5d2781..fd8c2600af 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool()" description: "Checks whether a variable is a boolean." sidebar: - order: 421 + order: 425 --- ## is_bool() @@ -18,6 +18,11 @@ Checks whether a variable is a boolean. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index 50e144beb0..b5dfc3c88d 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable()" description: "Checks whether a variable can be called as a function." sidebar: - order: 422 + order: 426 --- ## is_callable() @@ -18,6 +18,11 @@ Checks whether a variable can be called as a function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index 1cdf014f21..e18e41b2f9 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float()" description: "Checks whether a variable is a floating-point number." sidebar: - order: 423 + order: 427 --- ## is_float() @@ -18,6 +18,11 @@ Checks whether a variable is a floating-point number. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index b6ab7eb4b7..56569d6bd5 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int()" description: "Checks whether a variable is an integer." sidebar: - order: 424 + order: 428 --- ## is_int() @@ -18,6 +18,11 @@ Checks whether a variable is an integer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index c71165da23..931aae2140 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable()" description: "Checks whether a variable is iterable." sidebar: - order: 425 + order: 429 --- ## is_iterable() @@ -18,6 +18,11 @@ Checks whether a variable is iterable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index 2722731bdc..75062324ff 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null()" description: "Checks whether a variable is null." sidebar: - order: 426 + order: 430 --- ## is_null() @@ -18,6 +18,11 @@ Checks whether a variable is null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index b45733be8f..ebe68cb067 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric()" description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 427 + order: 431 --- ## is_numeric() @@ -18,6 +18,11 @@ Checks whether a variable is a number or a numeric string. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index a093703491..b858336723 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object()" description: "Checks whether a variable is an object." sidebar: - order: 428 + order: 432 --- ## is_object() @@ -18,6 +18,11 @@ Checks whether a variable is an object. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index 3821445800..3680361239 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource()" description: "Checks whether a variable is a resource." sidebar: - order: 429 + order: 433 --- ## is_resource() @@ -18,6 +18,11 @@ Checks whether a variable is a resource. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index 41e0acf8db..65aa1759aa 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar()" description: "Checks whether a variable is a scalar." sidebar: - order: 430 + order: 434 --- ## is_scalar() @@ -18,6 +18,11 @@ Checks whether a variable is a scalar. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index 60a624402d..c86c981aa9 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string()" description: "Checks whether a variable is a string." sidebar: - order: 431 + order: 435 --- ## is_string() @@ -18,6 +18,11 @@ Checks whether a variable is a string. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index 698862b041..609df86682 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype()" description: "Sets the type of a variable." sidebar: - order: 432 + order: 436 --- ## settype() @@ -19,6 +19,11 @@ Sets the type of a variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/settype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/classes.md b/docs/php/classes.md index 8d7b3b1427..4f12786a7c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -2,7 +2,7 @@ title: "Classes" description: "Classes, interfaces, abstract classes, traits, enums, properties, and inheritance." sidebar: - order: 9 + order: 10 --- ## Class declaration @@ -44,8 +44,9 @@ class Product implements Named { public function label() { return strtoupper($this->name()); } } ``` -- signature-only methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces +- signature-only instance/static methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces - interface inheritance flattened transitively with cycle detection +- static interface methods must be implemented by public static methods; instance and static methods cannot satisfy each other's contracts Interfaces may also declare `static` methods (PHP 8.3+). A concrete implementing class must provide a compatible public static method (an instance method does @@ -301,6 +302,7 @@ Rules: - The write visibility must not be weaker than the read visibility (`private public(set)` is rejected). - The property must be typed, and the modifier is not allowed on static properties. - Indirect writes through an array element (`$obj->items[] = x`, `$obj->items['k'] = x`) are writes too, so they honor the `set` visibility — not the (wider) read visibility. +- Abstract and interface property hook contracts may carry asymmetric write visibility on writable (`{ set; }`) contracts. `private(set)` contracts are final and cannot be implemented or redeclared by a concrete child property. ### Property redeclaration @@ -313,21 +315,30 @@ A child class may redeclare a property inherited from a non-private parent. The - `final` parent properties cannot be redeclared. - The child shares the parent's slot, so reads of the property from inherited methods see the child's value. +Private parent properties are different: they are not overridden. A child may +declare a same-named property, and elephc keeps a separate storage slot for each +declaring class. Methods declared on the parent keep reading/writing the private +parent slot, while child methods and external public access use the child +property. + ```php value; + } } class Child extends Base { public int $value = 5; } -echo (new Child())->value; // 5 +$child = new Child(); +echo $child->value . ":" . $child->parentValue(); // 5:1 ``` -Private parent properties are still considered separate slots in PHP, but elephc rejects same-named redeclarations through them; declare a different name in the child for now. - ### Property hooks (`get` / `set`) A property can define **hooks** that run when it is read or written, replacing hand-written getter and setter methods. The hooks live in a `{ ... }` block after the property name. Reading the property runs its `get` hook; assigning to it runs its `set` hook. @@ -371,6 +382,18 @@ $t->fahrenheit = 212.0; echo $t->celsius; // 100 ``` +The short `set => expr;` form stores `expr` into the property's own backing slot: + +```php + $this->value; + set => trim($value); + } +} +``` + Inside a property's own hook, `$this->prop` accesses the raw stored value rather than re-running the hook, so a hook may read and write the property it belongs to (a *backed* property): ```php @@ -392,7 +415,7 @@ Rules: - A property with a `get` hook but no `set` hook is read-only. Writing it is a compile-time error. - Hooked properties cannot have a default value and cannot be `static`, `final`, or `readonly`. - Each hook may be declared at most once per property. -- The short `set => expr;` form is not supported; use a block `set { ... }`. +- `set => expr;` is equivalent to assigning `expr` to the property's own backing slot. - Hooks are inherited by subclasses along with the property. Abstract classes, interfaces, and traits may declare hook *contracts* (`{ get; }`, `{ set; }`, `{ get; set; }`) with no body; see [Abstract properties](#abstract-properties) and [Interfaces](#interfaces). @@ -586,8 +609,8 @@ class Node { trait Fluent { // In a trait, `static` resolves to the class that uses the trait. - public function copy(): static { - return clone $this; + public function self(): static { + return $this; } } ``` @@ -614,11 +637,10 @@ $obj = new $cls(); // Foo instance echo gettype($obj); // "object" $missing = "NoSuchClass"; -$bad = new $missing(); // PHP null -echo gettype($bad); // "NULL" +new $missing(); // fatal: Class "NoSuchClass" not found ``` -elephc resolves the class name case-insensitively against compile-time class metadata, matching PHP class lookup. A match dispatches through the same allocation path as `new ClassName()`, including constructor calls, declared property defaults, and supported built-in/SPL runtime storage initialization. An unknown name currently yields PHP `null`; the missing-class fatal path is not yet tightened. +elephc resolves the class name case-insensitively against compile-time class metadata, matching PHP class lookup. A match dispatches through the same allocation path as `new ClassName()`, including constructor calls, declared property defaults, and supported built-in/SPL runtime storage initialization. Unknown class strings are fatal, and non-string class expressions are rejected with PHP's invalid class-name fatal. ## Dynamic method and static calls @@ -642,7 +664,7 @@ $static = "version"; echo $class::$static(); // 1.0 — both class and method dynamic ``` -`$obj->$name(...)` and `$class::$name(...)` are equivalent to `call_user_func([$obj, $name], ...)` / `call_user_func([$class, $name], ...)`. A dynamic method name on a literal class also works (`ClassName::$name(...)`). Arguments are forwarded positionally. A nullsafe dynamic method call (`$obj?->$name()`) is not yet supported, and **named arguments are rejected** in dynamic calls because the target method — and therefore its parameter names — is not known at compile time. +`$obj->$name(...)` and `$class::$name(...)` are equivalent to `call_user_func([$obj, $name], ...)` / `call_user_func([$class, $name], ...)`. A dynamic method name on a literal class also works (`ClassName::$name(...)`). Arguments are forwarded positionally. Nullsafe dynamic method calls (`$obj?->$name(...)` and `$obj?->{$expr}(...)`) short-circuit like other `?->` chains: if the receiver is `null`, the method-name expression and arguments are not evaluated and the result is `null`. **Named arguments are rejected** in dynamic calls because the target method — and therefore its parameter names — is not known at compile time. ## Anonymous classes (`new class {}`) @@ -763,12 +785,13 @@ Rules: - Instance methods may use `$this` (the case), `match ($this)`, the case `$this->name`, the backing `$this->value`, and `self::CONST`. - Static methods dispatch like class static methods and can act as factories. - An enum can `implements` one or more interfaces and be used through them. +- Enums can `use` traits that provide methods, including `insteadof` and `as` + adaptations. Traits with properties are rejected because PHP enums cannot have + properties. - `self`/`static` type hints in enum methods resolve to the enum. Enum constants are readable both inside the enum (`self::CONST`) and from outside it (`EnumName::CONST`). Enum method bodies are type-checked like class method bodies, so a mismatched return type or an undefined variable inside an enum method is reported. -Current limitations: using a trait inside an enum is not supported. - ### Built-in `SortDirection` PHP 8.6's global unit enum is available without a user declaration: @@ -790,11 +813,12 @@ echo sqlSortKeyword(SortDirection::Descending); // DESC ## Magic methods - `__construct(...)` — runs at instantiation - `__destruct()` — runs when the object is released (see below) +- `__clone()` — runs after `clone $object` creates the shallow copy - `__toString()` — string coercion - `__get($name)` — reading an undeclared property - `__set($name, $value)` — writing an undeclared property -- `__isset($name)` — `isset()`/`empty()` on an undeclared property -- `__unset($name)` — `unset()` of an undeclared property +- `__isset($name)` — `isset()`/`empty()` on an undeclared or inaccessible property +- `__unset($name)` — `unset()` of an undeclared or inaccessible property - `__invoke(...$args)` — calling an object directly - `__call($name, $args)` — intercepting missing instance methods - `__callStatic($name, $args)` — intercepting missing static methods @@ -867,6 +891,48 @@ echo User::orderBy("name"); // orderBy(name) → __callStatic Contract: `__callStatic` must be declared `public static` and takes exactly two arguments — the method name (`string`) and the argument list (`array`). +## Object cloning (`clone`) + +`clone $object` creates a shallow copy of a user object or `stdClass` instance: +declared property slots are copied into a new object, dynamic properties are +copied into a separate hash table, and object-valued properties still point at +the same nested object just like PHP. + +```php +child = new Child(); + } + public function __clone(): void { + // Runs on the copy after the shallow copy has been created. + $this->child->x = $this->child->x + 1; + } +} + +$a = new Boxed(); +$b = clone $a; +``` + +`__clone` must be non-static, take no arguments, and if it declares a return +type the return type must be `void`. PHP permits any visibility for `__clone`; +elephc checks that the `clone` expression is allowed to access the hook from the +current scope. A private `__clone` can therefore be used from inside the +declaring class, while cloning that object from unrelated global code is +rejected. + +Runtime-managed built-in objects whose storage is not represented as ordinary +declared properties are not cloneable yet. This includes `Fiber`, `Generator`, +Reflection objects, and SPL containers/iterators with native storage such as +`SplFixedArray`, `SplDoublyLinkedList`, `SplStack`, `SplQueue`, +`IteratorIterator`, `CallbackFilterIterator`, and +`RecursiveCallbackFilterIterator`. + ## Destructors (`__destruct`) A class may declare `public function __destruct(): void` to run cleanup when an @@ -922,7 +988,7 @@ Rules and notes: ## Attributes -PHP 8.0 attributes (`#[Name]`) decorate declarations. elephc parses attributes at every site PHP allows: classes, interfaces, traits, enums, enum cases, top-level functions, methods, properties, function/method/closure parameters (incl. promoted constructor params), closures, and arrow functions. Class, method, and property attributes have limited runtime reflection through the helpers below; attributes on other declaration sites are currently validated for syntax and kept only in the AST. +PHP 8.0 attributes (`#[Name]`) decorate declarations. elephc parses attributes at every site PHP allows: classes, interfaces, traits, enums, enum cases, top-level functions, methods, properties, function/method/closure parameters (incl. promoted constructor params), closures, and arrow functions. Class, function, method, property, and method-parameter attributes have limited runtime reflection through the helpers below; attributes on other declaration sites are currently validated for syntax and kept only in the AST. ```php name; // "elephc" echo $b->missing; // empty (Mixed null) ``` -User-defined attributes (e.g. `#[Author]`, `#[Pure]`, `#[Memoized]`) parse and persist in the AST. They have no compile-time semantics, but their **names** and **literal arguments** (positional and named) are reachable at runtime through lightweight helper builtins and the supported Reflection API: +User-defined attributes (e.g. `#[Author]`, `#[Pure]`, `#[Memoized]`) parse and persist in the AST. They have no compile-time semantics, but their **names** and literal positional or named **arguments** are reachable at runtime through lightweight helper builtins and the supported Reflection API: ```php $arg) { + echo $key, "=", $arg, "\n"; } -// /api/users -// GET -// 1 ← `true` echoes as 1 in PHP +// path=/api/users +// method=GET +// secure=1 ← `true` echoes as 1 in PHP ``` -`class_attribute_args()` returns an `array` whose elements preserve their original PHP type — strings stay strings, ints stay ints, booleans stay booleans, and `null` is `null`. The args are interned at compile time and boxed into mixed cells on demand at the call site. +`class_attribute_args()` returns an `array`: positional arguments keep integer keys, named arguments keep their PHP argument names as string keys, and values preserve their original PHP type — strings stay strings, ints stay ints, floats stay floats, booleans stay booleans, `null` is `null`, and supported nested arrays stay arrays. The args are interned at compile time and boxed into mixed cells on demand at the call site. For a more PHP-idiomatic API, `class_get_attributes()` and `ReflectionClass::getAttributes()` return the same data wrapped as `ReflectionAttribute` instances: @@ -1054,7 +1121,7 @@ $property = new ReflectionProperty('Controller', 'id'); echo $property->getAttributes()[0]->getName(); // Column ``` -`ReflectionAttribute` is a final synthetic built-in class with `getName(): string`, `getArguments(): array`, and `newInstance(): mixed` methods. It is populated internally by `class_get_attributes()` and the supported Reflection lookups and cannot be constructed or populated directly from user code; its metadata slots are private. `newInstance()` constructs the attribute class on demand when the attribute class exists in the program and the captured arguments are supported literals: +`ReflectionAttribute` is a final synthetic built-in class with `getName(): string`, `getArguments(): array`, and `newInstance(): mixed` methods. It is populated internally by `class_get_attributes()` and the supported Reflection lookups and cannot be constructed or populated directly from user code; its metadata slots are private. `getArguments()` returns the same `array` shape as `class_attribute_args()`. `newInstance()` constructs the attribute class on demand when the attribute class exists in the program and the captured positional or named arguments are supported literals: ```php value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. -- A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. -- The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, traits, and statically typed object expressions, including anonymous-class objects; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name->id lookup table that is not yet implemented. +- Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, `ClassName::class` strings, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** - a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` -> `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. +- A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. +- The flat `class_attribute_args()` helper materializes supported literal and array values directly, but symbolic references are resolved through `ReflectionClass::getAttributes()->getArguments()` / `ReflectionAttribute::newInstance()` instead. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()` and `getAttributes()`. `ReflectionMethod` and `ReflectionProperty` currently support `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. -- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. Inside eval fragments, registered generated/AOT free-function defaults are reflected through `ReflectionParameter` metadata. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isDeprecated()`, `hasType()`, `getType()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. The enum-case reflectors expose the same `isDeprecated()`, `hasType()`, and `getType()` defaults as `ReflectionClassConstant`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. +- `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. +- `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. +- For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. +- `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Runtime-held static `ReflectionProperty` objects can read `getValue()` and `isInitialized()` from the materialized declaring-class static-property snapshot. Live refresh after reflector construction and dynamic static `setValue()` still require richer runtime metadata. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Inside eval fragments, runtime-held generated/AOT function reflectors can also invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, indexed or string-keyed runtime argument arrays, and reflected default metadata for supported parameter defaults. Runtime-only generated/AOT function invocation outside eval still requires richer runtime/typechecker support. +- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. +- `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. + +- `ReflectionObject` construction helpers normalize named constructor arguments through the reflected constructor signature for straight-line receivers built from statically typed objects. + +`ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, +`isCallable()`, and `getClass()` are supported for retained parameter metadata. +`isArray()` / `isCallable()` follow PHP's legacy behavior: nullable or +non-nullable direct `array` / `callable` declarations report `true`, while +union declarations report `false`. `getClass()` follows PHP's legacy behavior +for nullable or non-nullable named object types and reports `null` for builtin, +union, intersection, or untyped parameters. + +`ReflectionProperty::isDefault()` is also supported for materialized declared +and dynamic properties. Declared properties report `true`; supported dynamic +properties materialized with `new ReflectionProperty($object, $property_name)` +report `false`. + +`ReflectionProperty::isDynamic()` reports `false` for supported declared +properties and `true` for public dynamic object properties materialized with +`new ReflectionProperty($object, $property_name)`. + +`ReflectionProperty::isLazy()` reports `false`, and +`skipLazyInitialization()` is accepted as a no-op for supported non-lazy +declared properties. ### Class constants @@ -1169,7 +1438,6 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa ## Limitations - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. -- Backed property hooks may read and write their own backing slot, but the short `set => expr;` form is not supported; use a block `set { ... }`. -- Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name) +- Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()`); per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array/object parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/eval.md b/docs/php/eval.md new file mode 100644 index 0000000000..9df120a23f --- /dev/null +++ b/docs/php/eval.md @@ -0,0 +1,1047 @@ +--- +title: "Eval" +description: "Experimental PHP eval support, including literal AOT lowering, dynamic interpreter fallback, scope synchronization, safety, and limitations." +sidebar: + order: 5 +--- + +`eval($code): mixed` evaluates a PHP fragment at the call site in the +caller-visible local scope. Dynamic source is parsed at runtime; eligible +string literals may be parsed and lowered ahead of time. It is a PHP language +construct, not a normal callable: `function_exists("eval")` and +`is_callable("eval")` return `false`, +and first-class callable syntax for `eval` is rejected. + +> **Experimental:** Eval support is still evolving. The supported fragment +> surface and the boundary between AOT lowering and interpreter fallback may +> change between releases. + +> **Security:** `eval()` is not a sandbox. Evaluated code can access the caller's +> state and the host-visible filesystem, environment, process, and network +> facilities exposed by supported builtins. Never evaluate untrusted input. + +The evaluated string must be a PHP fragment without an opening ` EIR -> native code | No eval context and no Magician bridge | +| Eligible string literal that needs only known scope reads/writes | AOT-lowered with direct locals or core eval-scope helpers | Eval scope only; no interpreter bridge | +| Dynamic string, runtime declaration, include, reference, dynamic dispatch, or unsupported literal construct | Parsed into EvalIR and interpreted at runtime | Persistent eval context, synchronized scopes, and `elephc_magician` | + +The compiler makes this decision per literal call. A program may therefore use +`eval()` without linking Magician when every call is fully handled by the AOT +paths. A program that needs interpreter fallback links the optional static +bridge into the standalone binary; programs without that requirement do not. +`--with-eval` force-links the bridge for testing or indirect use, but it is not +required to enable the language construct. + +The AOT and fallback paths are covered on macOS ARM64, Linux ARM64, and Linux +x86_64. CI runs dedicated eval integration shards for every supported target. + +## Performance + +A fully native literal fragment has no runtime parsing or interpreter-dispatch +cost and does not increase the binary with Magician. Scope-backed AOT adds only +the materialization required for the statically known reads and writes. + +Interpreter fallback parses dynamic source into EvalIR. Exact source strings +up to 64 KiB share a process-wide FIFO cache of 256 immutable parse results; +larger fragments bypass the cache. The cache avoids repeated tokenization and +parsing, but execution remains interpreted and scope/context state is never +cached. Force-linking with `--with-eval` also increases binary size even if no +call ultimately requires fallback. + +## Quick start + +```php +>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | +| Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | +| Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | +| Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | +| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax and PHP-compatible explicit set-hook parameter typing, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved bare class-like declaration/reference names are rejected. | +| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, comma-separated constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | +| Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | +| Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | + +`foreach` supports value-only and key-value iteration over indexed and +associative arrays, plus eval-declared and generated/AOT `Iterator` and +`IteratorAggregate` objects. Eval associative arrays preserve PHP insertion +order for iteration. + +Includes follow PHP's cwd-first lookup and then fall back to the eval call-site +directory. Included PHP files may contain normal `` blocks, raw text +outside PHP tags is echoed, a `return` inside the included file becomes the +include expression value, successful includes without `return` evaluate to `1`, +repeated `*_once` includes evaluate to `true`, missing `include` returns +`false` with warnings, and missing `require` aborts the eval fragment. + +## Supported expressions + +| Expression area | Support | +|---|---| +| Scalars | `null`, booleans, integers, floats, and strings. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | +| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function, method, and invokable-object targets, invokable eval and generated/AOT objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | +| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, union/nullable registered type validation, intersection object type validation, registered by-reference lvalue validation, and representable scalar/string constant expressions, resolved global/named class-like constants, null, empty-array, supported array-valued, or supported object-valued default arguments when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference constructor parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | +| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus representable scalar/string constant expressions, resolved global/named class-like constants, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private parameters when the current eval class scope satisfies PHP visibility; scalar, nullable, callable, Mixed, array, iterable, object, union, and intersection-object return values are checked and boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | +| Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | +| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, and reflected parameter defaults using the declaring callable scope, including trait-origin names for imported trait members. | +| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | +| Ternaries | Full ternary and short ternary (`?:`). | +| Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | + +Supported unary operators are `+`, `-`, `!`, and integer bitwise `~`. + +Supported binary operators are: + +| Category | Operators | +|---|---| +| Arithmetic | `+`, `-`, `*`, `**`, `/`, `%` | +| String | `.` | +| Integer bitwise and shifts | `&`, `|`, `^`, `<<`, `>>` | +| Logical | `&&`, `||`, `and`, `or`, `xor` | +| Null coalescing | `??` | +| Equality | `==`, `!=`, `===`, `!==` | +| Comparison | `<`, `<=`, `>`, `>=`, `<=>` | + +Array literals and append writes use PHP's next automatic integer key rule, +including integer-string keys such as `"2"`, boolean and float keys normalized +to integers, and `null` keys normalized to the empty string. Eval array writes +preserve native PHP copy-on-write behavior for by-value aliases while still +mutating reference aliases. + +## Functions and callable dispatch + +Eval-declared functions are callable from later eval fragments, from native code +after the eval barrier, and from string-literal `call_user_func()` / +`call_user_func_array()` paths. Eval-declared functions and registered AOT +global user functions support positional, named, and spread arguments inside +eval fragments. String keys in unpacked argument arrays bind as named +parameters. Direct calls and variable-function calls can also invoke registered +AOT global user functions when their by-reference parameters use generated +`mixed`/union-style boxed storage or one-word scalar raw storage (`int`, `bool`, +or `float`), string raw storage, or one-word heap raw storage (`array`, +`iterable`, and object/class parameters). Nullable scalar, native-only ABI +layouts, and other unsupported raw-storage by-reference free-function +parameters remain metadata-only until the function bridge has typed +staging/writeback for those ABI layouts. +Generated/AOT positional `&...$variadic` by-reference parameter tails are +supported for eval direct, variable-function, first-class callable, +`Closure::fromCallable()`, and `call_user_func_array()` dispatch when the +passed tail arguments have lvalue/ref-cell storage; element-level writes +propagate back to the caller variables, while rebinding the variadic container +itself remains local to the callee. +`call_user_func()` remains by-value for registered AOT free-function +by-reference parameters. + +String-variable and expression callable calls such as `$fn(...)` and +`$callbacks[0](...)` share the eval callable dispatcher for supported builtins, +eval-declared functions, and registered AOT functions. +Inside eval, `is_callable()` also supports PHP's optional `$syntax_only` probe +and by-reference `$callable_name` output for string, array, object, and +`Closure` callable forms. + +First-class callable syntax such as `strlen(...)`, `$object->method(...)`, +`ClassName::method(...)`, and `$invokable(...)` materializes eval callback +values as PHP-visible `Closure` objects that can be invoked through +`$callback(...)`, `call_user_func()`, and `call_user_func_array()`. Method +targets are validated when the first-class callable is created, including +missing, inaccessible, non-static static-syntax, and magic-call fallback cases; +static-syntax instance methods capture `$this` when PHP permits that form. +Namespaced function callables follow PHP's global fallback rule when the +namespaced function is not visible. +`Closure::fromCallable()` accepts the same supported string, callable-array, +object, and existing `Closure` callback values and materializes a PHP-visible +`Closure` object backed by the normalized eval callable target. `Closure::call()` +on those closure objects supports same-class method and invokable-object +rebinding, passes the call arguments after the receiver by value like PHP, and +reports PHP-compatible warning/null results for function and static-method +callables. `Closure::bind()` and `Closure::bindTo()` persistently bind eval +closure literals, function callable targets, same-class method targets, and +invokable-object closure targets to a new receiver. Function-target closures +accept omitted, `null`, or `"static"` scope, but reject explicit object/class +scope rebinding like PHP. The optional scope argument is accepted for method +closures, but eval's current binding model derives method scope from the bound +receiver rather than exposing the full PHP scope-mutation surface. + +Closure literals created inside eval are PHP-visible `Closure` objects: they +report true for `is_object()`, `get_class($fn)` returns `Closure`, +`$fn instanceof Closure` works, and they remain callable through direct calls, +`call_user_func()`, `call_user_func_array()`, `Closure::call()` for transiently +binding `$this` to an object when invoking the eval-created closure, +`Closure::bind()` / `Closure::bindTo()` for persistent receiver binding, and +`ReflectionFunction`. + +Inside eval fragments, two-element object-method callable arrays such as +`[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, +...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()`. Eval-declared +object methods support string-keyed named arguments through +`call_user_func_array()`. Eval-declared objects with `__invoke()` can be called +through `$object(...)`, `call_user_func($object, ...)`, and +`call_user_func_array($object, [...])`, and `is_callable($object)` reports them +as callable. Generated/AOT objects with bridge-supported `__invoke()` metadata +use the same direct and callback call paths, including named/defaulted argument +binding, and non-invokable generated/AOT objects report PHP-compatible +direct-call or callback errors. Eval-declared classes that extend generated/AOT +parents inherit bridge-supported parent instance-method callable probes and +`__invoke()` object-call dispatch. Static method callables can use +`["ClassName", "method"]` or +`"ClassName::method"` through `$cb(...)`, `call_user_func()`, and +`call_user_func_array()`. Eval-declared static methods also support string-keyed +named arguments through `call_user_func_array()`; generated/AOT static method +fallback supports the same named-argument and positional variadic-tail binding +for public/protected/private scalar, nullable, callable, Mixed, array, iterable, +object, union, and intersection-object parameter signatures, including +registered by-reference lvalue validation, when the current eval class scope +satisfies PHP visibility. Generated AOT bridge dispatch can invoke +`mixed`/untyped, scalar including string, nullable scalar, array, iterable, +object, and supported union/intersection object shapes, with by-reference method +and constructor parameters limited to the staged scalar/nullable +scalar/Mixed/array/iterable/object slice. + +Post-barrier native direct calls and string-literal `call_user_func()` callbacks +currently accept simple positional arguments. Post-barrier +`call_user_func_array()` callbacks can pass indexed or string-keyed argument +containers to eval-declared functions. + +## Classes and objects + +Eval-declared classes support inheritance, public/protected/private properties, +comma-separated simple property declarations, PHP's legacy `var` marker for +public properties, and methods, asymmetric property write visibility (`private(set)` / +`protected(set)`), constructor property promotion including by-reference promotion +for variable, array-element, object-property, static-property, +property-array-element, static-property-array-element, and default-value targets, +concrete property `get` / `set` hooks, interface property hook contract checks +including asymmetric write visibility, abstract property hook contracts +including asymmetric write visibility, property-level `readonly`, `readonly class`, +`__construct()`, abstract classes and methods, final classes, methods, and +properties, trait composition with `insteadof` conflict resolution and `as` +aliases/visibility adaptations, interface implementation checks, static +properties, static methods, static interface method contracts, generated/AOT +interface method contract checks, single and +comma-separated class, interface, trait, and enum constants including `final` +constants, class-level attributes, +`ClassName::class` literals, magic method fallback through `__call()` and +`__callStatic()`, and magic property fallback through `__get()` and `__set()`. +Reserved PHP bare class-like declaration and reference names are rejected, while +semi-reserved names that PHP allows, such as `enum`, `from`, and `resource`, +remain valid. +Explicit concrete `set` hook parameter types are retained as settable-property +metadata and must be PHP-compatible supertypes of the property type. +Eval traits also accept the legacy `var` marker for public properties. +PHP's global `#[Override]` marker is validated on eval-declared methods against +non-private parent methods and eval interface method contracts, and rejected on +non-method OOP declaration targets. On eval-declared interface methods, the +marker requires a matching method inherited from an eval, builtin, or +generated/AOT parent interface. +Eval validates method override and interface method parameter and return types +with PHP-style parameter contravariance and return covariance for supported +declared type metadata, including nullable, union, `mixed`, `self`, `parent`, +`static`, class, and interface types, including generated/AOT parent method +metadata retained by the eval bridge. +Concrete eval classes also enforce inherited generated/AOT abstract parent +method requirements when reflection/signature metadata is available, including +requirements carried through intermediate eval abstract classes. +The same applies to generated/AOT abstract parent property-hook contracts when +the bridge retains the class property contract metadata. +Abstract classes may defer missing interface methods and property contracts, but +declared or inherited members that cover an interface contract are validated at +declaration time. Generated/AOT interface method and property-hook contracts are +validated when their bridge metadata is available. +Eval validates inherited property redeclarations with PHP-style invariant +property types, matching static storage, compatible read/write visibility, and +readonly compatibility: child redeclarations may add `readonly`, but may not +remove inherited `readonly`. The same checks apply to generated/AOT parent +property metadata when the eval bridge exposes reflection flags and declared +property types. +Typed eval-declared instance and static properties track PHP initialization +state: reads before initialization or after `unset()` throw the same catchable +`Error`, while untyped properties and explicit `= null` defaults are initialized +to `null`. +Eval validates inherited class/interface constant redeclarations with PHP-style +visibility compatibility, including generated/AOT parent and interface constants +when their reflection metadata is available, and rejects non-public interface +constants. +Eval-declared method calls also enforce declared return values at runtime, with +weak scalar coercions and PHP-style handling for `void`, `never`, `self`, +`parent`, and late-bound `static`. +`isset()`, `empty()`, and `unset()` on missing or inaccessible eval properties +dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate +ordering. `instanceof` works with eval-declared classes and interfaces, +generated/AOT runtime objects, dynamic string targets, dynamic object targets, +and parenthesized target expressions. +Eval-declared objects in string contexts dispatch through public +parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, +callable `strval()` dispatch, and weak `string` parameter coercion. Classes +with a compatible `__toString()` satisfy `Stringable` implicitly. +Eval validates magic method staticness, visibility, arity, by-reference +parameter bans, and relevant declared parameter/return-type contracts for +`__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, +`__callStatic()`, `__sleep()`, `__wakeup()`, `__serialize()`, +`__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, +`__clone()`, `__destruct()`, and `__construct()` when dynamic classes or traits +are declared. +Eval-declared class-like metadata and aliases are synchronized across generated +eval contexts, so later eval fragments inside AOT methods can introspect +classes, interfaces, traits, enums, and aliases declared by an earlier eval call +in the same process. +Member +visibility is checked at runtime for eval-declared objects and +static/class-constant accesses. Class-level attributes declared on eval classes, +interfaces, traits, and enums, plus bridge-registered generated/AOT class-level +attributes, are visible through `class_attribute_names()`, +`class_attribute_args()`, and `class_get_attributes()` when their arguments fit +the supported literal positional/named subset (`string`, `int`, `float`, `bool`, +`null`, negated numeric literals, `ClassName::class` strings, or positional +or scalar-keyed array literals containing the same supported values). Positional +arguments keep integer keys in `class_attribute_args()` / +`ReflectionAttribute::getArguments()`, named arguments keep their PHP names as +string keys, and array literal keys support string keys plus PHP-normalized +integer, boolean, null, and float keys. +`ReflectionAttribute::newInstance()` instantiates eval-declared or +bridge-supported generated/AOT attribute classes from those materialized +attributes, and `ReflectionAttribute::getTarget()` / +`isRepeated()` report the reflected owner target and same-owner repetition +metadata. +Attribute names remain visible when an attribute uses unsupported argument +syntax, but requesting those arguments is a runtime fatal. +Private parent properties shadowed by same-named child properties use separate +runtime storage, so parent methods keep seeing the private parent value while +child methods and public access see the child property. +`ReflectionClass::getAttributes()`, `ReflectionEnum::getAttributes()`, +`ReflectionMethod::getAttributes()`, `ReflectionProperty::getAttributes()`, +`ReflectionClassConstant::getAttributes()`, and +`ReflectionParameter::getAttributes()` expose eval-retained class, enum, method, +property, class-constant, and method-parameter attributes for eval-declared +class-like symbols and bridge-registered generated/AOT class-level, method, +property, and class-constant attributes when their arguments fit the same +literal positional/named subset. Attribute array literal keys support string +keys plus PHP-normalized integer, boolean, null, and float keys; dynamic or +otherwise unsupported attribute array keys are still unsupported metadata. +`getName()` returns the reflected class, member, or parameter name +for those owners. `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, +`ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `getDocComment()` and report `false` because +eval does not retain docblock text. `ReflectionClass`, `ReflectionFunction`, +and `ReflectionMethod` expose `getExtensionName()` and `getExtension()` and +report `false` / `null` for eval-declared user symbols. +`ReflectionClass`, `ReflectionFunction`, and `ReflectionMethod` expose +`getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval +declarations. File names use PHP's synthetic eval file format from the current +eval call site, and line numbers are one-based inside the evaluated fragment. +Generated/AOT metadata for `ReflectionClass` over classes, interfaces, traits, +and enums, plus `ReflectionMethod`, exposes the original source file and +declaration line when EIR source metadata is available. AOT `getEndLine()` +currently reports the declaration line because the bridge keeps declaration +spans, not full body spans. +`ReflectionClass` construction accepts class-name strings and object arguments; +object arguments reflect the runtime class of eval-created or generated/AOT +objects. `ReflectionObject` construction accepts object arguments and exposes the +same class metadata through a `ReflectionObject` instance. Its inherited +`newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()` +helpers use the object's runtime class id. Straight-line `ReflectionObject` +receivers built from statically typed objects also normalize named constructor +arguments through the reflected constructor signature. Its inherited +`hasProperty()`, `getProperty()`, and `getProperties()` include public dynamic +properties from the reflected instance and mark those `ReflectionProperty` +objects as dynamic. +`ReflectionEnum` construction accepts enum-name strings for eval-declared +enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and +`getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, +`ReflectionEnumBackedCase`, and `ReflectionNamedType` objects where PHP does. +`ReflectionMethod` construction accepts class-name strings and object +arguments; object arguments resolve to the runtime class before method lookup. +It also accepts PHP's deprecated one-argument `ClassName::method` string form +for eval-visible and generated/AOT methods. +`ReflectionMethod::createFromMethodName()` accepts `ClassName::method` strings +for eval-visible and generated/AOT methods and returns retained method +metadata equivalent to direct `ReflectionMethod` construction. +`ReflectionClass::getShortName()`, +`ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` +derive namespace-aware parts from the resolved eval class-like name. +`ReflectionFunction::getShortName()`, `getNamespaceName()`, and +`inNamespace()` derive namespace-aware parts from the reflected eval function +name. `ReflectionMethod::getShortName()` reports the reflected method name, +while `ReflectionMethod::getNamespaceName()` reports an empty string and +`inNamespace()` reports `false`, matching PHP's method reflection behavior. +`ReflectionFunction` and `ReflectionMethod` report eval user-symbol defaults +through `isInternal()`, `isUserDefined()`, `isClosure()`, `returnsReference()`, +`isGenerator()`, `isVariadic()`, `isStatic()`, +`hasTentativeReturnType()`, and `getTentativeReturnType()`. `hasReturnType()` +and `getReturnType()` expose retained eval return type metadata for supported +named, nullable, union, and intersection declarations, including `void` and +`never` as builtin non-nullable named types. +`isDeprecated()` reflects PHP's global `#[Deprecated]` attribute on +eval-declared functions and methods, and reports `false` otherwise. +`ReflectionFunction::isAnonymous()` reports `false` for eval-declared named +functions, and `getClosureThis()`, `getClosureScopeClass()`, and +`getClosureCalledClass()` report `null` for eval-visible non-closure function +and method reflectors. +`ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. +`ReflectionFunction::getStaticVariables()` and +`ReflectionMethod::getStaticVariables()` expose eval-declared static local +variables, materializing initializer values before the first invocation and +returning updated values after reflected or direct calls. +`ReflectionFunction` over an eval closure literal reports `isClosure()` and +`isAnonymous()` as `true`, reports `isStatic()` from the literal's +`static function` marker, exposes captured `use` variables through +`getClosureUsedVariables()`, and can invoke the closure through `invoke()` or +`invokeArgs()`. +`ReflectionFunction::getClosureUsedVariables()` and +`ReflectionMethod::getClosureUsedVariables()` report empty arrays for supported +non-closure function and method reflectors. +`ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, +`ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and +`ReflectionClass::isEnum()` report eval and generated/AOT class-like metadata, +including PHP-compatible enum finality and class-like kind checks for eval +interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval and +generated/AOT `readonly class` metadata. `ReflectionClass::isAnonymous()` +reports true for eval anonymous classes and false for eval-declared named +class-like symbols. +`ReflectionClass::isInstantiable()` reports whether eval or generated/AOT +class-like metadata describes a concrete class with no constructor or a public +constructor. `ReflectionClass::isCloneable()` reports whether eval or +generated/AOT class metadata describes a concrete class with no `__clone()` or +a public `__clone()`. +`ReflectionClass::isIterable()` and `isIterateable()` report whether eval or +generated class metadata describes a concrete `Iterator` or `IteratorAggregate` +class. +`ReflectionClass::isInternal()` and `isUserDefined()` distinguish +compiler-injected class-like metadata from eval-declared or generated +user-defined class-like symbols. +`ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` +modifier bitmask for eval class-like metadata. +`ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval +and generated/AOT classes, plus parent interfaces for eval and generated/AOT +interfaces. `ReflectionClass::getInterfaces()` materializes those names as a +name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames()` +returns traits used directly by eval class-like symbols and generated/AOT +classes, traits, and enums. `ReflectionClass::getTraits()` materializes those +direct trait names as `ReflectionClass` objects, and +`ReflectionClass::getTraitAliases()` exposes direct eval class-like and +generated/AOT class/enum trait `as` aliases as PHP's alias-name to +`Trait::method` map. +`ReflectionClass::implementsInterface()` checks those eval relations +case-insensitively, returns true when reflecting the requested interface itself, +and checks generated/AOT class-interface relations through runtime metadata. It +throws catchable `ReflectionException` values when the argument names a class, +trait, enum, or missing interface. +`ReflectionClass::isSubclassOf()` checks eval parent-class chains and +implemented or extended interfaces case-insensitively. It excludes the reflected +symbol itself, returns `false` for trait and enum targets, and throws a +catchable `ReflectionException` when the target name is missing. +`ReflectionClass::isInstance()` checks eval-created or generated/AOT objects +against the reflected class-like metadata, including parent, interface, and enum +relations; trait targets return `false`. +`ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report +method and property membership for eval classes, interfaces, traits, and enums; +method lookup is case-insensitive, while property lookup is case-sensitive. +For generated/AOT classes, `ReflectionClass::hasMethod()` and `hasProperty()` +can also probe emitted method/property metadata without requiring the full +member lists to be materialized on the `ReflectionClass` object. +Eval-declared classes that extend generated/AOT parents expose inherited +bridge-supported public/protected parent members to `method_exists()`, +`property_exists()`, and `get_class_methods()` with PHP-compatible scope +filtering. +`ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, +`getDefaultProperties()`, `getStaticProperties()`, +`getStaticPropertyValue()`, `setStaticPropertyValue()`, +`getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible +class constants, interface constants, trait constants, enum constants, enum +cases, supported materialized property defaults, and current eval-declared +static property values. For generated/AOT class-like symbols, the constant APIs +also expose materializable scalar, string, null, `::class`, simple arithmetic or +concatenation, and enum-case constant metadata through runtime hooks, and the +static-property APIs expose bridge-supported public/protected/private generated/AOT static +property values. Constant +lookup is case-sensitive; single-value +lookups return `false` when no constant or case is visible. `getConstants()` +and `getReflectionConstants()` accept PHP's `ReflectionClassConstant::IS_*` +visibility/finality filter bitmask; `null` means no filter and `0` returns no +constants. +`ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return +materialized `ReflectionMethod` and `ReflectionProperty` objects for the same +visible member metadata, including supported member attributes and predicate +flags. For generated/AOT classes, `ReflectionClass::getMethod()` / +`getProperty()` and `getMethods()` / `getProperties()` materialize reflection +objects from emitted member-name and predicate metadata. Optional modifier +filters are supported for materialized `ReflectionClass` member lists; inline +or tracked receivers with known integer or `ReflectionMethod::IS_*` / +`ReflectionProperty::IS_*` constants can also be statically narrowed before +materialization. AOT method reflection also exposes registered parameter +names, declared parameter types, declared return types, required/optional +counts, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default values for generated +constructor, instance-method, and static-method signatures. AOT property +reflection exposes registered declared property types and supported scalar, +string, null, empty-array, or supported array-valued default values for generated property metadata, including +`ReflectionClass::getDefaultProperties()`. AOT method and +property/class-constant reflection expose generated member attributes when +their arguments fit the materializable literal subset. +`ReflectionClass`, `ReflectionObject`, and `ReflectionEnum` expose a compact +`__toString()` dump for eval-visible class-like metadata, including supported +constants, properties, and methods. +`ReflectionMethod::getDeclaringClass()` and +`ReflectionProperty::getDeclaringClass()` return a materialized +`ReflectionClass` for the symbol that declares the reflected +member. `ReflectionMethod::hasPrototype()` and `getPrototype()` expose +eval and generated/AOT parent-class overrides and interface implementation +prototypes, including static method prototypes; inherited methods that are not +overridden report no prototype, matching PHP reflection. +`ReflectionClass::getConstructor()` returns a materialized +`ReflectionMethod` for direct, inherited, interface, trait, and generated/AOT +constructors, including registered generated/AOT constructor parameter names, +counts, and supported defaults where available; it returns `null` when no +constructor is visible. `ReflectionClass::getParentClass()` +returns a materialized `ReflectionClass` for eval-declared and generated/AOT +parent classes or `false` when no parent class exists. +`ReflectionClass::newInstance()` constructs eval-declared and bridge-supported +generated/AOT reflected classes with public constructors and forwards +constructor arguments through eval's positional, named, and unpacking-aware call +binding. Non-public constructors fail like PHP reflection construction. +`ReflectionClass::newInstanceArgs()` constructs those reflected classes from an +indexed or string-keyed argument array, including arrays built at eval runtime, +and treats string keys as named constructor arguments. +`ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared and +generated/AOT reflected classes, initializes supported property defaults, skips +`__construct()`, and rejects reflected abstract classes, interfaces, traits, +and enums. +`ReflectionMethod::invoke()` and `invokeArgs()` call eval-declared reflected +methods, bypass public/protected/private visibility like PHP reflection, +preserve named arguments for the invoked method, follow PHP's by-value +`invoke()` variadic forwarding, accept `null` or an object for static methods, +and throw catchable `ReflectionException` values when an instance receiver is +not compatible with the reflected declaring class. For generated/AOT classes, +`ReflectionMethod::invoke()` and `invokeArgs()` are also lowered for inline or straight-line +tracked reflectors with declared or inferred/untyped parameter contracts; untyped +parameters are forwarded through the boxed `Mixed` ABI used by EIR. The lowered +call supports instance and static methods, constructors returned by +`ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, +and named arguments. Generated/AOT +bridge-supported invoke targets also bypass public/protected/private visibility +like PHP reflection. Inside eval fragments, `invokeArgs()` accepts literal or +runtime-built indexed/string-keyed argument arrays for those bridge-supported +generated/AOT targets; unsupported runtime-only reflector shapes outside that +signature slice still fail at runtime. +Eval-declared method parameter type hints are checked when the method is +entered. Supported checks include scalar hints with PHP-style weak scalar +coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and +eval/runtime class or interface names. +`ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, +`isFinal()`, `isAbstract()`, and `getModifiers()` report eval method metadata, +with PHP-compatible `ReflectionMethod::IS_*` constants for the bitmask. +`ReflectionMethod::isConstructor()` and `isDestructor()` report whether the +reflected method is `__construct` or `__destruct`. +`ReflectionMethod::setAccessible()` is accepted as a PHP-compatible no-op. +`ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, +`ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and +`getNumberOfRequiredParameters()` report retained eval-declared function and +method metadata, plus registered generated/AOT free-function, method, +static-method, and constructor parameter names, declared parameter and return +types, required/optional counts, by-reference and variadic flags, and scalar, +null, empty-array, supported array-valued, or supported object-valued default +values when native signatures are registered. Eval code can also reflect +supported callable-builtin signatures, including internal origin, parameter +names, parameter types, and return type metadata. +Eval-declared +functions and methods expose declared-type presence for parameters and return types, simple +named type metadata through +`ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, +`allowsNull()`, and `isBuiltin()`, and the legacy +`ReflectionParameter::isArray()` / `isCallable()` predicates for named `array` +and `callable` parameter types. `ReflectionParameter::getClass()` returns a +`ReflectionClass` object for retained nullable or non-nullable named object +parameter types and `null` for builtin, union, intersection, or untyped +parameters. Multi-member union metadata is exposed through +`ReflectionUnionType::getTypes()`, `allowsNull()`, and `__toString()`. Intersection parameter +metadata is exposed through `ReflectionIntersectionType::getTypes()` and +`allowsNull()` / `__toString()`. Named, nullable named, union, and intersection +`ReflectionType` objects stringify using the retained eval type metadata. +Function, method, and parameter attributes are exposed through +`getAttributes()` using materialized `ReflectionAttribute` objects. Parameter +default values, optionality, nullability, variadic flags, and by-reference +flags are retained for eval-declared functions and methods, including +`ReflectionParameter::allowsNull()` and `ReflectionParameter::__toString()`. +`ReflectionParameter::getDeclaringClass()` +returns the declaring class-like symbol for eval method parameters, and +`ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` +object for eval free-function parameters or a `ReflectionMethod` object for the +declaring eval method. Direct `new ReflectionParameter(...)` construction +accepts eval and registered generated/AOT free-function names, eval-visible and +generated/AOT class/interface/trait method arrays, and object-method arrays +resolved from the evaluated runtime object, including inline `new` expressions. +`ReflectionFunction::invoke()` and `invokeArgs()` +dispatch eval-declared functions with the same named/default/variadic argument +binding used by direct eval function calls. Runtime-held generated/AOT +`ReflectionFunction` objects can invoke registered generated functions through +the native bridge with parameter names, supported defaults, named arguments, and +indexed or string-keyed runtime argument arrays. Direct eval calls, variable +function calls, and `call_user_func()` paths can also invoke registered +generated/AOT free functions with positional variadic tails when the generated +signature has no by-reference parameters. Direct, variable-function, +first-class callable, `Closure::fromCallable()`, and `call_user_func_array()` +paths can additionally invoke generated/AOT free functions whose by-reference +parameters use boxed Mixed/union storage or one-word scalar raw storage +(`int`, `bool`, or `float`), string raw storage, or one-word heap raw storage +(`array`, `iterable`, and object/class parameters), including positional +`&...` variadic tails when the tail arguments have lvalue/ref-cell storage. +Registered generated/AOT free-function parameter +names, declared types, return types, by-reference and variadic flags, +required/optional counts, and supported defaults are also exposed through +`ReflectionFunction` / `ReflectionParameter` metadata. Unsupported +generated/AOT free-function bridge shapes, such as nullable tagged-scalar, +native-only ABI layouts, and other unsupported raw-storage by-reference +parameters, remain reflectable as metadata but are not invocable through eval. +Supported callable-builtin invocation is +covered by the general Reflection support documented in +`docs/php/classes.md`. +Defaulted eval method parameters are +bound when omitted and reported through `ReflectionParameter::isOptional()`, +`isDefaultValueAvailable()`, `isDefaultValueConstant()`, +`getDefaultValueConstantName()`, and `getDefaultValue()`. Constant-name metadata +is retained for predefined or eval-defined constant fetches, namespaced constant +fallback, and class/interface/trait/enum constant fetches; `::class` literals, +magic constants, and literal defaults are materialized as values but are not +reported as default-value constants. Supported default expressions include +scalar literals, arrays whose keys and values are supported default +expressions, magic constants, unary and binary operators supported by eval, +ternary and null-coalescing expressions, predefined or eval-defined constant +fetches, namespaced constant fallback, class/interface/trait/enum constant +fetches, `self::class` / `parent::class` / named class-like `::class` literals, +and `new ClassName(...)` / `new self(...)` / `new parent(...)` with supported +non-spread constructor arguments. Magic constants in reflected parameter +defaults are evaluated in the declaring function or method scope; methods +imported from traits retain PHP's trait-origin `__TRAIT__`, `__METHOD__`, and +`__FUNCTION__` values while `__CLASS__` follows the using class. Late-bound +`static::` defaults and unpacked +constructor arguments in defaults are rejected like PHP constant expressions. +Variadic eval method parameters bind extra positional and unknown named +arguments into a PHP array and are reported through +`ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. +Constructor-promoted eval parameters are reported through +`ReflectionParameter::isPromoted()`. By-reference eval method parameters accept +direct variable, array-element, object-property including dynamic property +names, object-property array-element, static-property, and static-property +array-element arguments including dynamic receivers and dynamic property names, +write back fixed parameters after method execution, write back mutated +`&...$items` elements when the variadic container itself is not rebound, and are +reported through +`ReflectionParameter::isPassedByReference()` and +`ReflectionParameter::canBePassedByValue()`. +`ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, +`isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isVirtual()`, +`isDynamic()`, `isProtectedSet()`, `isPrivateSet()`, `isInitialized()`, +`isDefault()`, and `getModifiers()` report eval property +metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the +bitmask. `isPromoted()` reports generated/AOT and eval-declared +promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from +the retained modifier bitmask, including eval-declared class, abstract-property, +interface-property, and generated/AOT asymmetric visibility plus public readonly +property metadata. `isDynamic()` reports `false` for supported +declared properties and `true` for public dynamic object properties +materialized with `new ReflectionProperty($object, $property_name)`. +`ReflectionProperty::isDefault()` is the inverse for those supported dynamic +properties. `isInitialized()` tracks eval-backed and bridge-supported +generated/AOT instance and static property storage, including typed properties +without defaults, unset eval properties, virtual property hooks, and public +dynamic properties on the inspected object. +`ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose +retained property type metadata through `ReflectionNamedType`, +`ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained +a supported declared type. `getSettableType()` follows an explicit property +`set(Type $value)` hook parameter when one is retained; otherwise it falls back +to the declared property type. +`ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose +materialized property default metadata, including PHP's implicit `null` default +for untyped concrete properties without an explicit initializer. +`ReflectionProperty::__toString()` formats retained eval/generated property +metadata as a PHP-style `Property [ ... ]` descriptor for the supported +visibility, static, type, default, and virtual-property surface. +`ReflectionProperty::hasHooks()`, `hasHook()`, `getHooks()`, and `getHook()` +expose eval-declared concrete, abstract, and interface property get/set hook +metadata plus registered generated/AOT interface property-hook metadata, and +return hook `ReflectionMethod` objects using PHP's `$property::get` / +`$property::set` names. Eval also exposes +`PropertyHookType::Get` and `PropertyHookType::Set` inside evaluated fragments +for those APIs, including `PropertyHookType::cases()`, `from()`, and +`tryFrom()`. +`ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. +`ReflectionProperty::getValue()` and `setValue()` read and write eval-declared +and bridge-supported generated/AOT instance and static property values, bypass +public/protected/private visibility like PHP reflection, route concrete eval +property hooks through their accessors, and still reject readonly writes. +`ReflectionProperty::getRawValue()` and `setRawValue()` are supported for +eval-declared backed instance properties, including backed property hooks, and +for bridge-supported generated/AOT instance properties. Raw access bypasses +concrete eval property hook accessors. Virtual property hooks reject raw +access like PHP. `ReflectionProperty::isLazy()` reports `false` for +eval-declared and bridge-supported generated/AOT properties because eval does +not implement lazy properties; +`skipLazyInitialization()` is a no-op for supported non-static backed +properties, and `setRawValueWithoutLazyInitialization()` follows the same raw +storage write path as `setRawValue()`. +`ReflectionClassConstant::getAttributes()`, +`ReflectionEnumUnitCase::getAttributes()`, and +`ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant +and enum-case attributes through the same materialized `ReflectionAttribute` +shape; their `getName()` methods return the reflected constant or case name, +`ReflectionClassConstant::getValue()` returns the class-constant value or enum +case object, while `ReflectionEnumUnitCase::getValue()` and +`ReflectionEnumBackedCase::getValue()` return the reflected enum-case object. +`ReflectionEnumBackedCase::getBackingValue()` returns the scalar backing value, +`getDeclaringClass()` returns the declaring class or enum as a +`ReflectionClass`, and `getEnum()` returns the declaring enum as a +`ReflectionEnum`. `ReflectionClassConstant::isEnumCase()` reports enum cases. +`ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `isDeprecated()`, `hasType()`, and +`getType()` with PHP's current untyped defaults: `false`, `false`, and `null`. +Their `__toString()` methods format retained constant and enum-case metadata in +PHP's `Constant [ ... ] { ... }` shape. +`ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `isEnumCase()`, `isPublic()`, +`isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()` with PHP's +`ReflectionClassConstant::IS_*` bitmasks. Enum cases report public, +non-final constant metadata, and the enum-case reflection classes expose the +same inherited `IS_*` constants as PHP. +Concrete property hooks are lowered to eval accessor methods; reads and writes +route through inherited hooks, while access from the accessor itself uses the +raw backing slot. Both `get => expr;` and `set => expr;` short forms are +supported; short set hooks store the expression result in the raw backing slot. +`readonly` eval properties require a declared type, may be +assigned from the constructor of the declaring class, and later writes fail as +eval runtime fatals. A `readonly class` makes declared instance properties +readonly implicitly, while declared static properties remain mutable and are not +converted to readonly slots. Untyped instance properties in readonly classes are +still rejected. +Missing-property writes can still dispatch through `__set()`, but readonly +classes reject actual dynamic property creation. +PHP's global `#[AllowDynamicProperties]` marker is accepted only on +non-readonly eval-declared classes; eval rejects it on readonly classes, +interfaces, traits, enums, members, and enum cases. +`self::`, `parent::`, and late-bound `static::` work for supported static +members, class constants, and class-name literals. +Attempts to `unset()` static properties parse normally and throw PHP's +catchable `Error`, including runtime-valued static receivers and dynamic +static property names. + +Eval object construction can allocate eval-declared classes, `stdClass`, and +emitted AOT classes visible through runtime class metadata. Missing class names +during eval object construction fail with an eval runtime fatal diagnostic. +`clone $object` creates a shallow copy for eval-declared objects, `stdClass` +objects, and ordinary emitted/AOT objects. Eval `__clone()` hooks are invoked on +the cloned object after storage copying and use the same runtime visibility +checks as method calls. Emitted/AOT `__clone()` hooks, including non-public hooks +visible from the current eval class scope, are invoked through the generated +method bridge after the clone storage has been copied. + +AOT and eval-declared class-name probes are visible through `class_exists()`. +Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use +generated AOT class/interface metadata and eval-created object metadata. +`interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated +AOT metadata. `class_implements()` and `class_parents()` materialize +generated/AOT interface and parent metadata when the bridge exposes it. +`class_uses()` reports direct trait uses for eval-declared and generated/AOT +classes, traits, and enums. `class_alias()` can +alias eval-declared and generated/AOT +classes, interfaces, traits, and enums, preserving the target class-like kind +for the corresponding metadata probes. Top-level compiled `class_alias()` +calls still use elephc's generated subclass model, so eval sees the same AOT +parent metadata as the compiled program. `get_declared_classes()`, +`get_declared_interfaces()`, and `get_declared_traits()` expose eval-declared +names plus generated/AOT declaration names; enum names are included in the class +list like PHP. Aliases are usable for class-like lookups but are not added to +the declared-name lists. Eval-declared enums are visible inside eval through +`enum_exists()` and through class-like probes such as `class_exists()`. +`method_exists()` and `property_exists()` inspect eval-declared class/interface/ +trait/enum metadata and generated runtime metadata. Object targets also see +dynamic public properties, and `property_exists()` follows PHP's current-scope +visibility for inherited private instance properties on object targets while +leaving class-string targets hidden. `get_class_methods()`, `get_class_vars()`, +and `get_object_vars()` follow PHP visibility from the current eval class scope +for eval-declared metadata and objects, and use generated/AOT runtime metadata +when available through the bridge-visible hook slice. + +Eval-declared enums share the dynamic class-like metadata path used by +eval-declared classes. Pure and backed enum cases are singleton objects, +`EnumName::cases()` returns those singletons in declaration order, and backed +`EnumName::from()` / `EnumName::tryFrom()` compare against the declared scalar +values. `EnumName::from()` misses throw a catchable `ValueError`, while +`EnumName::tryFrom()` misses return `null`. Enums can implement eval-declared +or generated interfaces and can use their own instance/static methods and class +constants. Eval rejects enum declarations that redeclare reserved enum methods +or PHP-forbidden enum magic methods. Direct `new EnumName()` and property writes +to enum cases are rejected. + +Public declared property reads/writes through `$this->property` from native +methods are bridged to eval. Protected and private declared property +reads/writes are bridged when the eval fragment runs inside a native class scope +that satisfies PHP visibility for the declaring class. +Public declared static property reads/writes are bridged to eval. Protected and +private declared static property reads/writes are bridged from native class +scopes that satisfy PHP visibility for the declaring class. +Public/protected/private fixed scalar/nullable scalar/Mixed/array/iterable/object +method parameters through `$this->method(...)` and `Class::method(...)` are +supported by the native method bridge when the eval fragment runs inside a +native class scope that satisfies PHP visibility for the declaring class, +including registered named arguments and string-keyed unpacking; method returns +may be scalar, nullable, callable, Mixed, array, iterable, object, union, and +intersection-object values. + +## Namespaces and constants + +Eval namespace declarations qualify function declarations, class declarations, +object construction names, and qualified references against the active +namespace. Unqualified function and constant references fall back to the global +builtin/constant namespace when the namespaced symbol is absent. + +Simple and grouped `use`, `use function`, and `use const` declarations are +resolved while the bridge parser builds EvalIR: class imports rewrite `new` +targets, function imports rewrite unqualified calls, and constant imports +rewrite unqualified constant fetches in the active namespace declaration +region. + +Inside eval, `define()` stores dynamic constants that persist across later eval +fragments, `defined()` probes them, and bare constant expressions fetch their +retained boxed values. Native `defined("Name")`, bare constant fetches, and +string-literal `class_exists("Name")` calls after an eval barrier also probe +eval-created dynamic symbols. Duplicate eval `define()` calls keep the first +value, return `false`, and emit the same suppressible duplicate-constant warning +as AOT `define()`. + +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, +`PHP_INT_MAX`, `INF`, `NAN`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, +`COUNT_*`, and the supported `PREG_*` / `JSON_*` constants. `defined()` sees +these names, including an optional leading `\`, and `define()` cannot replace +them. + +## Builtins available through eval + +Eval builtin dispatch supports direct calls, named arguments, callable +dispatch, `call_user_func()`, `call_user_func_array()`, and `function_exists()` +where listed below unless a note says otherwise. + +| Area | Builtins | +|---|---| +| System, time, and environment | `time()`, `microtime()`, `hrtime()`, `date()`, `gmdate()`, `mktime()`, `gmmktime()`, `checkdate()`, `getdate()`, `localtime()`, `strtotime()`, `date_default_timezone_get()`, `date_default_timezone_set()`, `http_response_code()`, `header()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | +| Process execution | `exec()`, `shell_exec()`, `system()`, `passthru()`, `popen()`, `pclose()`, `readline()`, `die()`, `exit()` | +| Filesystem and paths | `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `chgrp()`, `chown()`, `lchgrp()`, `lchown()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `tmpfile()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()` | +| File and directory streams | `fopen()`, `fclose()`, `feof()`, `fflush()`, `fgetc()`, `fgets()`, `fgetcsv()`, `fpassthru()`, `fprintf()`, `fputcsv()`, `fread()`, `fscanf()`, `flock()`, `fseek()`, `fstat()`, `fsync()`, `fdatasync()`, `ftell()`, `ftruncate()`, `fwrite()`, `rewind()`, `vfprintf()`, `opendir()`, `readdir()`, `closedir()`, `rewinddir()` | +| Streams and stream contexts | `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `stream_isatty()`, `stream_is_local()`, `stream_supports_lock()`, `stream_get_contents()`, `stream_get_line()`, `stream_get_meta_data()`, `stream_copy_to_stream()`, `stream_resolve_include_path()`, `stream_select()`, `stream_set_blocking()`, `stream_set_chunk_size()`, `stream_set_read_buffer()`, `stream_set_timeout()`, `stream_set_write_buffer()`, `stream_context_create()`, `stream_context_get_default()`, `stream_context_get_options()`, `stream_context_get_params()`, `stream_context_set_default()`, `stream_context_set_option()`, `stream_context_set_params()`, `stream_wrapper_register()`, `stream_wrapper_unregister()`, `stream_wrapper_restore()`, `stream_filter_register()`, `stream_filter_append()`, `stream_filter_prepend()`, `stream_filter_remove()`, `stream_bucket_new()`, `stream_bucket_make_writeable()`, `stream_bucket_append()`, `stream_bucket_prepend()` | +| Stream sockets and network databases | `stream_socket_server()`, `stream_socket_client()`, `stream_socket_accept()`, `stream_socket_enable_crypto()`, `stream_socket_get_name()`, `stream_socket_pair()`, `stream_socket_recvfrom()`, `stream_socket_sendto()`, `stream_socket_shutdown()`, `fsockopen()`, `pfsockopen()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()` | +| Strings, bytes, and formatting | `strlen()`, `ord()`, `chr()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `ucwords()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `strrev()`, `grapheme_strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `gzcompress()`, `gzdeflate()`, `gzinflate()`, `gzuncompress()`, `number_format()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()` | +| Hashing | `crc32()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `hash_init()`, `hash_update()`, `hash_final()`, `hash_copy()` | +| JSON | `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()` | +| Regex | `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `mb_ereg_match()` | +| Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | +| Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()`, `spl_autoload()`, `spl_autoload_call()`, `spl_autoload_extensions()`, `spl_autoload_functions()`, `spl_autoload_register()`, `spl_autoload_unregister()` | +| Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | +| Raw memory and buffers | `buffer_new()`, `buffer_len()`, `buffer_free()`, `ptr()`, `ptr_null()`, `ptr_is_null()`, `ptr_offset()`, `ptr_get()`, `ptr_set()`, `ptr_read8()`, `ptr_read16()`, `ptr_read32()`, `ptr_read_string()`, `ptr_write8()`, `ptr_write16()`, `ptr_write32()`, `ptr_write_string()`, `ptr_sizeof()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `empty()`, `isset()`, `unset()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_scalar()`, `is_resource()` | +| Debug output | `print_r()`, `var_dump()` | +| Constants | `define()`, `defined()` | + +## Builtin notes + +Eval `settype()` mutates direct variables, array elements, object properties +including dynamic property names, and static properties including dynamic +receivers or dynamic property names. Callable dispatch of `settype()` remains +by-value and emits PHP's by-reference warning. + +Eval supports the deprecated no-argument `get_class()` and `get_parent_class()` +forms inside class methods. They read the declaring method's class scope rather +than the late-static called class. Outside a class scope, no-argument +`get_class()` throws `Error`; no-argument `get_parent_class()` returns an empty +string, matching elephc's AOT lowering for parentless or scope-less lookups. + +Eval `array_map()` supports one or more source arrays with supported callable +values or a `null` callback. `array_filter()`, `array_reduce()`, +`array_walk()`, `usort()`, `uasort()`, and `uksort()` share the same callback +dispatcher, including first-class function, method, and invokable-object callback values. One-array +`array_map()` results preserve source keys, multi-array results are reindexed, +missing source values are padded with `null`, and `array_map(null, ...)` +returns zipped row arrays. + +Eval `count()` supports normal and recursive array counting and dispatches +top-level eval-declared or generated/AOT objects implementing `Countable` +through their `count()` method. `COUNT_RECURSIVE` still validates as a mode for +`Countable` objects, but the method result is used directly like PHP. + +Eval `array_filter()` supports the PHP default omitted/null callback form, +filters falsey values, preserves source keys, and supports +`ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and +`ARRAY_FILTER_USE_KEY`. + +Eval mutating array builtins such as `array_pop()`, `array_shift()`, +`array_push()`, `array_unshift()`, `array_splice()`, `sort()`, `rsort()`, +`asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, `natcasesort()`, +`shuffle()`, `usort()`, `uksort()`, and `uasort()` write back through direct +variable calls. When reached through dynamic callable dispatch, they follow +PHP's by-value callback behavior: the return value is computed from the +supplied array, a by-reference warning is emitted where PHP would emit one, and +the caller's original array is not mutated. + +Eval regex dispatch uses PCRE2 through the POSIX wrapper for common PCRE-style +delimited patterns. It strips PHP delimiters, supports the `i`, `m`, `s`, `u`, +and `U` modifiers, supports common capture array shapes and replacement +references, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and +`PREG_SPLIT_OFFSET_CAPTURE`. Patterns, delimiters, modifiers, or subject bytes +that the eval bridge cannot pass through this wrapper fail as eval runtime +fatals. Native non-eval regex codegen remains PCRE2-backed as documented in +[Regex](regex.md). + +Eval JSON support covers null, booleans, integers, floats, strings, indexed +arrays, associative arrays, and `stdClass` dynamic properties. `json_encode()` +supports zero flags plus the documented `JSON_HEX_*`, +`JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, +`JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, +`JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, +`JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` flags. `json_decode()` +and `json_validate()` support PHP-compatible depth handling, malformed UTF-8 +ignore/substitute modes where applicable, `JSON_BIGINT_AS_STRING` for +overflowing integer tokens in `json_decode()`, and `JsonException` through +`JSON_THROW_ON_ERROR`. + +Eval local filesystem calls operate on host filesystem paths and support the +implemented stream-wrapper paths, including `file://`, `php://memory`, +`data://`, `phar://`, plain `http://`, and eval-registered userspace wrappers. +Eval also supports complete `fstat()` arrays and portable ownership/group +metadata calls where the host platform exposes them. TLS-backed `https://` +URLs remain outside magician's implemented wrapper paths. + +Eval `print_r()` supports the normal echoing form and `print_r($value, true)`. +Scalars print through the same output path as `echo`, boolean false and null +print nothing, arrays use PHP's recursive `Array\n(\n [key] => value\n)\n` +shape, and objects render class names plus bridge-visible properties. + +Eval `var_dump()` supports one or more arguments. Scalars print typed +diagnostic lines, indexed or associative arrays print foreach-visible keys and +nested values through eval value hooks, and objects print class names, object +ids, bridge-visible properties, eval-declared private/protected/public property +labels, and eval property references when alias metadata is available. + +## Current limitations + +Dynamic fragments and literal fragments outside the current AOT eligibility +rules execute through the `elephc_magician` interpreter bridge. Eligible +literal fragments instead use the normal AST -> EIR -> native codegen pipeline, +either with direct caller locals or with core eval-scope helpers. Unsupported +constructs that reach the interpreter, and missing class names during eval +object construction, fail at runtime with an eval fatal diagnostic. + +The fragment subset is broad but not the full elephc language surface. Eval +retains `ReflectionFunction` closure metadata for common +`Closure::fromCallable()` targets, including bound free functions, object +methods, static methods, and invokable objects. Advanced native callable +descriptors and full PHP `Closure` APIs, such as arbitrary scope mutation and +reflection over every first-class callable shape, are still outside eval +fragments. Runtime/AOT free-function, object-method, static-method, and +constructor fallback from eval +can bind registered names, defaults, and positional variadic tails; method, +static-method, and constructor fallback can also bind by-reference lvalue +metadata. Generated AOT bridge dispatch supports visibility-checked method, +static-method, and constructor signatures whose generated ABI storage is +scalar/string, callable descriptor, boxed Mixed/union, array/hash, iterable, or +object, plus free-function signatures using the descriptor invoker ABI when +by-reference parameters, if any, use boxed Mixed/union storage or raw one-word +scalar storage (`int`, `bool`, or `float`), string raw storage, or one-word heap +storage (`array`, `iterable`, and object/class parameters), including +positional `&...` variadic tail element writeback. +Registered type specs validate nullable and union members, intersection object +parameters, and intersection object returns before or after the generated AOT +call. By-reference method and constructor parameters remain limited to the +staged scalar/nullable scalar/Mixed/array/iterable/object slice, including the +generated positional variadic array slot when present. These supported method +and constructor shapes use normal target ABI materialization for arguments +beyond the register set instead of a fixed eval arity cap. Layouts such as +pointer, buffer, packed, resource, and other specialized native-only ABI shapes +remain outside those bridge paths. + +Eval class support is still smaller than the full static class system. +Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches +final release through ordinary eval statement execution, such as `unset($obj)` or +a discarded temporary expression, and through the native runtime final-release +path after an eval-owned object escapes back into compiled code, including +runtime cycle collection of eval-owned object graphs. The main remaining +class-system gaps are +broader reflection APIs beyond the supported +ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType +and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated +property, and function/method return metadata, generated property default-value +materialization beyond scalar, null, empty-array, and supported array-valued +defaults, parameter defaults beyond representable scalar/string constant +expressions, resolved global/named class-like constants, supported array-valued +defaults, and supported object-valued defaults during generated/AOT invocation, +and generated/AOT method and constructor +bridge signatures beyond the current visibility-checked by-reference parameter +slice, specialized native-only ABI shapes, and `__clone()` hooks; the remaining +limit there is the supported type slice rather than a fixed parameter count. +Generated/AOT free-function and method type metadata, return metadata, +by-reference and variadic parameter flags, and generated/AOT +class/method/property/class-constant attributes are exposed for registered +metadata slices, while unsupported native-only bridge shapes and raw-storage +by-reference free-function bridge shapes beyond the current +Mixed/scalar/string/one-word heap slice remain metadata-only rather than +invocable through eval. + +The type checker and AST optimizer conservatively treat every `eval()` call as +a dynamic barrier: local facts are widened, constant propagation is invalidated, +and pre-call facts cannot be blindly reused afterward. Later EIR planning may +prove that a literal fragment can run natively and omit the runtime barrier; +values that actually cross a dynamic barrier use boxed `Mixed` storage. diff --git a/docs/php/fibers.md b/docs/php/fibers.md index 039d1eb1b1..accd17c503 100644 --- a/docs/php/fibers.md +++ b/docs/php/fibers.md @@ -2,7 +2,7 @@ title: "Fibers" description: "Cooperative coroutines (PHP 8.1+ Fiber): create, start, suspend, resume, with FiberError." sidebar: - order: 15 + order: 16 --- A `Fiber` is a cooperative coroutine: a callable that owns its own call stack and can suspend execution at arbitrary depth. Control alternates explicitly between the caller and the fiber — there is no preemption. diff --git a/docs/php/generators.md b/docs/php/generators.md index 176c53a0f3..7b03376d89 100644 --- a/docs/php/generators.md +++ b/docs/php/generators.md @@ -2,7 +2,7 @@ title: "Generators" description: "Generator functions with yield, the built-in Iterator and Generator types, foreach over Iterator objects, and Generator::send for coroutine-style flow." sidebar: - order: 16 + order: 17 --- A *generator* is a function whose body uses the `yield` keyword. Calling a diff --git a/docs/php/magic-constants.md b/docs/php/magic-constants.md index e6059a201e..a69525cbb3 100644 --- a/docs/php/magic-constants.md +++ b/docs/php/magic-constants.md @@ -2,7 +2,7 @@ title: "Magic Constants" description: "PHP magic constants (__DIR__, __FILE__, __LINE__, __FUNCTION__, __CLASS__, __METHOD__, __NAMESPACE__, __TRAIT__) resolved at compile time to plain string or integer literals." sidebar: - order: 14 + order: 15 --- PHP defines a set of *magic constants* that change value depending on where they appear in the source. elephc supports all eight, lowering each to a plain string or integer literal at compile time. They behave identically to PHP for every common case. diff --git a/docs/php/math.md b/docs/php/math.md index cd627afeb8..1891412f6b 100644 --- a/docs/php/math.md +++ b/docs/php/math.md @@ -2,7 +2,7 @@ title: "Math" description: "Mathematical functions: abs, floor, ceil, round, clamp, trigonometry, logarithms, and more." sidebar: - order: 8 + order: 9 --- ## Built-in math functions diff --git a/docs/php/namespaces.md b/docs/php/namespaces.md index bb78173d85..a4d2db3bd1 100644 --- a/docs/php/namespaces.md +++ b/docs/php/namespaces.md @@ -2,7 +2,7 @@ title: "Namespaces" description: "Namespace declarations, use imports, name resolution, include/require." sidebar: - order: 11 + order: 12 --- ## Declaring a namespace diff --git a/docs/php/pdo.md b/docs/php/pdo.md index cf290c7520..8c06df2d66 100644 --- a/docs/php/pdo.md +++ b/docs/php/pdo.md @@ -2,7 +2,7 @@ title: "PDO (Databases)" description: "PDO database access with the SQLite, PostgreSQL, and MySQL/MariaDB drivers: connections, prepared statements, fetch modes, and transactions." sidebar: - order: 17 + order: 18 --- elephc supports a practical subset of PHP's PDO database layer, with the diff --git a/docs/php/regex.md b/docs/php/regex.md index f3940cb341..e3e4fb215c 100644 --- a/docs/php/regex.md +++ b/docs/php/regex.md @@ -2,7 +2,7 @@ title: "Regex" description: "PCRE2-backed regular expressions, preg_* functions, SPL regex iterators, and native library requirements." sidebar: - order: 6 + order: 7 --- elephc implements PHP regular expressions with PCRE2 through PCRE2's diff --git a/docs/php/spl.md b/docs/php/spl.md index 052c62494e..2b4ce11360 100644 --- a/docs/php/spl.md +++ b/docs/php/spl.md @@ -2,7 +2,7 @@ title: "SPL" description: "Standard PHP Library interfaces, exceptions, and runtime-backed container classes." sidebar: - order: 10 + order: 11 --- elephc ships the SPL pieces that are needed by supported PHP code today: @@ -575,6 +575,10 @@ SPL autoload and class-introspection helpers are documented in `spl_object_id()`, `spl_object_hash()`, `class_implements()`, `class_parents()`, and `class_uses()`. +Inside `eval()`, `spl_classes()` is available through direct calls, +`call_user_func()`, `call_user_func_array()`, and `function_exists()` and +returns the same static registry snapshot as native code. + ## Iterator Helper Functions The iterator helper functions cover the PHP SPL traversal helpers: diff --git a/docs/php/streams.md b/docs/php/streams.md index 260b899f26..40eb9cd356 100644 --- a/docs/php/streams.md +++ b/docs/php/streams.md @@ -2,7 +2,7 @@ title: "Streams" description: "Stream resources, wrappers, contexts, filters, sockets, TLS, and process pipes." sidebar: - order: 13 + order: 14 --- ## Resource model diff --git a/docs/php/strings.md b/docs/php/strings.md index 251e739f51..c13a4c1344 100644 --- a/docs/php/strings.md +++ b/docs/php/strings.md @@ -2,7 +2,7 @@ title: "Strings" description: "String types, escape sequences, interpolation, heredoc/nowdoc, and built-in string functions." sidebar: - order: 5 + order: 6 --- ## Double-quoted strings diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b8b4a87d7b..5c8744ce97 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -2,7 +2,7 @@ title: "System & I/O" description: "System functions, date/time, JSON, filesystem utilities, process execution, and debugging utilities." sidebar: - order: 12 + order: 13 --- ## System functions diff --git a/docs/php/types.md b/docs/php/types.md index 5d8074a72f..f80021fe1f 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -20,7 +20,7 @@ sidebar: | `iterable` | Yes | PHP pseudo-type for `array \| Traversable`. Supports indexed arrays, associative arrays, `Iterator`, and `IteratorAggregate`; runtime operations (`foreach`, `echo`, `gettype()`, `var_dump()`, `===`, casts, `is_iterable()`) dispatch on heap-kind, value-type, or interface metadata as needed. | | `resource` | Inferred only | File handles and standard streams are modeled separately from integers. `fopen()` returns `resource\|false`, and `STDIN`, `STDOUT`, and `STDERR` are stream resources. PHP does not allow `resource` as a type declaration, so elephc does not accept `resource` annotations. | | `callable` | Yes | Closures, arrow functions, first-class callables, and FFI callback parameters. | -| `object` | Yes | Class instances. Heap-allocated, fixed-layout. `new ClassName(...)` | +| `object` | Yes | Class instances. Heap-allocated, fixed-layout. `new ClassName(...)`; the generic `object` type hint accepts any class, interface, or enum object. | | `enum` | Yes | Pure and backed enums. Cases are singletons. Backed enums support `->value`, `::from()`, `::tryFrom()`, `::cases()`. | | `int|string` | Yes | Union type — variable accepts any of the listed types. Lowered to Mixed at runtime. | | `?int` | Yes | Nullable shorthand — sugar for `int|null`. The explicit `T|null` form (e.g. `A|null`) is also accepted. | @@ -240,7 +240,11 @@ Narrowing applies to function and method parameters. A parameter whose call site - Plain array numeric casts (`(int)$array`, `(float)$array`) follow elephc's existing array cast semantics (return the element count rather than PHP's `0`/`1`). Direct `iterable` numeric casts use PHP's empty/non-empty `0`/`1` semantics. - `__destruct` runs when an object's refcount reaches zero (scope exit, reassignment, `unset`, program end), matching PHP's timing, but **object resurrection is not supported**: re-storing `$this` so the object would outlive the destructor does not keep it alive — the object is still freed once `__destruct` returns. - Under the compatibility `--null-repr=sentinel` opt-out, the integer `9223372036854775806` (`PHP_INT_MAX - 1`) collides with elephc's internal null marker in unboxed scalar slots and is misread as `null` by `echo`, `var_dump()`, `is_null()`, `??`, and related null checks. The default tagged null representation does not have this collision: the full 64-bit integer range round-trips. -- Variable variables (`$$name`, `${$expr}`) are not supported. elephc allocates each local to a fixed compile-time stack slot and keeps no per-frame variable-name table, so a variable whose name is computed at runtime cannot be resolved. Use an array keyed by the dynamic name instead. +- Variable variables (`$$name`, `${$expr}`) are not supported yet. Native AOT + locals use fixed compile-time stack slots; supporting a runtime-computed name + will require routing the access through Magician's materialized named scope + and synchronizing the affected native locals. Use an array keyed by the + dynamic name in portable elephc code for now. - Reference aliases to array elements (`$b =& $a[0]`) are limited to **indexed arrays with integer indices**; associative arrays (`$b =& $a['key']`) are rejected at compile time. Referencing an out-of-range index binds the alias to a null cell instead of creating the element (PHP autovivifies it as `null`), and the alias points into the array's storage, so it is only valid while the array is alive and not reallocated by growth. - `print_r($value, true)` captures into a fixed 64 KiB buffer: rendered output longer than 65536 bytes is truncated at the cap (PHP returns the full string). Echo mode (`print_r($value)`) is unaffected. - `serialize()`/`unserialize()` cover scalars, arrays, and objects (including the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods and `r:`/`R:` object back-references) byte-for-byte compatibly with PHP. Remaining gaps: a cyclic reference inside an object's own properties resolves to `null` on `unserialize()` (serialization handles cycles), the deprecated `Serializable` interface (`C:` wire form) is unsupported, writing a property of an unserialized object held in a `Mixed` does not persist (a separate `Mixed` property-write limitation), and `unserialize()` does not emit PHP's `E_WARNING` / `E_NOTICE` on malformed input — it just returns `false`. diff --git a/examples/eval-globals/.gitignore b/examples/eval-globals/.gitignore new file mode 100644 index 0000000000..fc9576aeb9 --- /dev/null +++ b/examples/eval-globals/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main diff --git a/examples/eval-globals/main.php b/examples/eval-globals/main.php new file mode 100644 index 0000000000..7e234ded12 --- /dev/null +++ b/examples/eval-globals/main.php @@ -0,0 +1,15 @@ + 0 ? "argc" : "no-argc") . ":" . (count($argv) > 0 ? "argv" : "no-argv");'); +} + +class EvalCounter { + public int $value = 1; + + public function bump(): void { + eval('$this->value = $this->value + 1;'); + } + + public function read(): int { + return $this->value; + } + + public function add(int $amount): int { + return $this->value + $amount; + } + + public function label(int $amount, string $suffix): string { + return ($this->value + $amount) . $suffix; + } + + public function echoReadThroughEval(): void { + echo "eval-this-method=" . eval('return $this->read();') . "\n"; + } + + public function echoAddThroughEval(): void { + echo "eval-this-method-arg=" . eval('return $this->add(5);') . "\n"; + } + + public function echoLabelThroughEval(): void { + echo "eval-this-method-two-args=" . eval('return $this->label(5, "!");') . "\n"; + } + + public function echoLabelSpreadThroughEval(): void { + echo "eval-this-method-spread=" . eval('return $this->label(...[5, "?"]);') . "\n"; + } +} + +class EvalAotBox { + public int $value = 0; + + public function __construct(int $value) { + $this->value = $value; + } +} + +$x = 1; +$profile = ["name" => "Ada"]; +$result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); +eval('$profile["name"] = "Grace";'); +eval('if ($x >= 3) { echo "x>=3\n"; }'); +eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); +eval('if ("10" !== 10) { echo "strict-ok\n"; }'); +$ternary = eval('return $x >= 3 ? "ternary-yes" : "ternary-no";'); +eval('do { echo "do-once\n"; } while (false);'); +eval('if (true) echo "single-if\n";'); +eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); +eval('foreach (["a" => 1, "b" => 2] as $key => $value) { echo "pair=" . $key . ":" . $value . "\n"; }'); +eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); +eval('echo "echo-list=", "ok\n";'); +eval('print_r("print-r=ok\n");'); +eval('var_dump("var-dump-ok");'); +eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); +eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); +$meta = eval('return ["source" => "eval"];'); +$meta_count = eval('return count($meta) . ":" . count([1, [2, 3], [4]], COUNT_RECURSIVE);'); +eval('function plus_one($value) { return $value + 1; }'); +eval('function eval_example_counter() { static $n = 0; $n++; return $n; }'); +$dynamic_call = eval('return plus_one(4);'); +$dynamic_named = eval('function named_pair($left, $right) { return $left . ":" . $right; } return named_pair(right: "R", left: "L");'); +$dynamic_spread = eval('function spread_pair($left, $right) { return $left . ":" . $right; } return spread_pair(...["L", "R"]);'); +$static_first = eval('return eval_example_counter();'); +$static_second = eval('return eval_example_counter();'); +$dynamic_cuf = eval('return call_user_func("plus_one", 6);'); +$dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); +$eval_native_call = eval('return compiled_add(2, 8);'); +$eval_native_named = eval('return compiled_add(right: 8, left: 2);'); +$eval_native_spread = eval('return compiled_add(...[2, 8]);'); +$eval_native_cufa_named = eval('return call_user_func_array("compiled_add", ["right" => 8, "left" => 2]);'); +$logic = eval('return true || missing_eval_rhs();'); +$keyword_logic = eval('return true xor false;'); +$not_false = eval('return !false;'); +$coalesced = eval('return $missing ?? "coalesced";'); +$compound = eval('$n = 2; $n += 3; $label = "n="; $label .= $n; return $label;'); +$incdec = eval('$i = 0; $i++; ++$i; return $i;'); +$negative = eval('return -5 + +2;'); +$quotient = eval('return 9 / 2;'); +$modulo = eval('$n = 20; $n /= 2; return $n % 6;'); +$power = eval('return 2 ** 3;'); +$bitwise = eval('return (5 & 3) | (1 << 2);'); +$spaceship = eval('return 3 <=> 2;'); +$magic_line = eval(" +return __LINE__; +"); +eval('function EvalMagicName() { return __FUNCTION__; }'); +$magic_function = eval('return evalmagicname();'); +eval('function EvalMagicMethodName() { return __METHOD__; }'); +$magic_method = eval('return evalmagicmethodname();'); +$magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); +$magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); +$magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); +$memory = fopen("php://memory", "r+"); +$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_iterable([1]) ? "t" : "?") . (is_numeric("42") ? "n" : "?") . (is_resource($memory) ? "r" : "?");'); +$casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); +$type_name = eval('return gettype(["ok"]);'); +$absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); +$root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); +$float_binary = eval('return fdiv(10, 4) . ":" . round(fmod(10.5, 3.2), 1);'); +$rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); +$builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); +$rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); +$formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); +$formatted_text = eval('return sprintf("%s:%04d:%s", "item", 7, vsprintf("%.1f", [3]));'); +$runtime_constants = eval('return PHP_OS . ":" . DIRECTORY_SEPARATOR . ":" . (PHP_INT_MAX > 0 ? "int" : "bad") . ":" . (defined("PHP_EOL") ? "eol" : "bad");'); +$minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); +$random_range = eval('$r = rand(1, 3); $secure = random_int(4, 4); return (($r >= 1 && $r <= 3) ? "bounded" : "bad") . ":" . ($secure === 4 ? "secure" : "bad");'); +$circle = eval('return round(pi(), 2);'); +$extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); +$float_predicates = eval('return (is_nan(fdiv(0, 0)) ? "nan" : "bad") . ":" . (is_infinite(fdiv(1, 0)) ? "inf" : "bad") . ":" . (is_finite(3.14) ? "finite" : "bad");'); +$case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); +$word_case = eval('return ucwords("hello eval");'); +$reversed = eval('return strrev("eval");'); +$json_encoded = eval('return json_encode(["name" => "Ada", "skills" => ["math", "code"]]);'); +$json_decoded = eval('$decoded = json_decode("{\"ok\":true,\"items\":[1,2]}", true); return ($decoded["ok"] ? "ok" : "bad") . ":" . $decoded["items"][1];'); +$json_status = eval('return json_last_error() . ":" . json_last_error_msg();'); +$json_valid = eval('return (json_validate("[1,2,3]") ? "valid" : "bad") . ":" . (json_validate("[1]", 1) ? "bad" : "depth");'); +$contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); +$positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); +$substring_from = eval('return strstr("user@example.com", "@");'); +$ordinal = eval('return ord("A") . ":" . ord("");'); +$boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); +$trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); +$aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$array_map = eval('function eval_example_double($value) { return $value * 2; } function eval_example_pair($left, $right) { return $left . $right; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); $pairs = array_map("eval_example_pair", ["x"], ["y"]); return $mapped["a"] . ":" . $mapped["b"] . ":" . $pairs[0];'); +$array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); +$array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); +$array_pop_shift = eval('$items = ["first", "middle", "last"]; $popped = array_pop($items); $shifted = array_shift($items); return $popped . ":" . $shifted . ":" . count($items) . ":" . $items[0];'); +$array_push_unshift = eval('$items = ["middle"]; array_push($items, "last"); array_unshift($items, "first"); return count($items) . ":" . $items[0] . ":" . $items[2];'); +$array_splice = eval('$items = [10, 20, 30, 40]; $removed = array_splice($items, 1, 2); return count($removed) . ":" . $removed[0] . ":" . count($items) . ":" . $items[1];'); +$array_sort = eval('$items = [3, 1, 2]; sort($items); rsort($items); return $items[0] . ":" . $items[2];'); +$array_key_sort = eval('$items = ["b" => 1, "a" => 2]; ksort($items); return $items["a"] . ":" . $items["b"];'); +$array_natural_sort = eval('$items = ["img10", "img2", "img1"]; natsort($items); return $items[2] . ":" . $items[0];'); +$array_shuffle = eval('$items = ["x" => 1, "y" => 2]; shuffle($items); return count($items) . ":" . array_sum($items) . ":" . (isset($items["x"]) ? "bad" : "reindexed");'); +$array_user_sort = eval('function eval_example_cmp($left, $right) { return $left <=> $right; } $items = [3, 1, 2]; usort($items, "eval_example_cmp"); return $items[0] . ":" . $items[2];'); +$array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); +$array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); +$named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); +$array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$iterator_arrays = eval('$items = ["x" => 1, "y" => 2]; $copy = iterator_to_array($items); $values = iterator_to_array($items, false); return iterator_count($items) . ":" . $copy["x"] . ":" . $values[0];'); +$regex_builtins = eval('function eval_example_regex_wrap($matches) { return "[" . $matches[0] . "]"; } $ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); $all_count = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); $parts = preg_split("/,/", "a,b,c", 2); return $ok . ":" . $matches[1] . ":" . $all_count . ":" . $all[0][1] . ":" . $all[2][1] . ":" . preg_replace("/[0-9]+/", "N", "a12") . ":" . preg_replace_callback("/[A-Z]/", "eval_example_regex_wrap", "AB") . ":" . $parts[1];'); +$mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); +$append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); +$append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); +$array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); +$array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); +$array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); +$array_column = eval('$rows = [["name" => "Ada"], ["name" => "Lin"]]; $names = array_column($rows, "name"); return count($names) . ":" . $names[0] . ":" . $names[1];'); +$array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); +$array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); +$array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); +$array_sets = eval('$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); $inter = array_intersect(["a" => 1, "b" => 2, "c" => "2"], ["2"]); return count($diff) . ":" . count($inter) . ":" . $inter["b"];'); +$array_key_sets = eval('$diff = array_diff_key(["a" => 1, "b" => 2], ["a" => 0]); $inter = array_intersect_key(["x" => 7, "y" => 8], ["y" => 0]); return count($diff) . ":" . $diff["b"] . ":" . $inter["y"];'); +$range = eval('$up = range(1, 3); $down = range(3, 1); return count($up) . ":" . $up[2] . ":" . $down[2];'); +$array_rand = eval('$items = ["a" => 1, "b" => 2]; $key = array_rand($items); return array_key_exists($key, $items) ? "valid" : "bad";'); +$string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); +$ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); +$slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); +$chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); +$repeated = eval('return str_repeat("ha", 3);'); +$substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); +$substring_replaced = eval('return substr_replace("hello world", "PHP", 6, 5);'); +$padded = eval('return str_pad("hi", 5, ".");'); +$wrapped = eval('return wordwrap("hello dynamic world", 7, "|");'); +$linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); +$split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); +$string_chunks = eval('$chunks = str_split("eval", 2); return $chunks[0] . ":" . $chunks[1];'); +$replaced = eval('return str_replace("green", "lime", "red green blue");'); +$html_escaped = eval('return htmlspecialchars("bold");'); +$url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); +$checksum = eval('return crc32("hello");'); +$hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); +$digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); +$file_digest = eval('file_put_contents("eval-hash-file.txt", "abc"); $digest = hash_file("sha256", "eval-hash-file.txt"); unlink("eval-hash-file.txt"); return substr($digest, 0, 8);'); +$system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . (strlen(php_uname("s")) > 0 ? "uname" : "bad") . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); +$date_sample = eval('$ts = mktime(0, 0, 0, 1, 2, 2024); return date("Y-m-d", $ts);'); +$strtotime_sample = eval('$ts = strtotime("2024-01-02 03:04:05"); return date("Y-m-d H:i:s", $ts);'); +$micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); +$realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); +$environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); +$sleeping = eval('usleep(0); return sleep(0) . ":awake";'); +$host_lookup = eval('return (strlen(gethostname()) > 0 ? "host" : "empty") . ":" . gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host") . ":" . (strlen(gethostbyaddr("127.0.0.1")) > 0 ? "reverse" : "empty") . ":" . (gethostbyaddr("not-an-ip-address") === false ? "bad-ip" : "bad");'); +$protocol_services = eval('return getprotobyname("tcp") . ":" . getprotobynumber(17) . ":" . getservbyname("http", "tcp") . ":" . getservbyport(443, "tcp");'); +$ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); +$stream_introspection = eval('$wrappers = stream_get_wrappers(); $transports = stream_get_transports(); $filters = stream_get_filters(); return count($wrappers) . ":" . $wrappers[0] . ":" . count($transports) . ":" . (in_array("string.rot13", $filters) ? "rot13" : "missing");'); +$spl_classes = eval('$names = spl_classes(); return count($names) . ":" . (in_array("Exception", $names) ? "exception" : "missing");'); +$path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); +$resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); +$path_info = eval('$info = pathinfo("/var/log/syslog.log"); $match = fnmatch("*.LOG", "system.log", FNM_CASEFOLD); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":" . ($match ? "match" : "bad");'); +$filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); +$disk_space = eval('return (disk_free_space(".") > 0 ? "free" : "bad") . ":" . (disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad");'); +$file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $info = stat("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0 && $info["size"] === 5 && $info[7] === $info["size"]; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); +$path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); +$file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $glob = glob("eval-list-dir/*.txt"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan) && count($glob) === 1; unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); +$file_modify = eval('touch("eval-touch.txt", 1000000000); file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = filemtime("eval-touch.txt") === 1000000000 && chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-touch.txt"); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); +$hexed = eval('return bin2hex("Az");'); +$unhexed = eval('return hex2bin("417a");'); +$base64 = eval('return base64_encode("Hello");'); +$base64_decoded = eval('return base64_decode("SGVsbG8=");'); +$eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); +eval('class EvalDynamicEmptyClass {}'); +$eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); +$eval_dynamic_class_native_probe = class_exists("EvalDynamicEmptyClass") ? "yes" : "no"; +$eval_dynamic_const_probe = eval('define("EvalDynamicConst", "yes"); return EvalDynamicConst;'); +$eval_dynamic_const_native_probe = defined("EvalDynamicConst") ? "yes" : "no"; +$eval_dynamic_const_native_fetch = EvalDynamicConst; +$eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); +eval('function native_add($left, $right) { return $left + $right; }'); +eval('function native_double($value) { return $value * 2; }'); + +echo "x=" . $x . "\n"; +echo "created=" . $created . "\n"; +echo "name=" . $profile["name"] . "\n"; +echo "source=" . $meta["source"] . "\n"; +echo "meta-count=" . $meta_count . "\n"; +echo "dynamic-call=" . $dynamic_call . "\n"; +echo "dynamic-named=" . $dynamic_named . "\n"; +echo "dynamic-spread=" . $dynamic_spread . "\n"; +echo "static-counter=" . $static_first . ":" . $static_second . "\n"; +echo "dynamic-cuf=" . $dynamic_cuf . "\n"; +echo "dynamic-cufa=" . $dynamic_cufa . "\n"; +echo "eval-native-call=" . $eval_native_call . "\n"; +echo "eval-native-named=" . $eval_native_named . "\n"; +echo "eval-native-spread=" . $eval_native_spread . "\n"; +echo "eval-native-cufa-named=" . $eval_native_cufa_named . "\n"; +echo "logic=" . $logic . "\n"; +echo "keyword-logic=" . $keyword_logic . "\n"; +echo "not-false=" . $not_false . "\n"; +echo "coalesce=" . $coalesced . "\n"; +echo "ternary=" . $ternary . "\n"; +echo "compound=" . $compound . "\n"; +echo "incdec=" . $incdec . "\n"; +echo "negative=" . $negative . "\n"; +echo "quotient=" . $quotient . "\n"; +echo "modulo=" . $modulo . "\n"; +echo "power=" . $power . "\n"; +echo "bitwise=" . $bitwise . "\n"; +echo "spaceship=" . $spaceship . "\n"; +echo "magic-line=" . $magic_line . "\n"; +echo "magic-function=" . $magic_function . "\n"; +echo "magic-method=" . $magic_method . "\n"; +echo "magic-file=" . $magic_file_has_path . "\n"; +echo "magic-dir=" . $magic_dir_has_path . "\n"; +echo "magic-scope=" . $magic_scope . "\n"; +echo "type-checks=" . $type_checks . "\n"; +echo "casts=" . $casts . "\n"; +echo "type-name=" . $type_name . "\n"; +echo "absolute=" . $absolute . "\n"; +echo "root=" . $root . "\n"; +echo "float-binary=" . $float_binary . "\n"; +echo "rounding=" . $rounding . "\n"; +echo "builtin-power=" . $builtin_power . "\n"; +echo "rounded=" . $rounded . "\n"; +echo "number-format=" . $formatted_number . "\n"; +echo "printf-format=" . $formatted_text . "\n"; +echo "runtime-constants=" . $runtime_constants . "\n"; +echo "minmax=" . $minmax . "\n"; +echo "random-range=" . $random_range . "\n"; +echo "pi=" . $circle . "\n"; +echo "extended-math=" . $extended_math . "\n"; +echo "float-predicates=" . $float_predicates . "\n"; +echo "case=" . $case . "\n"; +echo "ucwords=" . $word_case . "\n"; +echo "reversed=" . $reversed . "\n"; +echo "json-encoded=" . $json_encoded . "\n"; +echo "json-decoded=" . $json_decoded . "\n"; +echo "json-status=" . $json_status . "\n"; +echo "json-valid=" . $json_valid . "\n"; +echo "contains=" . $contains . "\n"; +echo "positions=" . $positions . "\n"; +echo "strstr=" . $substring_from . "\n"; +echo "ordinal=" . $ordinal . "\n"; +echo "boundaries=" . $boundaries . "\n"; +echo "trimmed=" . $trimmed . "\n"; +echo "aggregates=" . $aggregates . "\n"; +echo "array-map=" . $array_map . "\n"; +echo "array-reduce=" . $array_reduce . "\n"; +echo "array-walk=" . $array_walk . "\n"; +echo "array-pop-shift=" . $array_pop_shift . "\n"; +echo "array-push-unshift=" . $array_push_unshift . "\n"; +echo "array-splice=" . $array_splice . "\n"; +echo "array-sort=" . $array_sort . "\n"; +echo "array-key-sort=" . $array_key_sort . "\n"; +echo "array-natural-sort=" . $array_natural_sort . "\n"; +echo "array-shuffle=" . $array_shuffle . "\n"; +echo "array-user-sort=" . $array_user_sort . "\n"; +echo "array-filter=" . $array_filter . "\n"; +echo "array-filter-callback=" . $array_filter_callback . "\n"; +echo "named-builtins=" . $named_builtins . "\n"; +echo "array-projection=" . $array_projection . "\n"; +echo "iterator-arrays=" . $iterator_arrays . "\n"; +echo "regex-builtins=" . $regex_builtins . "\n"; +echo "mixed-literal=" . $mixed_literal . "\n"; +echo "append-items=" . $append_items . "\n"; +echo "append-assoc=" . $append_assoc . "\n"; +echo "array-key-exists=" . $array_key_probe . "\n"; +echo "array-search=" . $array_search . "\n"; +echo "array-fill=" . $array_fill . "\n"; +echo "array-column=" . $array_column . "\n"; +echo "array-shapes=" . $array_shapes . "\n"; +echo "array-slice=" . $array_slice . "\n"; +echo "array-merge=" . $array_merge . "\n"; +echo "array-sets=" . $array_sets . "\n"; +echo "array-key-sets=" . $array_key_sets . "\n"; +echo "range=" . $range . "\n"; +echo "array-rand=" . $array_rand . "\n"; +echo "string-compare=" . $string_compare . "\n"; +echo "ctype=" . $ctype_checks . "\n"; +echo "slashes=" . $slashes . "\n"; +echo "chr=" . $chr . "\n"; +echo "str-repeat=" . $repeated . "\n"; +echo "substr=" . $substring . "\n"; +echo "substr-replace=" . $substring_replaced . "\n"; +echo "str-pad=" . $padded . "\n"; +echo "wordwrap=" . $wrapped . "\n"; +echo "nl2br-hex=" . $linebreaks . "\n"; +echo "explode-implode=" . $split_joined . "\n"; +echo "str-split=" . $string_chunks . "\n"; +echo "str-replace=" . $replaced . "\n"; +echo "htmlspecialchars=" . $html_escaped . "\n"; +echo "url-codec=" . $url_codec . "\n"; +echo "crc32=" . $checksum . "\n"; +echo "hash-algos=" . $hash_algos . "\n"; +echo "digest=" . $digest . "\n"; +echo "hash-file=" . $file_digest . "\n"; +echo "system-info=" . $system_info . "\n"; +echo "date-sample=" . $date_sample . "\n"; +echo "strtotime-sample=" . $strtotime_sample . "\n"; +echo "microtime=" . $micro_time . "\n"; +echo "realpath-cache=" . $realpath_cache . "\n"; +echo "environment=" . $environment . "\n"; +echo "sleep=" . $sleeping . "\n"; +echo "host-lookup=" . $host_lookup . "\n"; +echo "protocol-services=" . $protocol_services . "\n"; +echo "ip-conversion=" . $ip_conversion . "\n"; +echo "stream-introspection=" . $stream_introspection . "\n"; +echo "spl-classes=" . $spl_classes . "\n"; +echo "path-components=" . $path_components . "\n"; +echo "realpath=" . $resolved_path . "\n"; +echo "pathinfo=" . $path_info . "\n"; +echo "filesystem=" . $filesystem . "\n"; +echo "disk-space=" . $disk_space . "\n"; +echo "file-stats=" . $file_stats . "\n"; +echo "path-ops=" . $path_ops . "\n"; +echo "file-listing=" . $file_listing . "\n"; +echo "file-modify=" . $file_modify . "\n"; +echo "bin2hex=" . $hexed . "\n"; +echo "hex2bin=" . $unhexed . "\n"; +echo "base64=" . $base64 . "\n"; +echo "base64-decode=" . $base64_decoded . "\n"; +echo "eval-class-exists=" . $eval_class_probe . "\n"; +echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; +echo "native-class-exists-eval-class=" . $eval_dynamic_class_native_probe . "\n"; +echo "eval-dynamic-const-exists=" . $eval_dynamic_const_probe . "\n"; +echo "native-defined-eval-const=" . $eval_dynamic_const_native_probe . "\n"; +echo "native-fetch-eval-const=" . $eval_dynamic_const_native_fetch . "\n"; +echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; +$counter = new EvalCounter(); +$counter->bump(); +echo "eval-this-property=" . $counter->value . "\n"; +$counter->echoReadThroughEval(); +$counter->echoAddThroughEval(); +$counter->echoLabelThroughEval(); +$counter->echoLabelSpreadThroughEval(); +echo "native-dynamic-call=" . native_add(40, 2) . "\n"; +echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; +echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; +echo "arg-globals=" . eval_arg_summary() . "\n"; +echo "result=" . $result . "\n"; diff --git a/scripts/benchmark_magician.py b/scripts/benchmark_magician.py new file mode 100644 index 0000000000..e5ac20fc96 --- /dev/null +++ b/scripts/benchmark_magician.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import shutil +import statistics +import subprocess +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +SOURCE_KINDS = {"literal", "dynamic"} + + +@dataclass +class EvalFragment: + label: str + source: str + invocations: int + source_kind: str + parse_cache_should_hit: bool + + @property + def size_bytes(self) -> int: + return len(self.source.encode()) + + +@dataclass +class CaseMetadata: + name: str + description: str + fragments: list[EvalFragment] + + @property + def eval_invocations(self) -> int: + return sum(fragment.invocations for fragment in self.fragments) + + @property + def max_fragment_size_bytes(self) -> int: + if not self.fragments: + return 0 + return max(fragment.size_bytes for fragment in self.fragments) + + @property + def parse_cache_summary(self) -> str: + if self.eval_invocations == 0: + return "n/a" + if any(fragment.parse_cache_should_hit for fragment in self.fragments): + return "hit expected" + return "no hit expected" + + @property + def source_kind_summary(self) -> str: + kinds = {fragment.source_kind for fragment in self.fragments if fragment.invocations > 0} + if not kinds: + return "n/a" + if len(kinds) == 1: + return next(iter(kinds)) + return "mixed" + + +@dataclass +class CommandResult: + label: str + median_ms: float | None + samples_ms: list[float] + detail: str + + +@dataclass +class BenchmarkResult: + case: str + metadata: CaseMetadata + elephc_native: CommandResult + elephc_eval: CommandResult + php_native: CommandResult + php_eval: CommandResult + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run the elephc magician/eval benchmark suite.") + parser.add_argument("--iterations", type=int, default=5, help="Measured runs per benchmark variant.") + parser.add_argument("--warmup", type=int, default=1, help="Warmup runs per benchmark variant.") + parser.add_argument( + "--case", + action="append", + default=[], + help="Magician benchmark case name to run. Can be passed multiple times.", + ) + parser.add_argument("--json", type=Path, help="Write machine-readable benchmark results to this path.") + parser.add_argument("--markdown", type=Path, help="Write the markdown summary table to this path.") + parser.add_argument("--list", action="store_true", help="List available magician benchmark cases and exit.") + return parser.parse_args() + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def benchmark_root() -> Path: + return repo_root() / "benchmarks" / "magician" / "cases" + + +def release_artifacts_ready() -> bool: + target_dir = repo_root() / "target" / "release" + return (target_dir / "elephc").exists() and (target_dir / "libelephc_magician.a").exists() + + +def elephc_bin() -> Path: + if not release_artifacts_ready(): + subprocess.run( + ["cargo", "build", "--release"], + cwd=repo_root(), + check=True, + ) + return repo_root() / "target" / "release" / "elephc" + + +def load_metadata(case_dir: Path) -> CaseMetadata: + data = json.loads((case_dir / "metadata.json").read_text()) + fragments: list[EvalFragment] = [] + for index, item in enumerate(data.get("eval_fragments", [])): + label = str(item.get("label", f"fragment-{index}")) + source = item.get("source") + invocations = item.get("invocations") + source_kind = item.get("source_kind") + parse_cache_should_hit = item.get("parse_cache_should_hit") + + if not isinstance(source, str): + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide a string source") + if not isinstance(invocations, int) or invocations < 0: + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide non-negative invocations") + if source_kind not in SOURCE_KINDS: + raise ValueError( + f"{case_dir.name}: eval fragment {label} source_kind must be one of " + f"{', '.join(sorted(SOURCE_KINDS))}" + ) + if not isinstance(parse_cache_should_hit, bool): + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide parse_cache_should_hit") + + fragments.append( + EvalFragment( + label=label, + source=source, + invocations=invocations, + source_kind=source_kind, + parse_cache_should_hit=parse_cache_should_hit, + ) + ) + + if not fragments: + raise ValueError(f"{case_dir.name}: metadata.json must define at least one eval fragment") + + return CaseMetadata( + name=case_dir.name, + description=str(data.get("description", "")), + fragments=fragments, + ) + + +def available_cases(selected: list[str]) -> list[Path]: + root = benchmark_root() + cases = sorted(path for path in root.iterdir() if path.is_dir()) + if not selected: + return cases + wanted = set(selected) + selected_cases = [case for case in cases if case.name in wanted] + missing = sorted(wanted - {case.name for case in selected_cases}) + if missing: + raise SystemExit(f"unknown magician benchmark case(s): {', '.join(missing)}") + return selected_cases + + +def run_process(cmd: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, check=True) + except subprocess.CalledProcessError as error: + raise RuntimeError( + "command failed: {cmd}\nstdout:\n{stdout}\nstderr:\n{stderr}".format( + cmd=" ".join(cmd), + stdout=error.stdout, + stderr=error.stderr, + ) + ) from error + + +def run_checked(cmd: list[str], cwd: Path, expected: str) -> None: + output = run_process(cmd, cwd) + if output.stdout != expected: + raise RuntimeError( + f"unexpected stdout for {' '.join(cmd)}\nexpected: {expected!r}\nactual: {output.stdout!r}" + ) + + +def measure_command(label: str, cmd: list[str], cwd: Path, expected: str, iterations: int, warmup: int) -> CommandResult: + for _ in range(warmup): + run_checked(cmd, cwd, expected) + + samples: list[float] = [] + for _ in range(iterations): + started = time.perf_counter() + run_checked(cmd, cwd, expected) + samples.append((time.perf_counter() - started) * 1000.0) + + return CommandResult( + label=label, + median_ms=statistics.median(samples), + samples_ms=samples, + detail=f"{iterations} runs", + ) + + +def skipped_result(label: str, detail: str) -> CommandResult: + return CommandResult(label=label, median_ms=None, samples_ms=[], detail=detail) + + +def compile_elephc_variant(variant: str, cwd: Path) -> Path: + source = cwd / f"{variant}.php" + run_process([str(elephc_bin()), str(source)], cwd) + return cwd / variant + + +def maybe_measure_php(variant: str, cwd: Path, expected: str, iterations: int, warmup: int) -> CommandResult: + php = shutil.which("php") + label = f"php-{variant}" + if php is None: + return skipped_result(label, "php not found") + return measure_command(label, [php, str(cwd / f"{variant}.php")], cwd, expected, iterations, warmup) + + +def measure_case(case_dir: Path, iterations: int, warmup: int) -> BenchmarkResult: + metadata = load_metadata(case_dir) + expected = (case_dir / "expected.txt").read_text() + if not expected.endswith("\n"): + expected += "\n" + + with tempfile.TemporaryDirectory(prefix=f"elephc-magician-bench-{case_dir.name}-") as temp_dir: + cwd = Path(temp_dir) / case_dir.name + shutil.copytree(case_dir, cwd) + + native_binary = compile_elephc_variant("native", cwd) + eval_binary = compile_elephc_variant("eval", cwd) + + elephc_native = measure_command( + "elephc-native", + [str(native_binary)], + cwd, + expected, + iterations, + warmup, + ) + elephc_eval = measure_command( + "elephc-eval", + [str(eval_binary)], + cwd, + expected, + iterations, + warmup, + ) + php_native = maybe_measure_php("native", cwd, expected, iterations, warmup) + php_eval = maybe_measure_php("eval", cwd, expected, iterations, warmup) + + return BenchmarkResult( + case=case_dir.name, + metadata=metadata, + elephc_native=elephc_native, + elephc_eval=elephc_eval, + php_native=php_native, + php_eval=php_eval, + ) + + +def format_ms(value: float | None) -> str: + if value is None: + return "n/a" + return f"{value:.2f}" + + +def ratio(numerator: float | None, denominator: float | None) -> str: + value = ratio_value(numerator, denominator) + if value is None: + return "n/a" + return f"{value:.2f}x" + + +def ratio_value(numerator: float | None, denominator: float | None) -> float | None: + if numerator is None or denominator is None or denominator == 0: + return None + return numerator / denominator + + +def fragment_size_summary(metadata: CaseMetadata) -> str: + sizes = [fragment.size_bytes for fragment in metadata.fragments] + if len(sizes) == 1: + return str(sizes[0]) + return f"{metadata.max_fragment_size_bytes} max" + + +def result_to_dict(result: CommandResult) -> dict[str, object]: + return { + "label": result.label, + "median_ms": result.median_ms, + "samples_ms": result.samples_ms, + "detail": result.detail, + "skipped": result.median_ms is None, + } + + +def metadata_to_dict(metadata: CaseMetadata) -> dict[str, object]: + return { + "description": metadata.description, + "eval_invocations": metadata.eval_invocations, + "literal_or_dynamic": metadata.source_kind_summary, + "parse_cache": metadata.parse_cache_summary, + "max_fragment_size_bytes": metadata.max_fragment_size_bytes, + "eval_fragments": [ + { + "label": fragment.label, + "invocations": fragment.invocations, + "source_kind": fragment.source_kind, + "parse_cache_should_hit": fragment.parse_cache_should_hit, + "size_bytes": fragment.size_bytes, + } + for fragment in metadata.fragments + ], + } + + +def benchmark_to_dict(result: BenchmarkResult) -> dict[str, object]: + return { + "case": result.case, + "metadata": metadata_to_dict(result.metadata), + "elephc_native": result_to_dict(result.elephc_native), + "elephc_eval": result_to_dict(result.elephc_eval), + "php_native": result_to_dict(result.php_native), + "php_eval": result_to_dict(result.php_eval), + "ratios": { + "elephc_eval_vs_native": ratio_value(result.elephc_eval.median_ms, result.elephc_native.median_ms), + "php_eval_vs_native": ratio_value(result.php_eval.median_ms, result.php_native.median_ms), + "elephc_eval_vs_php_eval": ratio_value(result.elephc_eval.median_ms, result.php_eval.median_ms), + }, + } + + +def render_markdown(results: list[BenchmarkResult]) -> str: + lines = [ + "| case | evals | fragment bytes | kind | cache | elephc native ms | elephc eval ms | eval/native | php native ms | php eval ms | php eval/native |", + "|---|---:|---:|---|---|---:|---:|---:|---:|---:|---:|", + ] + for result in results: + lines.append( + "| {case} | {evals} | {fragment_bytes} | {kind} | {cache} | {elephc_native} | {elephc_eval} | {elephc_ratio} | {php_native} | {php_eval} | {php_ratio} |".format( + case=result.case, + evals=result.metadata.eval_invocations, + fragment_bytes=fragment_size_summary(result.metadata), + kind=result.metadata.source_kind_summary, + cache=result.metadata.parse_cache_summary, + elephc_native=format_ms(result.elephc_native.median_ms), + elephc_eval=format_ms(result.elephc_eval.median_ms), + elephc_ratio=ratio(result.elephc_eval.median_ms, result.elephc_native.median_ms), + php_native=format_ms(result.php_native.median_ms), + php_eval=format_ms(result.php_eval.median_ms), + php_ratio=ratio(result.php_eval.median_ms, result.php_native.median_ms), + ) + ) + return "\n".join(lines) + "\n" + + +def run_benchmarks(cases: list[Path], iterations: int, warmup: int) -> list[BenchmarkResult]: + return [measure_case(case_dir, iterations, warmup) for case_dir in cases] + + +def list_cases(cases: list[Path]) -> None: + for case_dir in cases: + metadata = load_metadata(case_dir) + print(f"{case_dir.name}: {metadata.description}") + + +def main() -> None: + args = parse_args() + if args.iterations < 1: + raise SystemExit("--iterations must be at least 1") + if args.warmup < 0: + raise SystemExit("--warmup must be at least 0") + + cases = available_cases(args.case) + if not cases: + raise SystemExit("no magician benchmark cases selected") + + if args.list: + list_cases(cases) + return + + results = run_benchmarks(cases, args.iterations, args.warmup) + markdown = render_markdown(results) + print(markdown, end="") + + if args.markdown: + args.markdown.write_text(markdown) + + if args.json: + payload: dict[str, Any] = { + "iterations": args.iterations, + "warmup": args.warmup, + "cases": [benchmark_to_dict(result) for result in results], + } + args.json.write_text(json.dumps(payload, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/docs/README.md b/scripts/docs/README.md index b4e77c40d0..ef05ad9a65 100644 --- a/scripts/docs/README.md +++ b/scripts/docs/README.md @@ -10,9 +10,11 @@ tree into one Markdown page per supported PHP builtin, in two flavours: source file lowers the call, which runtime helper it dispatches to, what the type checker enforces. -The script is data-driven. Its source of truth is the single-source -`builtin!` registry (`src/builtins/`), read via the `gen_builtins` binary -(`cargo run --bin gen_builtins --include-internal`). It enriches that data +The script is data-driven. Its sources of truth are the single-source +`builtin!` registry (`src/builtins/`) and the eval interpreter's +`eval_builtin!` registry (`crates/elephc-magician/src/interpreter/builtins/`), +both read via the `gen_builtins` example +(`cargo run --example gen_builtins -- --include-internal`). It enriches that data with each builtin's lowering location (parsed from the home file's `lower` hook) and documentation area, then writes a single JSON registry (`scripts/docs/builtin_registry.json`) which the Markdown renderer consumes. @@ -20,13 +22,13 @@ Everything else is generated. ## Usage -From the repo root. The generator invokes the `gen_builtins` binary, so build -it first (the extractor prefers the prebuilt binary at `target/debug/gen_builtins` -and otherwise falls back to `cargo run`): +From the repo root. The generator invokes the `gen_builtins` example, so build +it first (the extractor prefers the prebuilt binary at +`target/debug/examples/gen_builtins` and otherwise falls back to `cargo run`): ```bash # 0. Build the registry exporter the generator reads from -cargo build --bin gen_builtins +cargo build --example gen_builtins # 1. Parse the source and write the JSON registry python3 scripts/docs/extract_builtins.py diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 11221f1026..f708adbdf2 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -3,6 +3,11 @@ "area": "Misc", "canonical_name": "__elephc_gmmktime_raw", "description": "Internal raw gmmktime alias used by the synthetic DateTime body.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -80,6 +85,11 @@ "area": "Misc", "canonical_name": "__elephc_mktime_raw", "description": "Internal raw mktime alias used by the synthetic DateTime body.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -157,6 +167,11 @@ "area": "IO", "canonical_name": "__elephc_phar_bzip2_archive", "description": "Compresses a PHAR archive using bzip2.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -195,6 +210,11 @@ "area": "IO", "canonical_name": "__elephc_phar_decompress_archive", "description": "Decompresses a PHAR archive to a new path.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -233,6 +253,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_file_metadata", "description": "Reads the serialized per-file metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -270,6 +295,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_metadata", "description": "Reads the serialized PHAR-level metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -307,6 +337,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_signature_hash", "description": "Returns the PHAR signature hash bytes.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -344,6 +379,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_signature_type", "description": "Returns the PHAR signature algorithm name.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -381,6 +421,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_stub", "description": "Reads the PHAR stub script.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -418,6 +463,11 @@ "area": "IO", "canonical_name": "__elephc_phar_gzip_archive", "description": "Compresses a PHAR archive using gzip.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -456,6 +506,11 @@ "area": "IO", "canonical_name": "__elephc_phar_list_entries", "description": "Lists the file paths within a PHAR archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -494,6 +549,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_compression", "description": "Sets the compression algorithm for a PHAR archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -539,6 +599,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_file_metadata", "description": "Writes the serialized per-file metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -585,6 +650,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_metadata", "description": "Writes the serialized PHAR-level metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -629,6 +699,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_stub", "description": "Writes the PHAR stub script.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -673,6 +748,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_zip_password", "description": "Sets the encryption password for a PHAR ZIP archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -711,6 +791,11 @@ "area": "IO", "canonical_name": "__elephc_phar_sign_hash", "description": "Signs a PHAR archive with the given hash algorithm.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -755,6 +840,11 @@ "area": "IO", "canonical_name": "__elephc_phar_sign_openssl", "description": "Signs a PHAR archive using an OpenSSL private key.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -799,6 +889,11 @@ "area": "Misc", "canonical_name": "__elephc_strtotime_raw", "description": "Internal raw strtotime alias returning a plain integer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -846,6 +941,27 @@ "area": "Math", "canonical_name": "abs", "description": "Absolute value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/abs.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -885,6 +1001,27 @@ "area": "Math", "canonical_name": "acos", "description": "Returns the arccosine of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/acos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -922,6 +1059,27 @@ "area": "String", "canonical_name": "addslashes", "description": "Adds backslashes before characters that need to be escaped.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -961,6 +1119,11 @@ "area": "Array", "canonical_name": "array_all", "description": "Returns true when every array element satisfies the predicate callback.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1008,6 +1171,11 @@ "area": "Array", "canonical_name": "array_any", "description": "Returns true when at least one array element satisfies the predicate callback.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1054,6 +1222,33 @@ "area": "Array", "canonical_name": "array_chunk", "description": "Splits an array into chunks of the given size.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1098,6 +1293,33 @@ "area": "Array", "canonical_name": "array_column", "description": "Returns the values from a single column of an array of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_column.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "column_key", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1142,6 +1364,33 @@ "area": "Array", "canonical_name": "array_combine", "description": "Creates an array by using one array for keys and another for values.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "keys", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1186,6 +1435,27 @@ "area": "Array", "canonical_name": "array_diff", "description": "Computes the difference of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1229,6 +1499,11 @@ "area": "Array", "canonical_name": "array_diff_assoc", "description": "Computes the difference of arrays with additional index check.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1268,6 +1543,27 @@ "area": "Array", "canonical_name": "array_diff_key", "description": "Computes the difference of arrays using keys for comparison.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1308,6 +1604,39 @@ "area": "Array", "canonical_name": "array_fill", "description": "Fill an array with values.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "start_index", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "count", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1359,6 +1688,33 @@ "area": "Array", "canonical_name": "array_fill_keys", "description": "Fill an array with values, specifying keys.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "keys", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1403,6 +1759,39 @@ "area": "Array", "canonical_name": "array_filter", "description": "Filters elements of an array using a callback function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1457,6 +1846,11 @@ "area": "Array", "canonical_name": "array_find", "description": "Returns the first element satisfying a predicate callback, or null.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1503,6 +1897,27 @@ "area": "Array", "canonical_name": "array_flip", "description": "Exchanges all keys with their associated values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1540,6 +1955,27 @@ "area": "Array", "canonical_name": "array_intersect", "description": "Computes the intersection of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1582,6 +2018,11 @@ "area": "Array", "canonical_name": "array_intersect_assoc", "description": "Computes the intersection of arrays with additional index check.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1624,6 +2065,27 @@ "area": "Array", "canonical_name": "array_intersect_key", "description": "Computes the intersection of arrays using keys for comparison.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1663,6 +2125,11 @@ "area": "Array", "canonical_name": "array_is_list", "description": "Checks whether an array is a list (sequential 0-based integer keys).", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1706,6 +2173,33 @@ "area": "Array", "canonical_name": "array_key_exists", "description": "Checks if the given key or index exists in the array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "key", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1717,7 +2211,9 @@ "notes": [ "Lowers `array_key_exists()` for indexed arrays and associative arrays." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_hash_get" + ], "sig_arm": null, "sig_file": "src/builtins/array/array_key_exists.rs", "sig_line": null @@ -1750,6 +2246,11 @@ "area": "Array", "canonical_name": "array_key_first", "description": "Gets the first key of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1790,6 +2291,11 @@ "area": "Array", "canonical_name": "array_key_last", "description": "Gets the last key of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1830,6 +2336,27 @@ "area": "Array", "canonical_name": "array_keys", "description": "Returns all the keys of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1867,6 +2394,33 @@ "area": "Array", "canonical_name": "array_map", "description": "Applies a callback to the elements of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_map.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1911,6 +2465,20 @@ "area": "Array", "canonical_name": "array_merge", "description": "Merges the elements of two arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1943,6 +2511,11 @@ "area": "Array", "canonical_name": "array_merge_recursive", "description": "Recursively merges two arrays, combining scalar collisions into lists.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1976,6 +2549,11 @@ "area": "Array", "canonical_name": "array_multisort", "description": "Sorts multiple arrays or multi-dimensional arrays.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2023,7 +2601,40 @@ "area": "Array", "canonical_name": "array_pad", "description": "Pads an array to the specified length with a value.", - "in_catalog": true, + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, @@ -2074,6 +2685,26 @@ "area": "Array", "canonical_name": "array_pop", "description": "Pops the element off the end of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2114,6 +2745,27 @@ "area": "Array", "canonical_name": "array_product", "description": "Calculate the product of values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_product.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2153,6 +2805,26 @@ "area": "Array", "canonical_name": "array_push", "description": "Pushes one or more elements onto the end of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_push.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2190,6 +2862,27 @@ "area": "Array", "canonical_name": "array_rand", "description": "Pick one or more random keys out of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2230,6 +2923,39 @@ "area": "Array", "canonical_name": "array_reduce", "description": "Iteratively reduces an array to a single value using a callback function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "initial", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2283,6 +3009,11 @@ "area": "Array", "canonical_name": "array_replace", "description": "Replaces elements from passed arrays into the first array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2331,6 +3062,11 @@ "area": "Array", "canonical_name": "array_replace_recursive", "description": "Replaces elements from passed arrays into the first array recursively.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2378,6 +3114,33 @@ "area": "Array", "canonical_name": "array_reverse", "description": "Returns an array with the elements in reverse order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2415,6 +3178,39 @@ "area": "Array", "canonical_name": "array_search", "description": "Searches the array for a given value and returns the first corresponding key if successful.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_search.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2466,6 +3262,26 @@ "area": "Array", "canonical_name": "array_shift", "description": "Shifts an element off the beginning of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2503,6 +3319,39 @@ "area": "Array", "canonical_name": "array_slice", "description": "Extracts a slice of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2554,6 +3403,44 @@ "area": "Array", "canonical_name": "array_splice", "description": "Removes a portion of the array and replaces it with something else.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "[]", + "name": "replacement", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2605,6 +3492,27 @@ "area": "Array", "canonical_name": "array_sum", "description": "Calculate the sum of values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2645,6 +3553,11 @@ "area": "Array", "canonical_name": "array_udiff", "description": "Computes the difference of arrays using a callback comparator.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2696,6 +3609,11 @@ "area": "Array", "canonical_name": "array_uintersect", "description": "Computes the intersection of arrays using a callback comparator.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2747,6 +3665,27 @@ "area": "Array", "canonical_name": "array_unique", "description": "Removes duplicate values from an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2787,6 +3726,26 @@ "area": "Array", "canonical_name": "array_unshift", "description": "Prepends one or more elements to the beginning of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2824,6 +3783,27 @@ "area": "Array", "canonical_name": "array_values", "description": "Returns all the values of an array, re-indexed numerically.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_values.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2861,6 +3841,32 @@ "area": "Array", "canonical_name": "array_walk", "description": "Applies a user function to every member of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2907,6 +3913,11 @@ "area": "Array", "canonical_name": "array_walk_recursive", "description": "Applies a user function recursively to every member of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2955,6 +3966,26 @@ "area": "Array", "canonical_name": "arsort", "description": "Sorts an array in descending order and maintains index association.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/arsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2998,6 +4029,27 @@ "area": "Math", "canonical_name": "asin", "description": "Returns the arcsine of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/asin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3035,11 +4087,31 @@ "area": "Array", "canonical_name": "asort", "description": "Sorts an array and maintains index association.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/asort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_asort", "codegen_line": 1089, @@ -3079,6 +4151,27 @@ "area": "Math", "canonical_name": "atan", "description": "Returns the arctangent of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/atan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3116,6 +4209,33 @@ "area": "Math", "canonical_name": "atan2", "description": "Returns the arc tangent of two variables.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/atan2.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "y", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "x", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3160,6 +4280,27 @@ "area": "String", "canonical_name": "base64_decode", "description": "Decodes a Base64-encoded string back into its original data.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3199,6 +4340,27 @@ "area": "String", "canonical_name": "base64_encode", "description": "Encodes binary data into a Base64 string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3238,6 +4400,33 @@ "area": "Filesystem", "canonical_name": "basename", "description": "Returns the trailing name component of a path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "\"\"", + "name": "suffix", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3282,6 +4471,27 @@ "area": "String", "canonical_name": "bin2hex", "description": "Converts binary data into its hexadecimal string representation.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3321,6 +4531,27 @@ "area": "Type", "canonical_name": "boolval", "description": "Returns the boolean value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/boolval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3328,11 +4559,13 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_boolval", - "codegen_line": 587, + "codegen_line": 1176, "notes": [ "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_mixed_cast_bool" + ], "sig_arm": null, "sig_file": "src/builtins/types/boolval.rs", "sig_line": null @@ -3358,6 +4591,27 @@ "area": "Buffer", "canonical_name": "buffer_free", "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3395,6 +4649,27 @@ "area": "Buffer", "canonical_name": "buffer_len", "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3432,6 +4707,27 @@ "area": "Misc", "canonical_name": "buffer_new", "description": "", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3467,6 +4763,27 @@ "area": "Array", "canonical_name": "call_user_func", "description": "Calls a callback with the given arguments.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "args" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3507,6 +4824,33 @@ "area": "Array", "canonical_name": "call_user_func_array", "description": "Calls a callback with an array of parameters.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "args", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3554,6 +4898,27 @@ "area": "Math", "canonical_name": "ceil", "description": "Rounds a number up to the nearest integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/ceil.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3591,6 +4956,27 @@ "area": "Filesystem", "canonical_name": "chdir", "description": "Changes the current directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3634,6 +5020,39 @@ "area": "Date", "canonical_name": "checkdate", "description": "Validates a Gregorian date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3691,6 +5110,33 @@ "area": "Filesystem", "canonical_name": "chgrp", "description": "Changes file group.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "group", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3737,6 +5183,33 @@ "area": "Filesystem", "canonical_name": "chmod", "description": "Changes file mode.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "permissions", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3781,6 +5254,33 @@ "area": "String", "canonical_name": "chop", "description": "Alias of rtrim: strips whitespace (or other characters) from the end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/chop.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3825,6 +5325,33 @@ "area": "Filesystem", "canonical_name": "chown", "description": "Changes file owner.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3871,11 +5398,32 @@ "area": "String", "canonical_name": "chr", "description": "Returns a one-character string from the given byte code point.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/chr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "codepoint", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_chr", "codegen_line": 876, @@ -3910,6 +5458,39 @@ "area": "Math", "canonical_name": "clamp", "description": "Clamps a value to be within a specified range.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/clamp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3961,6 +5542,39 @@ "area": "Class", "canonical_name": "class_alias", "description": "Creates an alias for a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "alias", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4012,6 +5626,33 @@ "area": "Class", "canonical_name": "class_attribute_args", "description": "Returns the constructor arguments of a named attribute applied to a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "attribute_name", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4019,9 +5660,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_attribute_args", - "codegen_line": 52, + "codegen_line": 62, "notes": [ - "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array." + "Lowers `class_attribute_args(class, attr)` into a Mixed PHP argument array." ], "runtime_helpers": [], "sig_arm": null, @@ -4056,6 +5697,27 @@ "area": "Class", "canonical_name": "class_attribute_names", "description": "Returns the list of attribute names applied to a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4063,7 +5725,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_attribute_names", - "codegen_line": 36, + "codegen_line": 46, "notes": [ "Lowers `class_attribute_names(class)` into an indexed string array." ], @@ -4093,6 +5755,33 @@ "area": "Class", "canonical_name": "class_exists", "description": "Checks whether the given class has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4100,9 +5789,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -4137,6 +5826,27 @@ "area": "Class", "canonical_name": "class_get_attributes", "description": "Returns an array of ReflectionAttribute objects for all attributes of a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4144,7 +5854,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_get_attributes", - "codegen_line": 68, + "codegen_line": 78, "notes": [ "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects." ], @@ -4174,6 +5884,33 @@ "area": "Class", "canonical_name": "class_implements", "description": "Returns the interfaces which are implemented by the given class or its parents.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4218,6 +5955,33 @@ "area": "Class", "canonical_name": "class_parents", "description": "Returns the parent classes of the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4262,6 +6026,33 @@ "area": "Class", "canonical_name": "class_uses", "description": "Returns the traits used by the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4306,6 +6097,33 @@ "area": "Filesystem", "canonical_name": "clearstatcache", "description": "Clears file status cache.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "clear_realpath_cache", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "filename", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4352,6 +6170,27 @@ "area": "IO", "canonical_name": "closedir", "description": "Closes directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4394,6 +6233,33 @@ "area": "Filesystem", "canonical_name": "copy", "description": "Copies a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4443,6 +6309,27 @@ "area": "Math", "canonical_name": "cos", "description": "Returns the cosine of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/cos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4480,6 +6367,27 @@ "area": "Math", "canonical_name": "cosh", "description": "Returns the hyperbolic cosine of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/cosh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4517,6 +6425,33 @@ "area": "Array", "canonical_name": "count", "description": "Counts all elements in an array or Countable object.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/count.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4524,7 +6459,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_count", - "codegen_line": 440, + "codegen_line": 1025, "notes": [ "Lowers `count(array)` for concrete array values by reading the runtime length header.", "Called from `crate::builtins::array::count` (the registry home) via a thin wrapper.", @@ -4567,6 +6502,27 @@ "area": "String", "canonical_name": "crc32", "description": "Calculates the CRC32 polynomial of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/crc32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4609,6 +6565,27 @@ "area": "Type", "canonical_name": "ctype_alnum", "description": "Checks if all characters in the string are alphanumeric.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4646,6 +6623,27 @@ "area": "Type", "canonical_name": "ctype_alpha", "description": "Checks if all characters in the string are alphabetic.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4683,7 +6681,28 @@ "area": "Type", "canonical_name": "ctype_digit", "description": "Checks if all characters in the string are digits.", - "in_catalog": true, + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, @@ -4720,6 +6739,27 @@ "area": "Type", "canonical_name": "ctype_space", "description": "Checks if all characters in the string are whitespace characters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4757,6 +6797,33 @@ "area": "Date", "canonical_name": "date", "description": "Formats a local time/date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4804,6 +6871,20 @@ "area": "Date", "canonical_name": "date_default_timezone_get", "description": "Gets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4838,6 +6919,27 @@ "area": "Date", "canonical_name": "date_default_timezone_set", "description": "Sets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "timezoneId", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4883,6 +6985,33 @@ "area": "Misc", "canonical_name": "define", "description": "Defines a named constant at runtime.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/define.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "constant_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4890,7 +7019,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_define", - "codegen_line": 82, + "codegen_line": 372, "notes": [ "Lowers `define(\"NAME\", value)` with the duplicate-name runtime guard." ], @@ -4927,6 +7056,27 @@ "area": "Misc", "canonical_name": "defined", "description": "Checks whether a given named constant exists.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/defined.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "constant_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4934,7 +7084,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_defined", - "codegen_line": 261, + "codegen_line": 551, "notes": [ "Lowers `defined(\"NAME\")` for compile-time string constant names." ], @@ -4964,6 +7114,27 @@ "area": "Math", "canonical_name": "deg2rad", "description": "Converts a degree value to radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5001,6 +7172,27 @@ "area": "Process", "canonical_name": "die", "description": "", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/die.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "status", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5036,6 +7228,33 @@ "area": "Filesystem", "canonical_name": "dirname", "description": "Returns a parent directory's path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "1", + "name": "levels", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5083,6 +7302,27 @@ "area": "Filesystem", "canonical_name": "disk_free_space", "description": "Returns available space on filesystem or disk partition.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5122,6 +7362,27 @@ "area": "Filesystem", "canonical_name": "disk_total_space", "description": "Returns the total size of a filesystem or disk partition.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5161,6 +7422,27 @@ "area": "Misc", "canonical_name": "empty", "description": "Determines whether a variable is considered empty.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5168,7 +7450,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 619, + "codegen_line": 1218, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -5200,6 +7482,33 @@ "area": "Class", "canonical_name": "enum_exists", "description": "Checks if the enum has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "enum", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5207,9 +7516,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -5244,6 +7553,27 @@ "area": "Process", "canonical_name": "exec", "description": "Executes an external program and returns the last line of output.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5281,6 +7611,27 @@ "area": "Process", "canonical_name": "exit", "description": "", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/exit.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "status", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5316,6 +7667,27 @@ "area": "Math", "canonical_name": "exp", "description": "Returns e raised to the power of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/exp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5353,6 +7725,39 @@ "area": "String", "canonical_name": "explode", "description": "Splits a string by a separator into an array of substrings.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/explode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "separator", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "9223372036854775807", + "name": "limit", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5407,6 +7812,27 @@ "area": "IO", "canonical_name": "fclose", "description": "Closes an open file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5444,6 +7870,27 @@ "area": "IO", "canonical_name": "fdatasync", "description": "Synchronizes data (but not meta-data) to file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5483,6 +7930,33 @@ "area": "Math", "canonical_name": "fdiv", "description": "Divides two numbers, according to IEEE 754.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5527,6 +8001,27 @@ "area": "IO", "canonical_name": "feof", "description": "Tests for end-of-file on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5567,14 +8062,35 @@ "area": "IO", "canonical_name": "fflush", "description": "Flushes the output to a file.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen/lower_inst/builtins/io.rs", - "codegen_function": "lower_fflush", - "codegen_line": 3266, + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fflush", + "codegen_line": 3266, "notes": [ "Lowers `fflush(stream)` through the shared fd flush runtime helper." ], @@ -5606,6 +8122,27 @@ "area": "IO", "canonical_name": "fgetc", "description": "Gets a character from the given file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5646,6 +8183,39 @@ "area": "IO", "canonical_name": "fgetcsv", "description": "Gets line from file pointer and parse for CSV fields.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "\",\"", + "name": "separator", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5700,6 +8270,27 @@ "area": "IO", "canonical_name": "fgets", "description": "Gets line from file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5740,6 +8331,27 @@ "area": "IO", "canonical_name": "file", "description": "Reads an entire file into an array.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5780,6 +8392,27 @@ "area": "Filesystem", "canonical_name": "file_exists", "description": "Checks whether a file or directory exists.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5820,6 +8453,27 @@ "area": "IO", "canonical_name": "file_get_contents", "description": "Reads an entire file into a string.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5860,6 +8514,33 @@ "area": "IO", "canonical_name": "file_put_contents", "description": "Writes data to a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5907,6 +8588,27 @@ "area": "Filesystem", "canonical_name": "fileatime", "description": "Gets last access time of file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5949,6 +8651,27 @@ "area": "Filesystem", "canonical_name": "filectime", "description": "Gets inode change time of file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5991,6 +8714,27 @@ "area": "Filesystem", "canonical_name": "filegroup", "description": "Gets file group.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6033,6 +8777,27 @@ "area": "Filesystem", "canonical_name": "fileinode", "description": "Gets file inode.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6075,6 +8840,27 @@ "area": "Filesystem", "canonical_name": "filemtime", "description": "Gets file modification time.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6118,6 +8904,27 @@ "area": "Filesystem", "canonical_name": "fileowner", "description": "Gets file owner.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6159,6 +8966,27 @@ "area": "Filesystem", "canonical_name": "fileperms", "description": "Gets file permissions.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6201,6 +9029,27 @@ "area": "Filesystem", "canonical_name": "filesize", "description": "Gets file size.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6243,6 +9092,27 @@ "area": "Filesystem", "canonical_name": "filetype", "description": "Gets file type.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6284,6 +9154,27 @@ "area": "Type", "canonical_name": "floatval", "description": "Returns the float value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/floatval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6291,11 +9182,12 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_floatval", - "codegen_line": 557, + "codegen_line": 1142, "notes": [ "Lowers `floatval()` for concrete scalar operands." ], "runtime_helpers": [ + "__rt_mixed_cast_float", "__rt_str_to_number" ], "sig_arm": null, @@ -6323,6 +9215,38 @@ "area": "IO", "canonical_name": "flock", "description": "Portable advisory file locking.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "operation", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "would_block", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6374,6 +9298,27 @@ "area": "Math", "canonical_name": "floor", "description": "Rounds a number down to the nearest integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/floor.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6411,6 +9356,33 @@ "area": "Math", "canonical_name": "fmod", "description": "Returns the floating point remainder of the division of the arguments.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/fmod.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6455,15 +9427,48 @@ "area": "Filesystem", "canonical_name": "fnmatch", "description": "Matches a filename against a pattern.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen/lower_inst/builtins/io.rs", - "codegen_function": "lower_fnmatch", - "codegen_line": 4603, - "notes": [ + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fnmatch", + "codegen_line": 4603, + "notes": [ "Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper." ], "runtime_helpers": [], @@ -6506,6 +9511,45 @@ "area": "IO", "canonical_name": "fopen", "description": "Opens file or URL.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "use_include_path", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "context", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6566,6 +9610,27 @@ "area": "IO", "canonical_name": "fpassthru", "description": "Output all remaining data on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6606,6 +9671,33 @@ "area": "IO", "canonical_name": "fprintf", "description": "Write a formatted string to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6652,6 +9744,45 @@ "area": "IO", "canonical_name": "fputcsv", "description": "Format line as CSV and write to file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "fields", + "optional": false + }, + { + "by_ref": false, + "default": "\",\"", + "name": "separator", + "optional": true + }, + { + "by_ref": false, + "default": "\"\\\"\"", + "name": "enclosure", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6712,6 +9843,33 @@ "area": "IO", "canonical_name": "fread", "description": "Binary-safe file read.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6758,6 +9916,33 @@ "area": "IO", "canonical_name": "fscanf", "description": "Parses input from a file according to a format.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6805,6 +9990,39 @@ "area": "IO", "canonical_name": "fseek", "description": "Seeks on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "whence", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6856,6 +10074,50 @@ "area": "Streams", "canonical_name": "fsockopen", "description": "Open Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "error_code", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "error_message", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6921,6 +10183,27 @@ "area": "IO", "canonical_name": "fstat", "description": "Gets information about a file using an open file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6960,6 +10243,27 @@ "area": "IO", "canonical_name": "fsync", "description": "Synchronizes changes to the file (including meta-data).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7000,6 +10304,27 @@ "area": "IO", "canonical_name": "ftell", "description": "Returns the current position of the file read/write pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7039,6 +10364,33 @@ "area": "IO", "canonical_name": "ftruncate", "description": "Truncates a file to a given length.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7083,6 +10435,27 @@ "area": "Class", "canonical_name": "function_exists", "description": "Returns true if the given function has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "function", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7090,7 +10463,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_function_exists", - "codegen_line": 276, + "codegen_line": 566, "notes": [ "Lowers `function_exists(\"name\")` for compile-time string names.", "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", @@ -7125,6 +10498,33 @@ "area": "IO", "canonical_name": "fwrite", "description": "Binary-safe file write.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7169,10 +10569,72 @@ }, { "area": "Class", - "canonical_name": "get_class", - "description": "Returns the name of the class of an object.", - "in_catalog": true, - "is_internal": false, + "canonical_name": "get_called_class", + "description": "get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_called_class", + "sig": { + "params": [], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_called_class", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_class", + "description": "Returns the name of the class of an object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, @@ -7204,10 +10666,136 @@ "slug": "get_class", "sub_area": "Class" }, + { + "area": "Class", + "canonical_name": "get_class_methods", + "description": "get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_class_methods", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_class_methods", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_class_vars", + "description": "get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_class_vars", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_class_vars", + "sub_area": "Class" + }, { "area": "Class", "canonical_name": "get_declared_classes", "description": "Returns an array of the names of the defined classes.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7215,7 +10803,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7237,6 +10825,20 @@ "area": "Class", "canonical_name": "get_declared_interfaces", "description": "Returns an array of all declared interfaces.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7244,7 +10846,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7266,6 +10868,20 @@ "area": "Class", "canonical_name": "get_declared_traits", "description": "Returns an array of all declared traits.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7273,7 +10889,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7291,10 +10907,87 @@ "slug": "get_declared_traits", "sub_area": "Class" }, + { + "area": "Class", + "canonical_name": "get_object_vars", + "description": "get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_object_vars", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_object_vars", + "sub_area": "Class" + }, { "area": "Class", "canonical_name": "get_parent_class", "description": "Returns the name of the parent class of an object or class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object_or_class", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7332,6 +11025,27 @@ "area": "Type", "canonical_name": "get_resource_id", "description": "Returns an integer identifier for the given resource.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7339,7 +11053,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_id", - "codegen_line": 424, + "codegen_line": 431, "notes": [ "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." ], @@ -7369,6 +11083,27 @@ "area": "Type", "canonical_name": "get_resource_type", "description": "Returns the type of a resource.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7376,7 +11111,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_type", - "codegen_line": 412, + "codegen_line": 419, "notes": [ "Lowers `get_resource_type(resource)` to elephc's current resource type label." ], @@ -7406,6 +11141,20 @@ "area": "Filesystem", "canonical_name": "getcwd", "description": "Gets the current working directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7438,6 +11187,27 @@ "area": "Date", "canonical_name": "getdate", "description": "Returns date/time information.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/getdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7482,6 +11252,27 @@ "area": "Filesystem", "canonical_name": "getenv", "description": "Gets the value of an environment variable.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7521,6 +11312,27 @@ "area": "IO", "canonical_name": "gethostbyaddr", "description": "Gets the Internet host name corresponding to a given IP address.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7561,6 +11373,27 @@ "area": "IO", "canonical_name": "gethostbyname", "description": "Gets the IPv4 address corresponding to the given Internet host name.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7601,6 +11434,20 @@ "area": "IO", "canonical_name": "gethostname", "description": "Gets the standard host name for the local machine.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7634,6 +11481,27 @@ "area": "IO", "canonical_name": "getprotobyname", "description": "Gets the protocol number associated with the given protocol name.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7673,6 +11541,27 @@ "area": "IO", "canonical_name": "getprotobynumber", "description": "Gets the protocol name associated with the given protocol number.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7712,6 +11601,33 @@ "area": "IO", "canonical_name": "getservbyname", "description": "Gets port number associated with an Internet service and protocol.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "service", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7758,6 +11674,33 @@ "area": "IO", "canonical_name": "getservbyport", "description": "Gets the Internet service that corresponds to a port and protocol.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7804,6 +11747,27 @@ "area": "Type", "canonical_name": "gettype", "description": "Returns the type of a variable as a string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/gettype.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7811,7 +11775,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_gettype", - "codegen_line": 129, + "codegen_line": 419, "notes": [ "Lowers `gettype(value)` for statically concrete PHP types." ], @@ -7841,6 +11805,27 @@ "area": "Filesystem", "canonical_name": "glob", "description": "Finds pathnames matching a pattern.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7880,6 +11865,33 @@ "area": "Date", "canonical_name": "gmdate", "description": "Formats a GMT/UTC date and time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7929,6 +11941,57 @@ "area": "Date", "canonical_name": "gmmktime", "description": "Returns the Unix timestamp for a GMT date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hour", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "minute", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "second", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 6, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8007,6 +12070,27 @@ "area": "String", "canonical_name": "grapheme_strrev", "description": "Reverses a string by grapheme cluster, returning false on failure.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8047,6 +12131,33 @@ "area": "String", "canonical_name": "gzcompress", "description": "Compress a string using the ZLIB data format.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8091,6 +12202,33 @@ "area": "String", "canonical_name": "gzdeflate", "description": "Deflate a string using the DEFLATE data format.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8135,6 +12273,33 @@ "area": "String", "canonical_name": "gzinflate", "description": "Inflate a deflated string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8179,6 +12344,33 @@ "area": "String", "canonical_name": "gzuncompress", "description": "Uncompress a compressed string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8225,6 +12417,39 @@ "area": "String", "canonical_name": "hash", "description": "Generates a hash value using the given algorithm.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8278,6 +12503,20 @@ "area": "String", "canonical_name": "hash_algos", "description": "Returns an array of supported hashing algorithm names.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8310,6 +12549,27 @@ "area": "String", "canonical_name": "hash_copy", "description": "Copies the state of an incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8352,6 +12612,33 @@ "area": "String", "canonical_name": "hash_equals", "description": "Compares two strings using a constant-time algorithm.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "known_string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user_string", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8400,6 +12687,39 @@ "area": "IO", "canonical_name": "hash_file", "description": "Generates a hash value using the contents of a given file.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8451,6 +12771,33 @@ "area": "String", "canonical_name": "hash_final", "description": "Finalizes an incremental hash and returns the digest string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8497,6 +12844,45 @@ "area": "String", "canonical_name": "hash_hmac", "description": "Generates a keyed hash value using the HMAC method.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "key", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8558,6 +12944,39 @@ "area": "String", "canonical_name": "hash_init", "description": "Initialize an incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "key", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8604,13 +13023,40 @@ "return_type": "mixed", "variadic": null }, - "slug": "hash_init", - "sub_area": "String" - }, - { - "area": "String", - "canonical_name": "hash_update", - "description": "Pumps data into an active incremental hashing context.", + "slug": "hash_init", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "hash_update", + "description": "Pumps data into an active incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8657,6 +13103,39 @@ "area": "Misc", "canonical_name": "header", "description": "Sends a raw HTTP header.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/header.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "header", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "replace", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "response_code", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8715,6 +13194,27 @@ "area": "String", "canonical_name": "hex2bin", "description": "Decodes a hexadecimal string back into its binary representation.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8754,6 +13254,27 @@ "area": "Date", "canonical_name": "hrtime", "description": "Returns the current high-resolution time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "as_number", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8798,6 +13319,27 @@ "area": "String", "canonical_name": "html_entity_decode", "description": "Converts HTML entities in a string back into their corresponding characters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8837,6 +13379,39 @@ "area": "String", "canonical_name": "htmlentities", "description": "Converts all applicable characters in a string into their HTML entities.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"UTF-8\"", + "name": "encoding", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8897,6 +13472,39 @@ "area": "String", "canonical_name": "htmlspecialchars", "description": "Converts the HTML special characters in a string into their entities.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"UTF-8\"", + "name": "encoding", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8957,6 +13565,27 @@ "area": "Misc", "canonical_name": "http_response_code", "description": "Gets or sets the HTTP response code.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "response_code", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9000,6 +13629,33 @@ "area": "Math", "canonical_name": "hypot", "description": "Calculates the length of the hypotenuse of a right-angle triangle.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/hypot.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "x", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "y", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9044,6 +13700,33 @@ "area": "String", "canonical_name": "implode", "description": "Joins array elements into a single string using a separator.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/implode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "separator", + "optional": true + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9088,6 +13771,39 @@ "area": "Array", "canonical_name": "in_array", "description": "Checks if a value exists in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/in_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9139,6 +13855,27 @@ "area": "String", "canonical_name": "inet_ntop", "description": "Converts a packed internet address to a human-readable representation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9178,6 +13915,27 @@ "area": "String", "canonical_name": "inet_pton", "description": "Converts a human-readable IP address to its packed in_addr representation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9217,6 +13975,33 @@ "area": "Math", "canonical_name": "intdiv", "description": "Integer division.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9261,6 +14046,33 @@ "area": "Class", "canonical_name": "interface_exists", "description": "Checks if the interface has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "interface", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9268,9 +14080,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -9305,6 +14117,27 @@ "area": "Type", "canonical_name": "intval", "description": "Returns the integer value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/intval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9312,7 +14145,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 524, + "codegen_line": 1109, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -9345,6 +14178,27 @@ "area": "String", "canonical_name": "ip2long", "description": "Converts a string containing an IPv4 address into a long integer.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9369,22 +14223,55 @@ "params": [ { "by_ref": false, - "default": null, - "name": "ip", - "optional": false, - "type": "string" + "default": null, + "name": "ip", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "ip2long", + "sub_area": "String" + }, + { + "area": "Class", + "canonical_name": "is_a", + "description": "Checks whether an object is of a given type or has it as one of its parents.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "allow_string", + "optional": true } ], - "return_type": "mixed", + "required_param_count": 2, + "supported": true, "variadic": null }, - "slug": "ip2long", - "sub_area": "String" - }, - { - "area": "Class", - "canonical_name": "is_a", - "description": "Checks whether an object is of a given type or has it as one of its parents.", + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9436,6 +14323,27 @@ "area": "Type", "canonical_name": "is_array", "description": "Checks whether a variable is an array.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9443,7 +14351,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_array", - "codegen_line": 1004, + "codegen_line": 1603, "notes": [ "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", "whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is", @@ -9475,6 +14383,27 @@ "area": "Type", "canonical_name": "is_bool", "description": "Checks whether a variable is a boolean.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9482,7 +14411,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9512,6 +14441,39 @@ "area": "Type", "canonical_name": "is_callable", "description": "Checks whether a variable can be called as a function.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "syntax_only", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "callable_name", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9519,14 +14481,13 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_callable", - "codegen_line": 319, + "codegen_line": 712, "notes": [ "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." ], "runtime_helpers": [ "__rt_is_callable_array", - "__rt_is_callable_assoc", - "__rt_is_callable_object" + "__rt_is_callable_assoc" ], "sig_arm": null, "sig_file": "src/builtins/types/is_callable.rs", @@ -9553,6 +14514,27 @@ "area": "Filesystem", "canonical_name": "is_dir", "description": "Tells whether the filename is a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9594,6 +14576,27 @@ "area": "Filesystem", "canonical_name": "is_executable", "description": "Tells whether the filename is executable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9636,6 +14639,27 @@ "area": "Filesystem", "canonical_name": "is_file", "description": "Tells whether the filename is a regular file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9677,6 +14701,27 @@ "area": "Math", "canonical_name": "is_finite", "description": "Checks whether a float is finite.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9714,6 +14759,27 @@ "area": "Type", "canonical_name": "is_float", "description": "Checks whether a variable is a floating-point number.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_float.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9721,7 +14787,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9751,6 +14817,27 @@ "area": "Math", "canonical_name": "is_infinite", "description": "Checks whether a float is infinite.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9788,6 +14875,27 @@ "area": "Type", "canonical_name": "is_int", "description": "Checks whether a variable is an integer.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_int.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9795,7 +14903,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9825,6 +14933,27 @@ "area": "Type", "canonical_name": "is_iterable", "description": "Checks whether a variable is iterable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9832,7 +14961,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_iterable", - "codegen_line": 795, + "codegen_line": 1394, "notes": [ "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], @@ -9862,6 +14991,27 @@ "area": "Filesystem", "canonical_name": "is_link", "description": "Tells whether the filename is a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9904,6 +15054,27 @@ "area": "Math", "canonical_name": "is_nan", "description": "Checks whether a float is NAN.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9941,6 +15112,27 @@ "area": "Type", "canonical_name": "is_null", "description": "Checks whether a variable is null.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9948,7 +15140,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_null_builtin", - "codegen_line": 994, + "codegen_line": 1593, "notes": [ "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." ], @@ -9978,6 +15170,27 @@ "area": "Type", "canonical_name": "is_numeric", "description": "Checks whether a variable is a number or a numeric string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10015,6 +15228,27 @@ "area": "Type", "canonical_name": "is_object", "description": "Checks whether a variable is an object.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_object.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10022,7 +15256,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_object", - "codegen_line": 1019, + "codegen_line": 1618, "notes": [ "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", "runtime tag is an object (6)." @@ -10053,6 +15287,27 @@ "area": "Filesystem", "canonical_name": "is_readable", "description": "Tells whether the filename is readable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10094,6 +15349,27 @@ "area": "Type", "canonical_name": "is_resource", "description": "Checks whether a variable is a resource.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10101,7 +15377,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_is_resource", - "codegen_line": 400, + "codegen_line": 407, "notes": [ "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." ], @@ -10131,6 +15407,27 @@ "area": "Type", "canonical_name": "is_scalar", "description": "Checks whether a variable is a scalar.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10138,7 +15435,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_scalar", - "codegen_line": 1035, + "codegen_line": 1634, "notes": [ "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", "Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,", @@ -10170,6 +15467,27 @@ "area": "Type", "canonical_name": "is_string", "description": "Checks whether a variable is a string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10177,7 +15495,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -10207,6 +15525,39 @@ "area": "Class", "canonical_name": "is_subclass_of", "description": "Checks if the object has a given class as one of its parents or implements it.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "allow_string", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10258,6 +15609,27 @@ "area": "Filesystem", "canonical_name": "is_writable", "description": "Tells whether the filename is writable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10299,6 +15671,27 @@ "area": "Filesystem", "canonical_name": "is_writeable", "description": "Tells whether the filename is writable (alias of is_writable).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10340,6 +15733,27 @@ "area": "Misc", "canonical_name": "isset", "description": "Determines whether a variable is set and is not null.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "var", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10377,6 +15791,39 @@ "area": "SPL", "canonical_name": "iterator_apply", "description": "Call a function for every element in an iterator.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "args", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10428,6 +15875,27 @@ "area": "SPL", "canonical_name": "iterator_count", "description": "Count the elements in an iterator.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10465,6 +15933,33 @@ "area": "SPL", "canonical_name": "iterator_to_array", "description": "Copy the iterator into an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "preserve_keys", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10509,6 +16004,45 @@ "area": "JSON", "canonical_name": "json_decode", "description": "Decodes a JSON string.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "json", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "associative", + "optional": true + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10569,6 +16103,39 @@ "area": "JSON", "canonical_name": "json_encode", "description": "Returns the JSON representation of a value.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10620,6 +16187,20 @@ "area": "JSON", "canonical_name": "json_last_error", "description": "Returns the last error (if any) occurred during the last JSON encoding/decoding.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10651,6 +16232,20 @@ "area": "JSON", "canonical_name": "json_last_error_msg", "description": "Returns the error string of the last json_encode() or json_decode() call.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10683,6 +16278,39 @@ "area": "JSON", "canonical_name": "json_validate", "description": "Checks if a string contains valid JSON.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "json", + "optional": false + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10736,6 +16364,26 @@ "area": "Array", "canonical_name": "krsort", "description": "Sorts an array by key in descending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/krsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10777,6 +16425,26 @@ "area": "Array", "canonical_name": "ksort", "description": "Sorts an array by key in ascending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/ksort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10819,6 +16487,27 @@ "area": "String", "canonical_name": "lcfirst", "description": "Lowercases the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10858,6 +16547,33 @@ "area": "Filesystem", "canonical_name": "lchgrp", "description": "Changes group ownership of a symlink.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "group", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10904,6 +16620,33 @@ "area": "Filesystem", "canonical_name": "lchown", "description": "Changes user ownership of a symlink.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10950,6 +16693,33 @@ "area": "Filesystem", "canonical_name": "link", "description": "Creates a hard link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "target", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "link", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10999,6 +16769,27 @@ "area": "Filesystem", "canonical_name": "linkinfo", "description": "Gets information about a link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11041,6 +16832,33 @@ "area": "Date", "canonical_name": "localtime", "description": "Returns the local time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/localtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "associative", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11093,6 +16911,33 @@ "area": "Math", "canonical_name": "log", "description": "Natural logarithm.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "2.718281828459045", + "name": "base", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11137,6 +16982,27 @@ "area": "Math", "canonical_name": "log10", "description": "Returns the base-10 logarithm of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log10.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11174,6 +17040,27 @@ "area": "Math", "canonical_name": "log2", "description": "Returns the base-2 logarithm of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log2.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11211,6 +17098,27 @@ "area": "String", "canonical_name": "long2ip", "description": "Converts an IPv4 address from long integer to dotted string notation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11251,6 +17159,27 @@ "area": "Filesystem", "canonical_name": "lstat", "description": "Gives information about a file or symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11291,6 +17220,33 @@ "area": "String", "canonical_name": "ltrim", "description": "Strips whitespace (or other characters) from the beginning of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11335,6 +17291,27 @@ "area": "Math", "canonical_name": "max", "description": "Find highest value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/max.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11372,6 +17349,39 @@ "area": "Regex", "canonical_name": "mb_ereg_match", "description": "Tests whether a regex pattern matches the beginning of a string (multibyte).", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11427,6 +17437,33 @@ "area": "String", "canonical_name": "md5", "description": "Calculates the MD5 hash of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/md5.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11475,6 +17512,27 @@ "area": "Date", "canonical_name": "microtime", "description": "Returns the current Unix timestamp with microseconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/microtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "as_float", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11523,6 +17581,27 @@ "area": "Math", "canonical_name": "min", "description": "Find lowest value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/min.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11560,6 +17639,27 @@ "area": "Filesystem", "canonical_name": "mkdir", "description": "Makes a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11603,6 +17703,57 @@ "area": "Date", "canonical_name": "mktime", "description": "Returns the Unix timestamp for a date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/mktime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hour", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "minute", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "second", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 6, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11679,6 +17830,33 @@ "area": "Math", "canonical_name": "mt_rand", "description": "Generate a random value via the Mersenne Twister Random Number Generator.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11725,6 +17903,26 @@ "area": "Array", "canonical_name": "natcasesort", "description": "Sorts an array using a case-insensitive natural order algorithm.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11764,6 +17962,26 @@ "area": "Array", "canonical_name": "natsort", "description": "Sorts an array using a natural order algorithm.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/natsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11804,6 +18022,33 @@ "area": "String", "canonical_name": "nl2br", "description": "Inserts HTML line breaks before newlines in a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "use_xhtml", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11843,6 +18088,45 @@ "area": "String", "canonical_name": "number_format", "description": "Formats a number with grouped thousands.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "decimals", + "optional": true + }, + { + "by_ref": false, + "default": "\".\"", + "name": "decimal_separator", + "optional": true + }, + { + "by_ref": false, + "default": "\",\"", + "name": "thousands_separator", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11903,6 +18187,27 @@ "area": "IO", "canonical_name": "opendir", "description": "Open directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11944,6 +18249,27 @@ "area": "String", "canonical_name": "ord", "description": "Returns the ASCII value of the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ord.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "character", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11981,6 +18307,27 @@ "area": "Process", "canonical_name": "passthru", "description": "Executes an external program and passes its output directly.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12021,6 +18368,33 @@ "area": "Filesystem", "canonical_name": "pathinfo", "description": "Returns information about a file path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "15", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12067,6 +18441,27 @@ "area": "Process", "canonical_name": "pclose", "description": "Closes process file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12106,6 +18501,50 @@ "area": "Streams", "canonical_name": "pfsockopen", "description": "Open persistent Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "error_code", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "error_message", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12171,6 +18610,27 @@ "area": "Misc", "canonical_name": "php_uname", "description": "Returns information about the operating system PHP is running on.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "\"a\"", + "name": "mode", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12210,6 +18670,20 @@ "area": "Misc", "canonical_name": "phpversion", "description": "Returns the current PHP version information.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12217,7 +18691,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_phpversion", - "codegen_line": 251, + "codegen_line": 541, "notes": [ "Lowers `phpversion()` as the compiler package version string." ], @@ -12239,6 +18713,20 @@ "area": "Math", "canonical_name": "pi", "description": "Gets value of pi.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/pi.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12268,6 +18756,33 @@ "area": "Process", "canonical_name": "popen", "description": "Opens process file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12314,6 +18829,33 @@ "area": "Math", "canonical_name": "pow", "description": "Exponential expression.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/pow.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "exponent", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12358,6 +18900,45 @@ "area": "Regex", "canonical_name": "preg_match", "description": "Performs a regular expression match.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": true, + "default": "[]", + "name": "matches", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12412,6 +18993,45 @@ "area": "Regex", "canonical_name": "preg_match_all", "description": "Performs a global regular expression match and returns the number of matches.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": true, + "default": "[]", + "name": "matches", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12459,6 +19079,39 @@ "area": "Regex", "canonical_name": "preg_replace", "description": "Performs a regular expression search and replace.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replacement", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12512,6 +19165,39 @@ "area": "Regex", "canonical_name": "preg_replace_callback", "description": "Performs a regular expression search and replace using a callback.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12565,6 +19251,45 @@ "area": "Regex", "canonical_name": "preg_split", "description": "Splits a string by a regular expression.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "limit", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12625,6 +19350,33 @@ "area": "Misc", "canonical_name": "print_r", "description": "Prints human-readable information about a variable.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/print_r.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "return", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12680,6 +19432,27 @@ "area": "String", "canonical_name": "printf", "description": "Outputs a formatted string.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12719,6 +19492,27 @@ "area": "Pointer", "canonical_name": "ptr", "description": "Returns a raw pointer to the given variable.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12756,6 +19550,27 @@ "area": "Pointer", "canonical_name": "ptr_get", "description": "Reads one machine word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12793,6 +19608,27 @@ "area": "Pointer", "canonical_name": "ptr_is_null", "description": "Returns true if the pointer is null.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12830,6 +19666,20 @@ "area": "Pointer", "canonical_name": "ptr_null", "description": "Returns a null raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12859,6 +19709,33 @@ "area": "Pointer", "canonical_name": "ptr_offset", "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12903,6 +19780,27 @@ "area": "Pointer", "canonical_name": "ptr_read16", "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12942,6 +19840,27 @@ "area": "Pointer", "canonical_name": "ptr_read32", "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12981,6 +19900,27 @@ "area": "Pointer", "canonical_name": "ptr_read8", "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13018,6 +19958,33 @@ "area": "Pointer", "canonical_name": "ptr_read_string", "description": "Copies raw bytes from a pointer into a PHP string of the given length.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13064,6 +20031,33 @@ "area": "Pointer", "canonical_name": "ptr_set", "description": "Writes one machine word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13108,6 +20102,27 @@ "area": "Pointer", "canonical_name": "ptr_sizeof", "description": "Returns the byte size of the named pointer target type.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13145,6 +20160,33 @@ "area": "Pointer", "canonical_name": "ptr_write16", "description": "Writes one 16-bit word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13191,6 +20233,33 @@ "area": "Pointer", "canonical_name": "ptr_write32", "description": "Writes one 32-bit word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13237,6 +20306,33 @@ "area": "Pointer", "canonical_name": "ptr_write8", "description": "Writes one byte through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13281,6 +20377,33 @@ "area": "Pointer", "canonical_name": "ptr_write_string", "description": "Copies PHP string bytes into raw memory at the given pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13327,6 +20450,27 @@ "area": "Filesystem", "canonical_name": "putenv", "description": "Sets an environment variable.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "assignment", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13366,6 +20510,27 @@ "area": "Math", "canonical_name": "rad2deg", "description": "Converts a radian value to degrees.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13403,6 +20568,33 @@ "area": "Math", "canonical_name": "rand", "description": "Generate a random integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13449,6 +20641,33 @@ "area": "Math", "canonical_name": "random_int", "description": "Get a cryptographically secure, uniformly selected integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/random_int.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13493,6 +20712,33 @@ "area": "Array", "canonical_name": "range", "description": "Create an array containing a range of elements.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/range.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "start", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "end", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13540,6 +20786,27 @@ "area": "String", "canonical_name": "rawurldecode", "description": "Decodes an RFC 3986 percent-encoded string without treating '+' as a space.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13579,6 +20846,27 @@ "area": "String", "canonical_name": "rawurlencode", "description": "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces).", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13618,6 +20906,27 @@ "area": "IO", "canonical_name": "readdir", "description": "Read entry from directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13660,6 +20969,27 @@ "area": "Filesystem", "canonical_name": "readfile", "description": "Outputs a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13697,6 +21027,27 @@ "area": "Process", "canonical_name": "readline", "description": "Reads a line from the user's terminal.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "prompt", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13736,6 +21087,27 @@ "area": "Filesystem", "canonical_name": "readlink", "description": "Returns the target of a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13778,6 +21150,27 @@ "area": "Filesystem", "canonical_name": "realpath", "description": "Returns canonicalized absolute pathname.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13817,6 +21210,20 @@ "area": "Filesystem", "canonical_name": "realpath_cache_get", "description": "Returns realpath cache entries.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13846,6 +21253,20 @@ "area": "Filesystem", "canonical_name": "realpath_cache_size", "description": "Returns the amount of memory used by the realpath cache.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13875,6 +21296,33 @@ "area": "Filesystem", "canonical_name": "rename", "description": "Renames a file or directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13923,6 +21371,27 @@ "area": "IO", "canonical_name": "rewind", "description": "Rewind the position of a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13960,6 +21429,27 @@ "area": "IO", "canonical_name": "rewinddir", "description": "Rewind directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14000,6 +21490,27 @@ "area": "Filesystem", "canonical_name": "rmdir", "description": "Removes a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14043,6 +21554,33 @@ "area": "Math", "canonical_name": "round", "description": "Rounds a float.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/round.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "precision", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14087,6 +21625,26 @@ "area": "Array", "canonical_name": "rsort", "description": "Sorts an array in descending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/rsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14132,6 +21690,33 @@ "area": "String", "canonical_name": "rtrim", "description": "Strips whitespace (or other characters) from the end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14176,6 +21761,27 @@ "area": "Filesystem", "canonical_name": "scandir", "description": "Lists files and directories inside the specified path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14216,6 +21822,11 @@ "area": "Misc", "canonical_name": "serialize", "description": "Generates a storable representation of a value.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14260,6 +21871,32 @@ "area": "Type", "canonical_name": "settype", "description": "Sets the type of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/settype.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "var", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14304,6 +21941,33 @@ "area": "String", "canonical_name": "sha1", "description": "Calculates the SHA-1 hash of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/sha1.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14351,6 +22015,27 @@ "area": "Process", "canonical_name": "shell_exec", "description": "Executes a command via the shell and returns the complete output as a string.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14388,6 +22073,26 @@ "area": "Array", "canonical_name": "shuffle", "description": "Shuffles an array into random order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14427,6 +22132,27 @@ "area": "Math", "canonical_name": "sin", "description": "Returns the sine of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14464,6 +22190,27 @@ "area": "Math", "canonical_name": "sinh", "description": "Returns the hyperbolic sine of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sinh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14501,6 +22248,27 @@ "area": "Process", "canonical_name": "sleep", "description": "Delays execution for a number of seconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/sleep.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14540,6 +22308,26 @@ "area": "Array", "canonical_name": "sort", "description": "Sorts an array in ascending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/sort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14579,13 +22367,40 @@ "return_type": "bool", "variadic": null }, - "slug": "sort", - "sub_area": "Array" - }, - { - "area": "SPL", - "canonical_name": "spl_autoload", - "description": "Default implementation for __autoload().", + "slug": "sort", + "sub_area": "Array" + }, + { + "area": "SPL", + "canonical_name": "spl_autoload", + "description": "Default implementation for __autoload().", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "file_extensions", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14630,6 +22445,27 @@ "area": "SPL", "canonical_name": "spl_autoload_call", "description": "Try all registered __autoload() functions to load the requested class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14667,6 +22503,27 @@ "area": "SPL", "canonical_name": "spl_autoload_extensions", "description": "Register and return default file extensions for spl_autoload.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "file_extensions", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14704,6 +22561,20 @@ "area": "SPL", "canonical_name": "spl_autoload_functions", "description": "Return all registered __autoload() functions.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14733,6 +22604,39 @@ "area": "SPL", "canonical_name": "spl_autoload_register", "description": "Register given function as __autoload() implementation.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true + }, + { + "by_ref": false, + "default": "true", + "name": "throw", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "prepend", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14784,6 +22688,27 @@ "area": "SPL", "canonical_name": "spl_autoload_unregister", "description": "Unregister given function as __autoload() implementation.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14821,6 +22746,20 @@ "area": "SPL", "canonical_name": "spl_classes", "description": "Return available SPL classes.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14852,6 +22791,27 @@ "area": "SPL", "canonical_name": "spl_object_hash", "description": "Return hash id for given object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14891,6 +22851,27 @@ "area": "SPL", "canonical_name": "spl_object_id", "description": "Return the integer object handle for given object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14930,6 +22911,27 @@ "area": "String", "canonical_name": "sprintf", "description": "Returns a formatted string.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14969,6 +22971,27 @@ "area": "Math", "canonical_name": "sqrt", "description": "Returns the square root of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15006,6 +23029,33 @@ "area": "String", "canonical_name": "sscanf", "description": "Parses a string according to a format.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15053,6 +23103,27 @@ "area": "Filesystem", "canonical_name": "stat", "description": "Gives information about a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15094,6 +23165,33 @@ "area": "String", "canonical_name": "str_contains", "description": "Determines if a string contains a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15140,6 +23238,33 @@ "area": "String", "canonical_name": "str_ends_with", "description": "Checks if a string ends with a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15186,6 +23311,45 @@ "area": "String", "canonical_name": "str_ireplace", "description": "Case-insensitive version of str_replace().", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "search", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "count", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15244,6 +23408,45 @@ "area": "String", "canonical_name": "str_pad", "description": "Pads a string to a certain length with another string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "\" \"", + "name": "pad_string", + "optional": true + }, + { + "by_ref": false, + "default": "1", + "name": "pad_type", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15304,6 +23507,33 @@ "area": "String", "canonical_name": "str_repeat", "description": "Repeats a string a given number of times.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "times", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15350,6 +23580,45 @@ "area": "String", "canonical_name": "str_replace", "description": "Replaces all occurrences of a search string with a replacement string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "search", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "count", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15408,6 +23677,33 @@ "area": "String", "canonical_name": "str_split", "description": "Converts a string into an array of chunks of the given length.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_split.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "1", + "name": "length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15454,6 +23750,33 @@ "area": "String", "canonical_name": "str_starts_with", "description": "Checks if a string starts with a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15500,6 +23823,33 @@ "area": "String", "canonical_name": "strcasecmp", "description": "Binary safe case-insensitive string comparison. Returns negative, zero, or positive.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15546,6 +23896,33 @@ "area": "String", "canonical_name": "strcmp", "description": "Binary safe string comparison. Returns negative, zero, or positive.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15592,6 +23969,33 @@ "area": "Streams", "canonical_name": "stream_bucket_append", "description": "Appends a bucket to the brigade.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "bucket", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15636,6 +24040,27 @@ "area": "IO", "canonical_name": "stream_bucket_make_writeable", "description": "Returns a bucket object from the brigade for use in a stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15675,6 +24100,33 @@ "area": "IO", "canonical_name": "stream_bucket_new", "description": "Creates a new bucket for use in a stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15719,6 +24171,33 @@ "area": "Streams", "canonical_name": "stream_bucket_prepend", "description": "Prepends a bucket to the brigade.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "bucket", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15763,6 +24242,33 @@ "area": "IO", "canonical_name": "stream_context_create", "description": "Creates a stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15807,6 +24313,27 @@ "area": "IO", "canonical_name": "stream_context_get_default", "description": "Retrieves the default stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15844,6 +24371,27 @@ "area": "IO", "canonical_name": "stream_context_get_options", "description": "Retrieves options for the specified stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15884,6 +24432,27 @@ "area": "IO", "canonical_name": "stream_context_get_params", "description": "Retrieves parameters from the specified stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15921,6 +24490,27 @@ "area": "IO", "canonical_name": "stream_context_set_default", "description": "Sets the default stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "options", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15958,6 +24548,45 @@ "area": "IO", "canonical_name": "stream_context_set_option", "description": "Sets an option on the specified context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "wrapper_or_options", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "option_name", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "value", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16016,6 +24645,33 @@ "area": "IO", "canonical_name": "stream_context_set_params", "description": "Sets parameters on the specified context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "params", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16060,6 +24716,45 @@ "area": "IO", "canonical_name": "stream_copy_to_stream", "description": "Copies data from one stream to another.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16108,16 +24803,55 @@ "type": "int" } ], - "return_type": "mixed", + "return_type": "mixed", + "variadic": null + }, + "slug": "stream_copy_to_stream", + "sub_area": "IO" + }, + { + "area": "Streams", + "canonical_name": "stream_filter_append", + "description": "Attaches a filter to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filtername", + "optional": false + }, + { + "by_ref": false, + "default": "3", + "name": "read_write", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, "variadic": null }, - "slug": "stream_copy_to_stream", - "sub_area": "IO" - }, - { - "area": "Streams", - "canonical_name": "stream_filter_append", - "description": "Attaches a filter to a stream.", + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16176,6 +24910,45 @@ "area": "Streams", "canonical_name": "stream_filter_prepend", "description": "Attaches a filter to a stream (prepend).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filtername", + "optional": false + }, + { + "by_ref": false, + "default": "3", + "name": "read_write", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16234,6 +25007,33 @@ "area": "IO", "canonical_name": "stream_filter_register", "description": "Registers a user-defined stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filter_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16280,6 +25080,27 @@ "area": "IO", "canonical_name": "stream_filter_remove", "description": "Removes a filter from a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream_filter", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16319,6 +25140,39 @@ "area": "IO", "canonical_name": "stream_get_contents", "description": "Reads remainder of a stream into a string.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16370,6 +25224,20 @@ "area": "IO", "canonical_name": "stream_get_filters", "description": "Retrieves list of registered filters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16399,6 +25267,39 @@ "area": "IO", "canonical_name": "stream_get_line", "description": "Gets line from stream resource up to a given delimiter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "\"\"", + "name": "ending", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16450,6 +25351,27 @@ "area": "IO", "canonical_name": "stream_get_meta_data", "description": "Retrieves metadata from streams/file pointers.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16489,6 +25411,20 @@ "area": "IO", "canonical_name": "stream_get_transports", "description": "Retrieves list of registered socket transports.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16518,6 +25454,20 @@ "area": "IO", "canonical_name": "stream_get_wrappers", "description": "Retrieves list of registered streams.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16547,6 +25497,27 @@ "area": "IO", "canonical_name": "stream_is_local", "description": "Checks if a stream is a local stream.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16584,6 +25555,27 @@ "area": "IO", "canonical_name": "stream_isatty", "description": "Checks if a stream is a TTY.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16623,6 +25615,27 @@ "area": "IO", "canonical_name": "stream_resolve_include_path", "description": "Resolves filename against the include path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16663,6 +25676,50 @@ "area": "IO", "canonical_name": "stream_select", "description": "Runs the equivalent of the select() system call on the given arrays of streams.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "read", + "optional": false + }, + { + "by_ref": true, + "default": null, + "name": "write", + "optional": false + }, + { + "by_ref": true, + "default": null, + "name": "except", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "microseconds", + "optional": true + } + ], + "required_param_count": 4, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16728,6 +25785,33 @@ "area": "IO", "canonical_name": "stream_set_blocking", "description": "Sets blocking/non-blocking mode on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "enable", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16775,6 +25859,33 @@ "area": "IO", "canonical_name": "stream_set_chunk_size", "description": "Sets the read chunk size on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16819,6 +25930,33 @@ "area": "IO", "canonical_name": "stream_set_read_buffer", "description": "Sets the read file buffering on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16841,28 +25979,61 @@ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" + "name": "stream", + "optional": false, + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false, + "type": "int" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "stream_set_read_buffer", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "stream_set_timeout", + "description": "Sets timeout period on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false }, { "by_ref": false, - "default": null, - "name": "size", - "optional": false, - "type": "int" + "default": "0", + "name": "microseconds", + "optional": true } ], - "return_type": "int", + "required_param_count": 2, + "supported": true, "variadic": null }, - "slug": "stream_set_read_buffer", - "sub_area": "IO" - }, - { - "area": "IO", - "canonical_name": "stream_set_timeout", - "description": "Sets timeout period on a stream.", + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16914,6 +26085,33 @@ "area": "IO", "canonical_name": "stream_set_write_buffer", "description": "Sets the write file buffering on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16958,6 +26156,38 @@ "area": "IO", "canonical_name": "stream_socket_accept", "description": "Accept a connection on a socket created by stream_socket_server().", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "peer_name", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17011,6 +26241,27 @@ "area": "IO", "canonical_name": "stream_socket_client", "description": "Open Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "address", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17051,6 +26302,45 @@ "area": "IO", "canonical_name": "stream_socket_enable_crypto", "description": "Turns encryption on/off on an already connected socket.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "enable", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "crypto_method", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "session_stream", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17109,6 +26399,33 @@ "area": "IO", "canonical_name": "stream_socket_get_name", "description": "Retrieve the name of the local or remote sockets.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "remote", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17155,6 +26472,39 @@ "area": "IO", "canonical_name": "stream_socket_pair", "description": "Creates a pair of connected, indistinguishable socket streams.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "domain", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17208,6 +26558,44 @@ "area": "IO", "canonical_name": "stream_socket_recvfrom", "description": "Receives data from a socket, connected or not.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": true, + "default": "\"\"", + "name": "address", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17266,6 +26654,45 @@ "area": "IO", "canonical_name": "stream_socket_sendto", "description": "Sends a message to a socket, whether it is connected or not.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "address", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17324,6 +26751,27 @@ "area": "IO", "canonical_name": "stream_socket_server", "description": "Create an Internet or Unix domain server socket.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "address", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17363,6 +26811,33 @@ "area": "IO", "canonical_name": "stream_socket_shutdown", "description": "Shutdown a full-duplex connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17409,6 +26884,27 @@ "area": "IO", "canonical_name": "stream_supports_lock", "description": "Tells whether the stream supports locking.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17448,6 +26944,39 @@ "area": "IO", "canonical_name": "stream_wrapper_register", "description": "Registers a URL wrapper implemented as a PHP class.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17501,6 +27030,27 @@ "area": "IO", "canonical_name": "stream_wrapper_restore", "description": "Restores a previously unregistered built-in wrapper.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17538,6 +27088,27 @@ "area": "IO", "canonical_name": "stream_wrapper_unregister", "description": "Unregisters a previously registered URL wrapper.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17577,6 +27148,27 @@ "area": "String", "canonical_name": "stripslashes", "description": "Removes backslashes from a string previously escaped by addslashes.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17616,6 +27208,27 @@ "area": "String", "canonical_name": "strlen", "description": "Returns the length of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strlen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17623,7 +27236,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_strlen", - "codegen_line": 494, + "codegen_line": 1079, "notes": [ "Lowers `strlen()` by coercing string-like values and returning the byte length." ], @@ -17655,6 +27268,39 @@ "area": "String", "canonical_name": "strpos", "description": "Finds the numeric position of the first occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strpos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17706,6 +27352,27 @@ "area": "String", "canonical_name": "strrev", "description": "Reverses a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrev.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17745,6 +27412,39 @@ "area": "String", "canonical_name": "strrpos", "description": "Finds the numeric position of the last occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17796,6 +27496,39 @@ "area": "String", "canonical_name": "strstr", "description": "Returns the portion of a string starting at the first occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strstr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "before_needle", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17847,6 +27580,27 @@ "area": "String", "canonical_name": "strtolower", "description": "Converts a string to lowercase.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17886,6 +27640,33 @@ "area": "Date", "canonical_name": "strtotime", "description": "Parses an English textual datetime description into a Unix timestamp.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "datetime", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "baseTimestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17938,6 +27719,27 @@ "area": "String", "canonical_name": "strtoupper", "description": "Converts a string to uppercase.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17977,6 +27779,39 @@ "area": "String", "canonical_name": "substr", "description": "Returns a portion of a string specified by the offset and length.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/substr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18030,6 +27865,45 @@ "area": "String", "canonical_name": "substr_replace", "description": "Replaces text within a portion of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18091,6 +27965,33 @@ "area": "Filesystem", "canonical_name": "symlink", "description": "Creates a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "target", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "link", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18140,6 +28041,20 @@ "area": "Filesystem", "canonical_name": "sys_get_temp_dir", "description": "Returns the directory path used for temporary files.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18171,6 +28086,27 @@ "area": "Process", "canonical_name": "system", "description": "Executes an external program and displays the output.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/system.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18210,6 +28146,27 @@ "area": "Math", "canonical_name": "tan", "description": "Returns the tangent of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/tan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18247,6 +28204,27 @@ "area": "Math", "canonical_name": "tanh", "description": "Returns the hyperbolic tangent of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/tanh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18284,6 +28262,33 @@ "area": "Filesystem", "canonical_name": "tempnam", "description": "Creates a file with a unique filename.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "prefix", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18332,6 +28337,20 @@ "area": "Date", "canonical_name": "time", "description": "Returns the current Unix timestamp.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/time.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18363,6 +28382,20 @@ "area": "Filesystem", "canonical_name": "tmpfile", "description": "Creates a temporary file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18389,13 +28422,46 @@ "return_type": "mixed", "variadic": null }, - "slug": "tmpfile", - "sub_area": "Filesystem" - }, - { - "area": "Filesystem", - "canonical_name": "touch", - "description": "Sets access and modification time of a file.", + "slug": "tmpfile", + "sub_area": "Filesystem" + }, + { + "area": "Filesystem", + "canonical_name": "touch", + "description": "Sets access and modification time of a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "mtime", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "atime", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18447,6 +28513,33 @@ "area": "Class", "canonical_name": "trait_exists", "description": "Checks whether the trait exists.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "trait", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18454,9 +28547,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -18491,6 +28584,33 @@ "area": "String", "canonical_name": "trim", "description": "Strips whitespace (or other characters) from the beginning and end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/trim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18535,6 +28655,32 @@ "area": "Array", "canonical_name": "uasort", "description": "Sorts an array with a user-defined comparison function and maintains index association.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/uasort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18581,6 +28727,27 @@ "area": "String", "canonical_name": "ucfirst", "description": "Uppercases the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18620,6 +28787,33 @@ "area": "String", "canonical_name": "ucwords", "description": "Uppercases the first character of each word in a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\t\\r\\n\\u{c}\\u{b}\"", + "name": "separators", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18666,6 +28860,32 @@ "area": "Array", "canonical_name": "uksort", "description": "Sorts an array by keys using a user-defined comparison function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/uksort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18712,6 +28932,27 @@ "area": "Filesystem", "canonical_name": "umask", "description": "Changes the current umask.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "mask", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18751,6 +28992,27 @@ "area": "Filesystem", "canonical_name": "unlink", "description": "Deletes a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18792,6 +29054,11 @@ "area": "Misc", "canonical_name": "unserialize", "description": "Creates a PHP value from a stored representation.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18843,6 +29110,27 @@ "area": "Misc", "canonical_name": "unset", "description": "Unsets the given variables.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "var", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18880,6 +29168,27 @@ "area": "String", "canonical_name": "urldecode", "description": "Decodes a URL-encoded string, including '+' as a space.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18919,6 +29228,27 @@ "area": "String", "canonical_name": "urlencode", "description": "URL-encodes a string using application/x-www-form-urlencoded rules.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18958,6 +29288,27 @@ "area": "Process", "canonical_name": "usleep", "description": "Delays execution for a number of microseconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/usleep.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "microseconds", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18997,6 +29348,32 @@ "area": "Array", "canonical_name": "usort", "description": "Sorts an array by values using a user-defined comparison function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/usort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19043,6 +29420,27 @@ "area": "Misc", "canonical_name": "var_dump", "description": "Dumps information about a variable, including its type and value.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19081,6 +29479,39 @@ "area": "IO", "canonical_name": "vfprintf", "description": "Write a formatted string to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19135,6 +29566,33 @@ "area": "String", "canonical_name": "vprintf", "description": "Outputs a formatted string using an array of values.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19181,6 +29639,33 @@ "area": "String", "canonical_name": "vsprintf", "description": "Returns a formatted string using an array of values.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19227,6 +29712,45 @@ "area": "String", "canonical_name": "wordwrap", "description": "Wraps a string to a given number of characters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "75", + "name": "width", + "optional": true + }, + { + "by_ref": false, + "default": "\"\\n\"", + "name": "break", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "cut_long_words", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19288,6 +29812,11 @@ "area": "Pointer", "canonical_name": "zval_free", "description": "Frees a PHP zval pointer allocated by `zval_pack`.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19327,6 +29856,11 @@ "area": "Pointer", "canonical_name": "zval_pack", "description": "Packs an elephc runtime value into a heap-allocated PHP zval pointer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19375,6 +29909,11 @@ "area": "Pointer", "canonical_name": "zval_type", "description": "Returns the PHP zval type byte for a zval pointer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19415,6 +29954,11 @@ "area": "Pointer", "canonical_name": "zval_unpack", "description": "Unpacks a PHP zval pointer into an owned elephc Mixed value.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { diff --git a/scripts/docs/elephc_builtins/extract.py b/scripts/docs/elephc_builtins/extract.py index f40fb36b19..917d5705fa 100644 --- a/scripts/docs/elephc_builtins/extract.py +++ b/scripts/docs/elephc_builtins/extract.py @@ -3,9 +3,11 @@ Since the single-source builtin registry migration, every PHP builtin is declared once via `builtin!` in ``src/builtins//.rs`` and collected through the `inventory` crate. The authoritative data is therefore read from the registry -itself, via the ``gen_builtins`` binary (``cargo run --bin gen_builtins +itself, via the ``gen_builtins`` example (``cargo run --example gen_builtins -- --include-internal``), NOT by regex-scraping ``catalog.rs`` / ``signatures.rs`` -(which the migration emptied). +(which the migration emptied). The exporter also attaches, per builtin, the eval +interpreter's (elephc-magician) support block sourced from the ``eval_builtin!`` +registry, plus records for builtins only the eval interpreter exposes. For each builtin we enrich the registry data with: @@ -60,25 +62,29 @@ # --------------------------------------------------------------------------- def run_gen_builtins(repo: Path) -> list[dict]: - """Return the registry as a list of dicts by invoking the `gen_builtins` binary. + """Return the registry as a list of dicts by invoking the `gen_builtins` example. Includes `internal` builtins (the docs pipeline renders compiler-internals - pages for the `__elephc_*` helpers). Prefers a prebuilt binary under - ``target/{release,debug}/`` when present (fast path for CI, which builds it - first); otherwise falls back to ``cargo run``. + pages for the `__elephc_*` helpers) and per-builtin eval-interpreter support + blocks. Prefers a prebuilt binary under ``target/{release,debug}/examples/`` + when present (fast path for CI, which builds it first); otherwise falls back + to ``cargo run``. """ cmd: list[str] for profile in ("release", "debug"): - exe = repo / "target" / profile / "gen_builtins" + exe = repo / "target" / profile / "examples" / "gen_builtins" if exe.exists(): cmd = [str(exe), "--include-internal"] break else: - cmd = ["cargo", "run", "--quiet", "--bin", "gen_builtins", "--", "--include-internal"] + cmd = [ + "cargo", "run", "--quiet", "--example", "gen_builtins", "--", + "--include-internal", + ] proc = subprocess.run(cmd, cwd=repo, capture_output=True, text=True) if proc.returncode != 0: sys.exit( - "gen_builtins failed (build it with `cargo build --bin gen_builtins`):\n" + "gen_builtins failed (build it with `cargo build --example gen_builtins`):\n" + proc.stderr ) try: @@ -468,10 +474,69 @@ def _render_default(value, optional: bool) -> Optional[str]: } +# Docs area for eval-only builtins, keyed by the magician EvalArea spelling. +EVAL_AREA_TO_DOCS_AREA: dict[str, tuple[str, str]] = { + "array": ("Array", "Array"), + "core": ("Misc", "Misc"), + "filesystem": ("Filesystem", "Filesystem"), + "formatting": ("String", "String"), + "json": ("JSON", "JSON"), + "math": ("Math", "Math"), + "network_env": ("Network", "Network"), + "regex": ("Regex", "Regex"), + "raw_memory": ("Pointer", "Pointer"), + "string": ("String", "String"), + "symbols": ("Class", "Class"), + "time": ("Date", "Date"), + "types": ("Type", "Type"), +} + + # --------------------------------------------------------------------------- # Orchestration # --------------------------------------------------------------------------- +def _eval_only_builtin(entry: dict) -> Builtin: + """Build a Builtin for a name only the eval interpreter (magician) exposes.""" + name = entry["name"] + canonical = name.lower() + eval_support = entry.get("eval") or {} + area, sub_area = EVAL_AREA_TO_DOCS_AREA.get( + eval_support.get("area", ""), ("Misc", "Misc") + ) + params = [ + Parameter( + name=p["name"], + php_type=_normalize_type(p.get("type", "mixed")), + by_ref=bool(p.get("by_ref")), + default=_render_default(p.get("default"), bool(p.get("optional"))), + optional=bool(p.get("optional")), + ) + for p in entry.get("params", []) + ] + description = DESCRIPTION_OVERRIDES.get(canonical, "") or ( + f"{name}() is available inside eval'd code via the magician interpreter; " + "compiled (AOT) code does not support it yet." + ) + return Builtin( + name=name, + canonical_name=canonical, + in_catalog=True, + is_internal=bool(entry.get("internal")), + area=area, + sub_area=sub_area, + sig=BuiltinSig( + params=params, + variadic=entry.get("variadic"), + return_type=_normalize_type(entry.get("returns", "mixed")), + ), + lowering=LoweringInfo(), + description=description, + eval_support=eval_support, + eval_only=True, + ) + + def build_registry(repo: Path) -> list[Builtin]: """Build the full list of builtins from the registry + language constructs.""" src = repo / "src" @@ -502,10 +567,22 @@ def resolve_check_body(fn_name: str) -> str: builtins: list[Builtin] = [] + # Eval blocks for compiler-resident constructs (isset, strval, ...): the + # exporter appends them as `aot_resident` records; their AOT docs live in + # the hand-curated LANGUAGE_CONSTRUCTS entries (or on canonical alias + # pages), so only the eval block is consumed here. + resident_eval: dict[str, dict] = {} + # --- registry builtins (PHP-visible + internal helpers) --- for entry in gen: name = entry["name"] canonical = name.lower() + if entry.get("aot_resident"): + resident_eval[canonical] = entry.get("eval") or {} + continue + if entry.get("eval_only"): + builtins.append(_eval_only_builtin(entry)) + continue is_internal = bool(entry.get("internal")) in_catalog = not is_internal @@ -570,6 +647,7 @@ def resolve_check_body(fn_name: str) -> str: ), lowering=lowering, description=description, + eval_support=entry.get("eval"), ) ) @@ -603,6 +681,7 @@ def resolve_check_body(fn_name: str) -> str: ), lowering=lowering, description=description, + eval_support=resident_eval.get(canonical), ) ) @@ -671,6 +750,8 @@ def _builtin_to_dict(b: Builtin) -> dict: "runtime_helpers": b.lowering.runtime_helpers, "notes": b.lowering.notes, }, + "eval": b.eval_support or {"supported": False, "kind": "none"}, + "eval_only": b.eval_only, } diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index 0451c1df4f..6bd7e82a01 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -1201,6 +1201,11 @@ class Builtin: examples: List[str] = field(default_factory=list) # raw ```php ... ``` blocks see_also: List[str] = field(default_factory=list) notes: List[str] = field(default_factory=list) + # Eval-interpreter (elephc-magician) support block from the gen_builtins + # exporter: {supported, kind, area, hooks, params, variadic, home_file}. + eval_support: Optional[dict] = None + # True when only the eval interpreter exposes this builtin (no AOT support). + eval_only: bool = False def slug(name: str) -> str: diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index 71792fe8be..3fc5004d99 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -45,6 +45,8 @@ {return_section} +{availability_section} + {examples_section} {notes_section} @@ -84,6 +86,10 @@ {checker_notes} +## Eval interpreter (magician) + +{eval_section} + ## Cross-references {user_link} @@ -203,6 +209,72 @@ def _github_url_with_line(repo_root: Path, file_path: str, line: int) -> str: return f"https://github.com/illegalstudio/elephc/blob/main/{rel}#L{line}" +def _availability_section(b: dict) -> str: + """Two-line support matrix: compiled (AOT) vs eval() interpreter.""" + lines = ["## Availability", ""] + if b.get("eval_only"): + lines.append( + "- **Compiled (AOT)**: not available — compiled programs cannot " + "call this builtin yet." + ) + else: + lines.append("- **Compiled (AOT)**: supported by the Elephc code generator.") + ev = b.get("eval") or {} + if ev.get("supported"): + kind = ev.get("kind") + if kind == "registry": + home = ev.get("home_file") or "" + lines.append( + "- **`eval()` (magician interpreter)**: supported — declarative " + f"interpreter builtin ([`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home}))." + ) + elif kind == "date-alias": + lines.append( + "- **`eval()` (magician interpreter)**: supported through the " + "procedural date/time alias dispatcher." + ) + else: + lines.append("- **`eval()` (magician interpreter)**: supported.") + else: + lines.append( + "- **`eval()` (magician interpreter)**: not available inside eval'd code." + ) + return "\n".join(lines) + + +def _eval_internals_section(b: dict) -> str: + """How the eval interpreter reaches this builtin, for the internals page.""" + ev = b.get("eval") or {} + if not ev.get("supported"): + return ( + "_Not callable from eval'd code — the magician interpreter has no " + "entry for this builtin._" + ) + if ev.get("kind") == "date-alias": + home = ev.get("home_file") or "" + return ( + "Dispatched as a procedural date/time alias by " + f"[`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home})." + ) + home = ev.get("home_file") or "" + hooks = ev.get("hooks") or [] + lines = [ + f"- **Declaration**: [`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home}) (`eval_builtin!`)", + "- **Dispatch hooks**: " + + (", ".join(f"`{h}`" for h in hooks) or "_none_"), + ] + by_ref = [p["name"] for p in (ev.get("params") or []) if p.get("by_ref")] + if by_ref: + lines.append( + "- **By-reference parameters**: " + + ", ".join(f"`${n}`" for n in by_ref) + + "." + ) + if ev.get("variadic"): + lines.append(f"- **Variadic**: collects excess arguments into `${ev['variadic']}`.") + return "\n".join(lines) + + def _internals_link(b: dict) -> str: """Cross-link to the internals page for this builtin, if it has been lowered. @@ -230,7 +302,7 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: _ = repo_root # reserved for future cross-repo links area_lower = b['area'].lower() article = "an" if area_lower[0] in "aeiou" else "a" - return USER_TEMPLATE.format( + rendered = USER_TEMPLATE.format( name=b["name"], short_description=_short_description(b).replace('"', '\\"'), area=b["area"], @@ -241,15 +313,26 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: "Behavior matches the PHP manual unless noted below.", parameters_section=_parameters_section(b), return_section=_return_section(b), + availability_section=_availability_section(b), examples_section=_examples_section(b), notes_section=_notes_section(b), see_also_section=_see_also_section(b), internals_link=_internals_link(b), ) + # Empty optional sections can stack several blank lines. Preserve the + # established output otherwise, but collapse pathological trailing runs. + if rendered.endswith("\n\n\n"): + return rendered.rstrip() + "\n" + return rendered def render_internals(b: dict, order: int, repo_root: Path) -> str: - sig_file = b["lowering"].get("sig_file") or "src/types/signatures.rs" + sig_file = b["lowering"].get("sig_file") + if not sig_file and b.get("eval_only"): + # Eval-only builtins have no static signature home; point at the + # magician declaration instead. + sig_file = (b.get("eval") or {}).get("home_file") + sig_file = sig_file or "src/types/signatures.rs" codegen_file = b["lowering"].get("codegen_file") codegen_line = b["lowering"].get("codegen_line") codegen_function = b["lowering"].get("codegen_function") or "(none — type-checker only)" @@ -303,6 +386,7 @@ def render_internals(b: dict, order: int, repo_root: Path) -> str: runtime_helpers_section=_runtime_helpers_section(b), signature=_signature_line(b), checker_notes=_checker_notes(b), + eval_section=_eval_internals_section(b), see_also_section=see_also_section, ) @@ -362,8 +446,11 @@ def _index_table_rows(builtins: list[dict], link_prefix: str = ".") -> list[str] link = f"{link_prefix}/_internal/{slug(b['name'])}.md" else: link = f"{link_prefix}/{area_folder}/{slug(b['name'])}.md" + aot = "—" if b.get("eval_only") else "✓" + ev = "✓" if (b.get("eval") or {}).get("supported") else "—" rows.append( - f"| [`{b['name']}()`]({link}) | `{sig}` | `{b['sig']['return_type']}` |" + f"| [`{b['name']}()`]({link}) | `{sig}` | `{b['sig']['return_type']}` " + f"| {aot} | {ev} |" ) return rows @@ -382,8 +469,8 @@ def render_area_index(area: str, builtins: list[dict], order: int = 0) -> str: "", f"## {area} builtins", "", - "| Function | Signature | Returns |", - "|---|---|---|", + "| Function | Signature | Returns | AOT | eval() |", + "|---|---|---|:-:|:-:|", ] lines.extend(_index_table_rows(relevant)) return "\n".join(lines) + "\n" @@ -403,8 +490,8 @@ def render_master_index(builtins: list[dict]) -> str: "", "## Builtins", "", - "| Function | Signature | Returns |", - "|---|---|---|", + "| Function | Signature | Returns | AOT | eval() |", + "|---|---|---|:-:|:-:|", ] lines.extend(_index_table_rows(relevant, link_prefix="./builtins")) return "\n".join(lines) + "\n" diff --git a/scripts/test-linux-arm64.sh b/scripts/test-linux-arm64.sh index 9d3b282312..16c9425001 100755 --- a/scripts/test-linux-arm64.sh +++ b/scripts/test-linux-arm64.sh @@ -69,8 +69,8 @@ trap cleanup EXIT INT TERM # Run tests with the project mounted as a volume. Build the bridge staticlib # crates first so libelephc_tls.a / libelephc_pdo.a / libelephc_crypto.a / -# libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a -# exist in the target dir — +# libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a / +# libelephc_magician.a exist in the target dir — # `cargo test` alone never emits the staticlib crate-type. if [ "$TEST_ARG_COUNT" -eq 0 ]; then echo "Running all tests on Linux ARM64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." @@ -86,7 +86,7 @@ if [ "$TEST_ARG_COUNT" -eq 0 ]; then -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web && cargo test' + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test' else echo "Running tests matching '${TEST_ARGS[*]}' on Linux ARM64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." docker run \ @@ -101,5 +101,5 @@ else -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web && cargo test "$@"' sh "${TEST_ARGS[@]}" + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test "$@"' sh "${TEST_ARGS[@]}" fi diff --git a/scripts/test-linux-x86_64.sh b/scripts/test-linux-x86_64.sh index a5aa94fb63..948659f968 100755 --- a/scripts/test-linux-x86_64.sh +++ b/scripts/test-linux-x86_64.sh @@ -69,8 +69,8 @@ trap cleanup EXIT INT TERM # Run tests with the project mounted as a volume. Build the bridge staticlib # crates first so libelephc_tls.a / libelephc_pdo.a / libelephc_crypto.a / -# libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a -# exist in the target dir — +# libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a / +# libelephc_magician.a exist in the target dir — # `cargo test` alone never emits the staticlib crate-type. if [ "$TEST_ARG_COUNT" -eq 0 ]; then echo "Running all tests on Linux x86_64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." @@ -86,7 +86,7 @@ if [ "$TEST_ARG_COUNT" -eq 0 ]; then -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web && cargo test' + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test' else echo "Running tests matching '${TEST_ARGS[*]}' on Linux x86_64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." docker run \ @@ -101,5 +101,5 @@ else -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web && cargo test "$@"' sh "${TEST_ARGS[@]}" + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test "$@"' sh "${TEST_ARGS[@]}" fi diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 151f3928e7..e358a3934d 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -74,8 +74,17 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "RecursiveIteratorIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", + "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", + "ReflectionParameter", "ReflectionProperty", + "ReflectionUnionType", + "ReflectionIntersectionType", "RuntimeException", "SeekableIterator", "SortDirection", diff --git a/src/autoload/walk.rs b/src/autoload/walk.rs index a16b661cb9..f8a8dcd84b 100644 --- a/src/autoload/walk.rs +++ b/src/autoload/walk.rs @@ -558,6 +558,17 @@ fn collect_refs_expr(expr: &Expr, out: &mut HashSet) { collect_refs_expr(a, out); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_refs_expr(object, out); + collect_refs_expr(method, out); + for a in args { + collect_refs_expr(a, out); + } + } ExprKind::NewScopedObject { receiver, args } => { collect_static_receiver(receiver, out); for a in args { diff --git a/src/bin/gen_builtins.rs b/src/bin/gen_builtins.rs deleted file mode 100644 index c40890ceec..0000000000 --- a/src/bin/gen_builtins.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Purpose: -//! Standalone tool that prints the single-source PHP builtin registry as documentation JSON. -//! -//! Called from: -//! - `cargo run --bin gen_builtins` (documentation generation / CI docs export). -//! -//! Key details: -//! - Delegates all logic to `elephc::builtins::docs`; this binary only serializes the value to -//! pretty JSON on stdout. -//! - `--include-internal` also emits `internal: true` builtins (the docs pipeline renders -//! compiler-internals pages for the `__elephc_*` helpers). Without it, only the PHP-visible -//! surface is emitted. - -/// Prints the builtin documentation JSON (pretty-printed) to stdout. -/// -/// Emits the PHP-visible builtin surface by default; pass `--include-internal` to also include -/// `internal` builtins (used by the documentation generator). -fn main() { - let include_internal = std::env::args().any(|a| a == "--include-internal"); - let value = if include_internal { - elephc::builtins::docs::export_builtins_json_all() - } else { - elephc::builtins::docs::export_builtins_json() - }; - let json = serde_json::to_string_pretty(&value).expect("serialize builtins JSON"); - println!("{}", json); -} diff --git a/src/builtin_metadata.rs b/src/builtin_metadata.rs new file mode 100644 index 0000000000..fbab246f2d --- /dev/null +++ b/src/builtin_metadata.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Public builtin metadata snapshots used by parity tests and external audits. +//! Keeps catalog and call-signature details observable without duplicating +//! builtin lists outside the compiler-owned sources of truth. +//! +//! Called from: +//! - Rust integration tests that compare `elephc` and `elephc-magician` builtin support. +//! +//! Key details: +//! - Names come from the checker builtin catalog. +//! - Signature shapes are derived from `FunctionSig`, not maintained by hand. + +/// A compact, comparison-friendly view of a builtin call signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinSignatureMetadata { + /// Parameter names in PHP call order. + pub params: Vec, + /// Number of leading parameters that must be supplied positionally or by name. + pub required_param_count: usize, + /// Number of parameters that carry explicit default values. + pub default_param_count: usize, + /// Name of the variadic parameter, when the builtin accepts one. + pub variadic: Option, + /// Parameter names that must be passed by reference. + pub by_ref_params: Vec, +} + +/// Returns the compiler's PHP-visible builtin names. +pub fn php_visible_builtin_names() -> &'static [&'static str] { + static NAMES: std::sync::OnceLock<&'static [&'static str]> = std::sync::OnceLock::new(); + NAMES.get_or_init(|| { + let names = crate::types::checker::builtins::supported_builtin_function_names(); + Box::leak(names.into_boxed_slice()) + }) +} + +/// Returns comparison metadata for one builtin signature, when the compiler tracks it. +pub fn builtin_signature_metadata(name: &str) -> Option { + let canonical = crate::names::php_symbol_key(name.trim_start_matches('\\')); + let sig = crate::types::builtin_call_sig(&canonical)?; + let params = sig + .params + .iter() + .map(|(name, _)| name.clone()) + .collect::>(); + let required_param_count = sig + .defaults + .iter() + .position(Option::is_some) + .unwrap_or(sig.params.len()); + let default_param_count = sig.defaults.iter().filter(|default| default.is_some()).count(); + let by_ref_params = sig + .params + .iter() + .zip(sig.ref_params.iter()) + .filter_map(|((name, _), is_ref)| is_ref.then(|| name.clone())) + .collect::>(); + + Some(BuiltinSignatureMetadata { + params, + required_param_count, + default_param_count, + variadic: sig.variadic, + by_ref_params, + }) +} diff --git a/src/builtins/array/uasort.rs b/src/builtins/array/uasort.rs index f509184416..a0063388f3 100644 --- a/src/builtins/array/uasort.rs +++ b/src/builtins/array/uasort.rs @@ -48,6 +48,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { if let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -58,6 +59,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_closure_type_with_param_hints( params, variadic, + *variadic_by_ref, return_type, body, captures, diff --git a/src/builtins/array/usort.rs b/src/builtins/array/usort.rs index 9f45cd533f..5197ad5b9f 100644 --- a/src/builtins/array/usort.rs +++ b/src/builtins/array/usort.rs @@ -48,6 +48,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { if let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -58,6 +59,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_closure_type_with_param_hints( params, variadic, + *variadic_by_ref, return_type, body, captures, diff --git a/src/builtins/callables/support.rs b/src/builtins/callables/support.rs index 52655f991f..36bad54b07 100644 --- a/src/builtins/callables/support.rs +++ b/src/builtins/callables/support.rs @@ -31,17 +31,10 @@ pub(crate) fn check_class_like_exists(cx: &mut BuiltinCheckCtx) -> Result Result Result { let first_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let dynamic_eval_target = cx.checker.eval_barrier_active + && matches!(first_ty.codegen_repr(), PhpType::Mixed | PhpType::Str); if !matches!(first_ty, PhpType::Object(_)) && !matches!(cx.args[0].kind, ExprKind::StringLiteral(_)) + && !dynamic_eval_target { return Err(CompileError::new( cx.span, diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs index c0522d0b12..d8f14becf7 100644 --- a/src/builtins/docs.rs +++ b/src/builtins/docs.rs @@ -3,7 +3,7 @@ //! Every PHP-visible registered builtin is emitted as one object; internal builtins are skipped. //! //! Called from: -//! - `src/bin/gen_builtins.rs` via `elephc::builtins::docs::export_builtins_json()`. +//! - `tools/gen_builtins.rs` (example target) via `elephc::builtins::docs::export_builtins_json()`. //! //! Key details: //! - Uses `crate::builtins::registry::{names, lookup}` so `inventory::iter` runs in the same @@ -69,6 +69,14 @@ fn default_spec_json(default: &DefaultSpec) -> Value { } } +/// Returns true when a PHP-visible builtin exists in the static AOT surface: +/// `builtin!` registry entries plus compiler-resident constructs (`isset`, +/// `strval`, predicate aliases, ...). Documentation tooling uses this to tell +/// resident names apart from genuinely eval-only builtins. +pub fn aot_php_visible_builtin_exists(name: &str) -> bool { + crate::types::checker::builtins::is_php_visible_builtin_function(name) +} + /// Builds the documentation JSON array for every PHP-visible registered builtin. /// /// Iterates the registry in sorted name order, skips `internal` builtins, and emits one object per diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 5584e29f4a..2df2faf9be 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -165,6 +165,8 @@ pub fn function_sig(name: &str) -> Option { let def = lookup(name)?; Some(FunctionSig { params: def.params.clone(), + param_type_exprs: vec![None; def.params.len()], + param_attributes: vec![Vec::new(); def.params.len()], defaults: def.defaults.clone(), return_type: def.return_type.clone(), declared_return: false, @@ -194,6 +196,13 @@ pub fn first_class_callable_sig(name: &str) -> Option { let mut fcc_sig = callable_wrapper_sig(&sig); refine_first_class_callable_sig(name, &mut fcc_sig); fcc_sig.declared_return = true; + // Mark params declared for reflection hasType, but keep by-ref params + // undeclared: their registry type is Mixed by generality, and a declared + // Mixed by-ref param would make the checker demand boxed storage from + // every argument variable (regressing e.g. `sort(...)` on plain arrays). + fcc_sig.declared_params = (0..fcc_sig.params.len()) + .map(|index| !fcc_sig.ref_params.get(index).copied().unwrap_or(false)) + .collect(); Some(fcc_sig) } diff --git a/src/builtins/system/attr_support.rs b/src/builtins/system/attr_support.rs index b07df26dae..bc996b6c13 100644 --- a/src/builtins/system/attr_support.rs +++ b/src/builtins/system/attr_support.rs @@ -50,33 +50,28 @@ pub(crate) fn class_attribute_args_unsupported( .enumerate() .find(|(_, name)| php_symbol_key(name.trim_start_matches('\\')) == attr_key) .is_some_and(|(idx, _)| match class_info.attribute_args.get(idx) { - // The flat `class_attribute_args()` helper returns a positional - // array of materialized scalars, so it cannot faithfully echo keyed - // arguments (named arguments or associative arrays, at any depth) or - // deferred symbolic references (global/class constants, enum cases). - // Reject them and direct users to - // `ReflectionClass::getAttributes()->getArguments()` instead. + // The flat `class_attribute_args()` helper can materialize literal + // positional, named, and array-keyed arguments. It cannot resolve + // deferred symbolic references (global/class constants, enum cases) + // on this echo path, so those stay rejected. Some(Some(entries)) => attr_entries_unsupported_by_flat_helper(entries), _ => true, }) } /// Returns true when the flat `class_attribute_args()` helper cannot faithfully -/// echo the captured entries: keyed arguments (named arguments or -/// associative-array keys, at any depth) would lose their keys, and deferred -/// symbolic references (global/class constants, enum cases) are not materialized -/// on this echo path. Both are supported through -/// `ReflectionClass::getAttributes()->getArguments()` instead. +/// echo the captured entries: deferred symbolic references (global/class +/// constants, enum cases) are not materialized on this echo path. Literal keyed +/// arguments are supported and retain their keys. pub(crate) fn attr_entries_unsupported_by_flat_helper( entries: &[crate::types::AttrArgEntry], ) -> bool { entries.iter().any(|entry| { - entry.key.is_some() - || matches!( - &entry.value, - crate::types::AttrArgValue::ConstRef(_) - | crate::types::AttrArgValue::ScopedConst(..) - ) + matches!( + &entry.value, + crate::types::AttrArgValue::ConstRef(_) + | crate::types::AttrArgValue::ScopedConst(..) + ) || matches!( &entry.value, crate::types::AttrArgValue::Array(inner) diff --git a/src/codegen/block_emit.rs b/src/codegen/block_emit.rs index cb80d7355a..c6198c5501 100644 --- a/src/codegen/block_emit.rs +++ b/src/codegen/block_emit.rs @@ -801,11 +801,11 @@ fn is_main(function: &Function) -> bool { /// Emits global singleton objects for enum cases used by EIR user code. fn emit_enum_singleton_initializers(ctx: &mut FunctionContext<'_>) { - let allowed_class_names = super::runtime_referenced_class_names(ctx.module); + let allowed_enum_names = super::runtime_referenced_enum_singleton_names(ctx.module); let mut sorted_enums = ctx.module.enum_infos.iter().collect::>(); sorted_enums.sort_by_key(|(name, _)| name.as_str()); for (enum_name, enum_info) in sorted_enums { - if !allowed_class_names.contains(enum_name) { + if !allowed_enum_names.contains(enum_name) { continue; } let Some(class_info) = ctx.module.class_infos.get(enum_name) else { diff --git a/src/codegen/context.rs b/src/codegen/context.rs index a53b46dcac..44e821727d 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -406,6 +406,17 @@ impl<'a> FunctionContext<'a> { Ok(()) } + /// Stores the current result register(s) directly into an addressable local slot. + pub(super) fn store_current_result_to_local(&mut self, slot: LocalSlotId) -> Result<()> { + let target_ty = self.local_php_type(slot)?; + if self.local_stores_ref_cell_pointer(slot) { + return self.store_current_result_to_ref_cell_local(slot, &target_ty); + } + let offset = self.local_offset(slot)?; + self.store_current_result_at_offset(&target_ty, offset); + Ok(()) + } + /// After an in-place hash/array mutation whose runtime helper returns the /// possibly-reallocated container pointer in `value`'s register (already /// persisted via `store_result_value`), writes that pointer back to global @@ -481,6 +492,56 @@ impl<'a> FunctionContext<'a> { Ok(()) } + /// Stores the current result register(s) through a local ref-cell pointer slot. + fn store_current_result_to_ref_cell_local( + &mut self, + slot: LocalSlotId, + target_ty: &PhpType, + ) -> Result<()> { + reject_multiword_ref_cell_local(target_ty, "store")?; + let offset = self.local_offset(slot)?; + let pointer_reg = abi::symbol_scratch_reg(self.emitter); + abi::load_at_offset(self.emitter, pointer_reg, offset); + match target_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(self.emitter); + abi::emit_store_to_address(self.emitter, ptr_reg, pointer_reg, 0); + abi::emit_store_to_address(self.emitter, len_reg, pointer_reg, 8); + } + PhpType::Float => { + abi::emit_store_to_address( + self.emitter, + abi::float_result_reg(self.emitter), + pointer_reg, + 0, + ); + } + PhpType::TaggedScalar => { + abi::emit_store_to_address( + self.emitter, + abi::int_result_reg(self.emitter), + pointer_reg, + 0, + ); + abi::emit_store_to_address( + self.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(self.emitter), + pointer_reg, + 8, + ); + } + _ => { + abi::emit_store_to_address( + self.emitter, + abi::int_result_reg(self.emitter), + pointer_reg, + 0, + ); + } + } + Ok(()) + } + /// Stores the current result register(s) into a frame offset. fn store_current_result_at_offset(&mut self, ty: &PhpType, offset: usize) { match &ty.codegen_repr() { @@ -549,6 +610,9 @@ impl<'a> FunctionContext<'a> { | Op::Call | Op::FunctionVariantCall | Op::BuiltinCall + | Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalConstantFetch | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs new file mode 100644 index 0000000000..5998c73ee1 --- /dev/null +++ b/src/codegen/eval_callable_helpers.rs @@ -0,0 +1,1891 @@ +//! Purpose: +//! Builds callable descriptor support reused by eval-to-native constructor and +//! method bridges. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()`. +//! - `crate::codegen::eval_constructor_helpers` and +//! `crate::codegen::eval_method_helpers`. +//! +//! Key details: +//! - Eval stores PHP callbacks as boxed runtime values, while AOT callable +//! parameters expect descriptor pointers. +//! - String callables and static callable arrays select static descriptors with +//! uniform invokers generated by the existing callable dispatch machinery. + +use crate::codegen::abi; +use crate::codegen::callable_descriptor::{ + self, CallableDescriptorInvocation, CallableDescriptorShape, +}; +use std::collections::HashSet; + +use crate::codegen::callable_dispatch::{ + self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeStaticMethodCallableCase, +}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::runtime_callable_invoker::RuntimeCallableInvoker; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{function_symbol, method_symbol, php_symbol_key, static_method_symbol}; +use crate::parser::ast::Visibility; +use crate::types::{callable_wrapper_sig, FunctionSig, PhpType}; + +const EVAL_RECEIVER_CAPTURE_PARAM: &str = "__elephc_eval_callable_receiver"; +const MIXED_METHOD_TAG_OFFSET: usize = 0; +const MIXED_METHOD_PAYLOAD_OFFSET: usize = 16; +const MIXED_RECEIVER_TAG_OFFSET: usize = 32; +const MIXED_RECEIVER_PAYLOAD_OFFSET: usize = 48; +const MIXED_SELECTOR_BYTES: usize = 64; +const SAVED_OBJECT_RECEIVER_BYTES: usize = 16; +const MIXED_TAG_STRING: i64 = 1; +const MIXED_TAG_OBJECT: i64 = 6; +const EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL: &str = "__elephc_eval_dynamic_callable_invoker"; +const EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL: &str = "__elephc_eval_dynamic_callable_entry"; +const EVAL_DYNAMIC_CONTEXT_CAPTURE: usize = 0; +const EVAL_DYNAMIC_CALLBACK_CAPTURE: usize = 1; +const EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES: usize = 32; +const AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET: i64 = 16; + +/// Callable descriptors available to eval constructor and method bridges. +pub(super) struct EvalCallableDescriptorSupport { + string_cases: Vec, + instance_array_cases: Vec, + static_array_cases: Vec, + object_cases: Vec, + dynamic_descriptor_label: Option, +} + +/// Runtime instance-method callable case emitted specifically for eval bridges. +#[derive(Clone)] +struct EvalInstanceMethodCallableCase { + class_id: u64, + method_name: String, + case: RuntimeCallableCase, +} + +/// Receiver-bound callable descriptor shape used by eval bridge metadata. +#[derive(Clone, Copy)] +enum EvalInstanceCallableShape { + InstanceMethod, + ObjectInvoke, +} + +/// Stable label allocator for eval bridge helper bodies emitted outside `FunctionContext`. +struct EvalCallableEmitState { + next_id: usize, +} + +impl EvalCallableEmitState { + /// Creates an empty label state for one eval callable-support emission pass. + fn new() -> Self { + Self { next_id: 0 } + } + + /// Returns a unique global/local label for generated eval callable support. + fn next_label(&mut self, prefix: &str) -> String { + let id = self.next_id; + self.next_id += 1; + format!("__elephc_eval_{}_{}", prefix, id) + } +} + +impl EvalCallableDescriptorSupport { + /// Returns true when no callable descriptor case is available. + pub(super) fn is_empty(&self) -> bool { + self.string_cases.is_empty() + && self.instance_array_cases.is_empty() + && self.static_array_cases.is_empty() + && self.object_cases.is_empty() + } + + /// Returns true when no callable-array descriptor case is available. + fn array_cases_empty(&self) -> bool { + self.instance_array_cases.is_empty() && self.static_array_cases.is_empty() + } + + /// Returns true when no invokable-object descriptor case is available. + fn object_cases_empty(&self) -> bool { + self.object_cases.is_empty() + } + + /// Returns true when eval callback fallback descriptors can be materialized. + fn has_dynamic_descriptor(&self) -> bool { + self.dynamic_descriptor_label.is_some() + } +} + +/// Returns true when eval bridges may need to turn PHP callbacks into descriptors. +pub(super) fn module_needs_eval_callable_descriptor_support(module: &Module) -> bool { + module_uses_eval(module) && module_has_callable_aot_method_param(module) +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns true when any generated class callable exposed to eval accepts `callable`. +fn module_has_callable_aot_method_param(module: &Module) -> bool { + module.class_infos.values().any(|class_info| { + class_info.methods.values().any(signature_has_callable_param) + || class_info + .static_methods + .values() + .any(signature_has_callable_param) + }) +} + +/// Returns true when one signature contains a callable ABI parameter. +fn signature_has_callable_param(signature: &crate::types::FunctionSig) -> bool { + signature + .params + .iter() + .any(|(_, ty)| ty.codegen_repr() == PhpType::Callable) +} + +/// Emits shared runtime callable wrappers/invokers and returns descriptor cases. +pub(super) fn emit_eval_callable_descriptor_support( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + needed: bool, +) -> EvalCallableDescriptorSupport { + if !needed { + return EvalCallableDescriptorSupport { + string_cases: Vec::new(), + instance_array_cases: Vec::new(), + static_array_cases: Vec::new(), + object_cases: Vec::new(), + dynamic_descriptor_label: None, + }; + } + let mut state = EvalCallableEmitState::new(); + let string_cases = eval_user_function_callable_cases(module, emitter, data, &mut state); + let instance_array_cases = eval_instance_method_callable_cases(module, emitter, data, &mut state); + let static_array_cases = eval_static_method_callable_cases(module, emitter, data, &mut state); + let object_cases = eval_invokable_object_callable_cases(module, emitter, data, &mut state); + let dynamic_descriptor_label = Some(eval_dynamic_callable_descriptor(data)); + emit_eval_dynamic_callable_invoker(module, emitter, data); + EvalCallableDescriptorSupport { + string_cases, + instance_array_cases, + static_array_cases, + object_cases, + dynamic_descriptor_label, + } +} + +/// Emits the static descriptor template for eval-owned callback values. +fn eval_dynamic_callable_descriptor(data: &mut DataSection) -> String { + let captures = vec![ + ( + "__elephc_eval_callable_context".to_string(), + PhpType::Int, + false, + ), + ( + "__elephc_eval_callable_value".to_string(), + PhpType::Mixed, + false, + ), + ]; + callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL, + Some("eval callback"), + callable_descriptor::CALLABLE_DESC_KIND_CALLBACK_ADAPTER, + None, + &captures, + &[], + CallableDescriptorInvocation::new(CallableDescriptorShape::CallbackAdapter), + Some(EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL), + ) +} + +/// Emits the uniform invoker for descriptors that capture an eval callback value. +fn emit_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + emitter.blank(); + emitter.comment("--- eval bridge: dynamic callable descriptor invoker ---"); + emitter.label(EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL); + emitter.instruction("ret"); // direct entry calls are never used for eval dynamic descriptors + emitter.label_global(EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL); + match module.target.arch { + crate::codegen::platform::Arch::AArch64 => { + emit_aarch64_eval_dynamic_callable_invoker(module, emitter, data); + } + crate::codegen::platform::Arch::X86_64 => { + emit_x86_64_eval_dynamic_callable_invoker(module, emitter, data); + } + } +} + +/// Emits the ARM64 body for eval dynamic callable descriptor invocation. +fn emit_aarch64_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + let throwable_label = "__elephc_eval_dynamic_callable_throwable"; + let fatal_label = "__elephc_eval_dynamic_callable_fatal"; + let done_label = "__elephc_eval_dynamic_callable_done"; + let symbol = module + .target + .extern_symbol("__elephc_eval_callable_call_array"); + emitter.instruction("sub sp, sp, #64"); // reserve descriptor, arg-array, eval result, and saved frame slots + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the caller frame around the Rust callback + emitter.instruction("add x29, sp, #48"); // establish a stable invoker frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the runtime callable descriptor pointer + emitter.instruction("str x1, [sp, #8]"); // save the boxed Mixed invoker argument array + emitter.instruction("str xzr, [sp, #16]"); // clear result kind and padding before the FFI call + emitter.instruction("str xzr, [sp, #24]"); // clear result value pointer before the FFI call + emitter.instruction("str xzr, [sp, #32]"); // clear result error pointer before the FFI call + emitter.instruction("ldr x9, [sp, #0]"); // reload descriptor before reading eval captures + emitter.instruction(&format!( + "ldr x0, [x9, #{}]", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE) + )); // pass the captured eval context as FFI argument 1 + emitter.instruction(&format!( + "ldr x1, [x9, #{}]", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE) + )); // pass the captured eval callback as FFI argument 2 + emitter.instruction("ldr x2, [sp, #8]"); // pass the boxed Mixed invoker argument array + emitter.instruction("add x3, sp, #16"); // pass writable eval result storage + abi::emit_call_label(emitter, &symbol); + emitter.instruction("cmp w0, #0"); // did magician complete callback dispatch successfully? + emitter.instruction(&format!("b.eq {}", done_label)); // return the boxed result on success + emitter.instruction("cmp w0, #3"); // status 3 means an eval Throwable escaped + emitter.instruction(&format!("b.eq {}", throwable_label)); // publish the Throwable through native unwinding + emitter.label(fatal_label); + emit_aarch64_eval_dynamic_callable_fatal(emitter, data); + emitter.label(throwable_label); + emitter.instruction("ldr x0, [sp, #32]"); // load the boxed Throwable returned by magician + emitter.instruction("bl __rt_mixed_unbox"); // expose the Throwable object payload + abi::emit_store_reg_to_symbol(emitter, "x1", "_exc_value", 0); + emitter.instruction("bl __rt_throw_current"); // enter the native Throwable unwinder + emitter.label(done_label); + emitter.instruction("ldr x0, [sp, #24]"); // return the boxed Mixed callback result + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the caller frame + emitter.instruction("add sp, sp, #64"); // release invoker scratch storage + emitter.instruction("ret"); // return to the descriptor invoker caller +} + +/// Emits the x86_64 body for eval dynamic callable descriptor invocation. +fn emit_x86_64_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + let throwable_label = "__elephc_eval_dynamic_callable_throwable_x"; + let fatal_label = "__elephc_eval_dynamic_callable_fatal_x"; + let done_label = "__elephc_eval_dynamic_callable_done_x"; + let symbol = module + .target + .extern_symbol("__elephc_eval_callable_call_array"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable invoker frame pointer + emitter.instruction("sub rsp, 64"); // reserve descriptor, arg-array, and eval result slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the runtime callable descriptor pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the boxed Mixed invoker argument array + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear result kind and padding before the FFI call + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // clear result value pointer before the FFI call + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // clear result error pointer before the FFI call + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload descriptor before reading eval captures + emitter.instruction(&format!( + "mov rdi, QWORD PTR [r9 + {}]", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE) + )); // pass the captured eval context as FFI argument 1 + emitter.instruction(&format!( + "mov rsi, QWORD PTR [r9 + {}]", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE) + )); // pass the captured eval callback as FFI argument 2 + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // pass the boxed Mixed invoker argument array + emitter.instruction("lea rcx, [rbp - 48]"); // pass writable eval result storage + abi::emit_call_label(emitter, &symbol); + emitter.instruction("test eax, eax"); // did magician complete callback dispatch successfully? + emitter.instruction(&format!("je {}", done_label)); // return the boxed result on success + emitter.instruction("cmp eax, 3"); // status 3 means an eval Throwable escaped + emitter.instruction(&format!("je {}", throwable_label)); // publish the Throwable through native unwinding + emitter.label(fatal_label); + emit_x86_64_eval_dynamic_callable_fatal(emitter, data); + emitter.label(throwable_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // load the boxed Throwable returned by magician + emitter.instruction("call __rt_mixed_unbox"); // expose the Throwable object payload + abi::emit_store_reg_to_symbol(emitter, "rdi", "_exc_value", 0); + emitter.instruction("call __rt_throw_current"); // enter the native Throwable unwinder + emitter.label(done_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the boxed Mixed callback result + emitter.instruction("mov rsp, rbp"); // discard invoker scratch storage + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the descriptor invoker caller +} + +/// Returns the byte offset for one dynamic eval callable descriptor capture. +fn dynamic_capture_offset(index: usize) -> usize { + callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + index * 16 +} + +/// Emits an ARM64 fatal diagnostic for failed eval callback dispatch. +fn emit_aarch64_eval_dynamic_callable_fatal(emitter: &mut Emitter, data: &mut DataSection) { + let (message_label, message_len) = + data.add_string(b"Fatal error: eval callback dispatch failed\n"); + emitter.instruction("mov x0, #2"); // write the eval callback diagnostic to stderr + emitter.adrp("x1", &message_label); + emitter.add_lo12("x1", "x1", &message_label); + emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the diagnostic byte length to write + emitter.syscall(4); + abi::emit_exit(emitter, 1); +} + +/// Emits an x86_64 fatal diagnostic for failed eval callback dispatch. +fn emit_x86_64_eval_dynamic_callable_fatal(emitter: &mut Emitter, data: &mut DataSection) { + let (message_label, message_len) = + data.add_string(b"Fatal error: eval callback dispatch failed\n"); + emitter.instruction("mov edi, 2"); // write the eval callback diagnostic to stderr + abi::emit_symbol_address(emitter, "rsi", &message_label); + emitter.instruction(&format!("mov edx, {}", message_len)); // pass the diagnostic byte length to write + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // emit the fatal diagnostic before terminating + abi::emit_exit(emitter, 1); +} + +/// Builds descriptor cases for user functions visible as eval string callables. +fn eval_user_function_callable_cases( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, +) -> Vec { + let mut functions = eval_user_function_sigs(module); + functions.sort_by(|left, right| left.0.cmp(&right.0)); + let mut cases = Vec::with_capacity(functions.len()); + for (name, sig) in functions { + let case_sig = callable_wrapper_sig(&sig); + let invoker_label = + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &case_sig, &[]); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &function_symbol(&name), + Some(&name), + callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + Some(&case_sig), + &[], + &[], + CallableDescriptorInvocation::named(CallableDescriptorShape::Function, &name), + Some(&invoker_label), + ); + cases.push(RuntimeCallableCase { + label: function_symbol(&name), + descriptor_label, + php_name: Some(name), + }); + } + cases +} + +/// Collects user function signatures available to eval string-callable descriptors. +fn eval_user_function_sigs(module: &Module) -> Vec<(String, FunctionSig)> { + let mut functions = module + .functions + .iter() + .filter(|function| { + !function.flags.is_main && !function.name.starts_with("_class_propinit_") + }) + .map(|function| { + ( + function.name.clone(), + super::lower_inst::function_signature_from_eir(function), + ) + }) + .collect::>(); + for group in super::function_variants::collect_dispatch_groups(module) { + if functions.iter().any(|(name, _)| name == &group.name) { + continue; + } + if let Some(function) = super::function_variants::variant_callee_for_group(module, &group.name) + { + functions.push(( + group.name.clone(), + super::lower_inst::function_signature_from_eir(function), + )); + } + } + functions +} + +/// Builds descriptor cases for emitted public instance methods visible to eval. +fn eval_instance_method_callable_cases( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, +) -> Vec { + eval_instance_callable_cases( + module, + emitter, + data, + state, + EvalInstanceCallableShape::InstanceMethod, + |_| true, + ) +} + +/// Builds descriptor cases for emitted public `__invoke` methods visible to eval. +fn eval_invokable_object_callable_cases( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, +) -> Vec { + eval_instance_callable_cases( + module, + emitter, + data, + state, + EvalInstanceCallableShape::ObjectInvoke, + |method_name| php_symbol_key(method_name) == "__invoke", + ) +} + +/// Builds receiver-bound descriptor cases for public emitted instance methods. +fn eval_instance_callable_cases( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, + shape: EvalInstanceCallableShape, + include_method: impl Fn(&str) -> bool, +) -> Vec { + let emitted_methods = eval_eir_class_method_keys(module); + let mut candidates = Vec::new(); + for (class_name, class_info) in &module.class_infos { + for (method_name, sig) in &class_info.methods { + if !include_method(method_name) { + continue; + } + if !class_info + .method_visibilities + .get(method_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Public)) + { + continue; + } + let method_key = php_symbol_key(method_name); + let impl_class = class_info + .method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + if emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { + candidates.push(( + class_name.clone(), + class_info.class_id, + method_name.clone(), + method_key, + impl_class.to_string(), + sig.clone(), + )); + } + } + } + candidates.sort_by(|left, right| (&left.0, &left.2).cmp(&(&right.0, &right.2))); + candidates + .into_iter() + .map( + |(class_name, class_id, method_name, method_key, impl_class, sig)| { + let case = receiver_bound_instance_method_case( + emitter, + data, + state, + &class_name, + &impl_class, + &method_name, + &method_key, + &sig, + shape, + ); + EvalInstanceMethodCallableCase { + class_id, + method_name, + case, + } + }, + ) + .collect() +} + +/// Builds descriptor cases for emitted public static methods visible to eval. +fn eval_static_method_callable_cases( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, +) -> Vec { + let emitted_methods = eval_eir_class_method_keys(module); + let mut candidates = Vec::new(); + for (class_name, class_info) in &module.class_infos { + for (method_name, sig) in &class_info.static_methods { + if !class_info + .static_method_visibilities + .get(method_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Public)) + { + continue; + } + let method_key = php_symbol_key(method_name); + let impl_class = class_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + if emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), true)) { + candidates.push(( + class_name.clone(), + method_name.clone(), + method_key, + impl_class.to_string(), + class_info.class_id, + sig.clone(), + )); + } + } + } + candidates.sort_by(|left, right| (&left.0, &left.1).cmp(&(&right.0, &right.1))); + candidates + .into_iter() + .map( + |(class_name, method_name, method_key, impl_class, class_id, sig)| { + let wrapper_sig = callable_dispatch::static_method_runtime_wrapper_sig(&sig); + let entry_label = emit_eval_static_method_descriptor_entry_wrapper( + emitter, + state, + &impl_class, + &method_key, + &wrapper_sig, + class_id, + ); + let php_name = format!("{}::{}", class_name, method_name); + let invoker_label = + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &wrapper_sig, &[]); + let descriptor_label = + callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &entry_label, + Some(&php_name), + callable_descriptor::CALLABLE_DESC_KIND_STATIC_METHOD, + Some(&wrapper_sig), + &[], + &[], + CallableDescriptorInvocation::method( + CallableDescriptorShape::StaticMethod, + Some(class_name.clone()), + method_name.as_str(), + ), + Some(&invoker_label), + ); + RuntimeStaticMethodCallableCase { + class_name, + method_name, + case: RuntimeCallableCase { + label: entry_label, + descriptor_label, + php_name: Some(php_name), + }, + } + }, + ) + .collect() +} + +/// Returns class-method symbols backed by lowered EIR functions. +fn eval_eir_class_method_keys(module: &Module) -> HashSet<(String, String, bool)> { + module + .class_methods + .iter() + .filter_map(|function| { + let (class_name, method_name) = function.name.rsplit_once("::")?; + Some(( + class_name.to_string(), + php_symbol_key(method_name), + function.flags.is_static, + )) + }) + .collect() +} + +/// Builds a receiver-bound descriptor case for one public instance method. +#[allow(clippy::too_many_arguments)] +fn receiver_bound_instance_method_case( + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, + class_name: &str, + impl_class: &str, + method_name: &str, + method_key: &str, + sig: &FunctionSig, + shape: EvalInstanceCallableShape, +) -> RuntimeCallableCase { + let case_sig = callable_wrapper_sig(sig); + let hidden_name = unique_hidden_param(EVAL_RECEIVER_CAPTURE_PARAM, &case_sig); + let capture_ty = PhpType::Object(class_name.to_string()); + let captures = vec![(hidden_name.clone(), capture_ty.clone(), false)]; + let entry_label = emit_eval_instance_method_descriptor_entry_wrapper( + emitter, + state, + impl_class, + method_key, + &case_sig, + ); + let invoker_label = + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &case_sig, &captures); + let (kind, invocation_shape) = match shape { + EvalInstanceCallableShape::ObjectInvoke => ( + callable_descriptor::CALLABLE_DESC_KIND_OBJECT_INVOKE, + CallableDescriptorShape::ObjectInvoke, + ), + EvalInstanceCallableShape::InstanceMethod => ( + callable_descriptor::CALLABLE_DESC_KIND_INSTANCE_METHOD, + CallableDescriptorShape::InstanceMethod, + ), + }; + let php_name = format!("{}::{}", class_name, method_name); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &entry_label, + Some(&php_name), + kind, + Some(&case_sig), + &captures, + &captures, + CallableDescriptorInvocation::method( + invocation_shape, + Some(class_name.to_string()), + method_name, + ), + Some(&invoker_label), + ); + RuntimeCallableCase { + label: entry_label, + descriptor_label, + php_name: Some(php_name), + } +} + +/// Emits a descriptor invoker inline and branches around its global entry body. +fn emit_eval_runtime_callable_invoker_inline( + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, + sig: &FunctionSig, + captures: &[(String, PhpType, bool)], +) -> String { + let label = state.next_label("callable_invoker"); + let done_label = state.next_label("callable_invoker_done"); + let invoker = RuntimeCallableInvoker { + label: &label, + sig, + captures, + }; + abi::emit_jump(emitter, &done_label); + super::runtime_callable_invoker::emit_runtime_callable_invoker(emitter, data, &invoker); + emitter.label(&done_label); + label +} + +/// Returns a hidden receiver parameter name that cannot collide with visible parameters. +fn unique_hidden_param(base: &str, sig: &FunctionSig) -> String { + if !sig.params.iter().any(|(name, _)| name == base) { + return base.to_string(); + } + let mut index = 0usize; + loop { + let candidate = format!("{}_{}", base, index); + if !sig.params.iter().any(|(name, _)| name == &candidate) { + return candidate; + } + index += 1; + } +} + +/// Emits an entry wrapper that receives visible args followed by the captured receiver. +fn emit_eval_instance_method_descriptor_entry_wrapper( + emitter: &mut Emitter, + state: &mut EvalCallableEmitState, + class_name: &str, + method_key: &str, + sig: &FunctionSig, +) -> String { + let visible_arg_types = descriptor_visible_arg_types(sig); + let wrapper_label = state.next_label("callable_instance_method"); + let done_label = state.next_label("callable_instance_method_done"); + abi::emit_jump(emitter, &done_label); + emitter.label(&wrapper_label); + emit_eval_instance_method_descriptor_entry_wrapper_body( + emitter, + class_name, + method_key, + &visible_arg_types, + ); + emitter.label(&done_label); + wrapper_label +} + +/// Emits an entry wrapper that prepends a concrete called-class id before calling a static method. +fn emit_eval_static_method_descriptor_entry_wrapper( + emitter: &mut Emitter, + state: &mut EvalCallableEmitState, + impl_class: &str, + method_key: &str, + sig: &FunctionSig, + called_class_id: u64, +) -> String { + let visible_arg_types = descriptor_visible_arg_types(sig); + let wrapper_label = state.next_label("static_method_descriptor_entry"); + let done_label = state.next_label("static_method_descriptor_entry_done"); + abi::emit_jump(emitter, &done_label); + emitter.label(&wrapper_label); + emit_eval_static_method_descriptor_entry_wrapper_body( + emitter, + impl_class, + method_key, + &visible_arg_types, + called_class_id, + ); + emitter.label(&done_label); + wrapper_label +} + +/// Returns codegen-representation parameter types for a descriptor entry wrapper. +fn descriptor_visible_arg_types(sig: &FunctionSig) -> Vec { + sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() +} + +/// Emits a descriptor entry wrapper body by reordering visible args after the receiver. +fn emit_eval_instance_method_descriptor_entry_wrapper_body( + emitter: &mut Emitter, + class_name: &str, + method_key: &str, + visible_arg_types: &[PhpType], +) { + let receiver_ty = descriptor_receiver_type(class_name); + let incoming_types = descriptor_entry_incoming_types(visible_arg_types, &receiver_ty); + let actual_types = descriptor_entry_actual_types(visible_arg_types, &receiver_ty); + let incoming_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &incoming_types, 0); + let actual_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); + let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); + let (actual_stack_offsets, actual_overflow_bytes) = + descriptor_entry_stack_offsets(&actual_assignments); + let frame_size = descriptor_entry_frame_size(incoming_types.len()); + + abi::emit_frame_prologue(emitter, frame_size); + for (idx, (ty, assignment)) in incoming_types + .iter() + .zip(incoming_assignments.iter()) + .enumerate() + { + store_descriptor_entry_incoming_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx), + incoming_stack_offsets[idx], + ); + } + if actual_overflow_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + } + for (idx, (ty, assignment)) in actual_types + .iter() + .zip(actual_assignments.iter()) + .enumerate() + { + let source_idx = if idx == 0 { + visible_arg_types.len() + } else { + idx - 1 + }; + load_descriptor_entry_actual_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(source_idx), + actual_stack_offsets[idx], + ); + } + abi::emit_call_label(emitter, &method_symbol(class_name, method_key)); + if actual_overflow_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + } + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); +} + +/// Emits a static descriptor entry wrapper body by prepending the called-class id. +fn emit_eval_static_method_descriptor_entry_wrapper_body( + emitter: &mut Emitter, + impl_class: &str, + method_key: &str, + visible_arg_types: &[PhpType], + called_class_id: u64, +) { + let actual_types = { + let mut types = Vec::with_capacity(visible_arg_types.len() + 1); + types.push(PhpType::Int); + types.extend_from_slice(visible_arg_types); + types + }; + let incoming_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, visible_arg_types, 0); + let actual_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); + let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); + let (actual_stack_offsets, actual_overflow_bytes) = + descriptor_entry_stack_offsets(&actual_assignments); + let frame_size = descriptor_entry_frame_size(visible_arg_types.len()); + + abi::emit_frame_prologue(emitter, frame_size); + for (idx, (ty, assignment)) in visible_arg_types + .iter() + .zip(incoming_assignments.iter()) + .enumerate() + { + store_descriptor_entry_incoming_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx), + incoming_stack_offsets[idx], + ); + } + if actual_overflow_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + } + for (idx, (ty, assignment)) in actual_types + .iter() + .zip(actual_assignments.iter()) + .enumerate() + { + if idx == 0 { + load_descriptor_entry_static_class_id( + emitter, + called_class_id, + assignment, + actual_stack_offsets[idx], + ); + } else { + load_descriptor_entry_actual_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx - 1), + actual_stack_offsets[idx], + ); + } + } + abi::emit_call_label(emitter, &static_method_symbol(impl_class, method_key)); + if actual_overflow_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + } + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); +} + +/// Loads the concrete called-class id into a descriptor wrapper's outgoing ABI slot. +fn load_descriptor_entry_static_class_id( + emitter: &mut Emitter, + class_id: u64, + assignment: &abi::OutgoingArgAssignment, + stack_offset: Option, +) { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + abi::secondary_scratch_reg(emitter) + }; + abi::emit_load_int_immediate(emitter, reg, class_id as i64); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } +} + +/// Returns the runtime receiver type threaded through the descriptor entry wrapper. +fn descriptor_receiver_type(class_name: &str) -> PhpType { + PhpType::Object(class_name.to_string()) +} + +/// Returns the wrapper incoming argument order: visible args followed by receiver. +fn descriptor_entry_incoming_types( + visible_arg_types: &[PhpType], + receiver_ty: &PhpType, +) -> Vec { + let mut types = visible_arg_types.to_vec(); + types.push(receiver_ty.clone()); + types +} + +/// Returns the real method ABI argument order: receiver followed by visible args. +fn descriptor_entry_actual_types( + visible_arg_types: &[PhpType], + receiver_ty: &PhpType, +) -> Vec { + let mut types = Vec::with_capacity(visible_arg_types.len() + 1); + types.push(receiver_ty.clone()); + types.extend_from_slice(visible_arg_types); + types +} + +/// Returns an aligned frame size for descriptor entry wrapper spill slots plus footer. +fn descriptor_entry_frame_size(slot_count: usize) -> usize { + align16((slot_count + 1) * 16) +} + +/// Returns the frame offset for a descriptor entry wrapper spill slot. +fn descriptor_entry_slot_offset(idx: usize) -> usize { + (idx + 1) * 16 +} + +/// Returns the local/outgoing byte size used for one descriptor wrapper argument. +fn descriptor_entry_arg_slot_size(ty: &PhpType) -> usize { + match ty.codegen_repr() { + PhpType::Void | PhpType::Never => 0, + _ => 16, + } +} + +/// Returns stack offsets for ABI assignments that overflow their target registers. +fn descriptor_entry_stack_offsets( + assignments: &[abi::OutgoingArgAssignment], +) -> (Vec>, usize) { + let mut offsets = vec![None; assignments.len()]; + let mut next_offset = 0usize; + for (idx, assignment) in assignments.iter().enumerate() { + if assignment.in_register() { + continue; + } + offsets[idx] = Some(next_offset); + next_offset += descriptor_entry_arg_slot_size(&assignment.ty); + } + (offsets, next_offset) +} + +/// Converts a descriptor overflow offset into a caller-stack frame offset. +fn descriptor_entry_caller_stack_offset(emitter: &Emitter, stack_offset: usize) -> usize { + let cursor = abi::IncomingArgCursor::for_target(emitter.target, 0); + cursor.caller_stack_offset + stack_offset +} + +/// Returns integer scratch registers that cannot overlap live descriptor argument registers. +fn descriptor_entry_int_spill_pair(emitter: &Emitter) -> (&'static str, &'static str) { + let lo_reg = abi::secondary_scratch_reg(emitter); + let hi_reg = match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => abi::tertiary_scratch_reg(emitter), + crate::codegen::platform::Arch::X86_64 => "r11", + }; + (lo_reg, hi_reg) +} + +/// Stores one incoming descriptor entry argument into its spill slot. +fn store_descriptor_entry_incoming_arg( + emitter: &mut Emitter, + ty: &PhpType, + assignment: &abi::OutgoingArgAssignment, + offset: usize, + stack_offset: Option, +) { + match ty.codegen_repr() { + PhpType::Float => { + let reg = if assignment.in_register() { + abi::float_arg_reg_name(emitter.target, assignment.start_reg) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let spill_reg = match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => "d15", + crate::codegen::platform::Arch::X86_64 => "xmm15", + }; + abi::load_from_caller_stack(emitter, spill_reg, caller_offset); + spill_reg + }; + abi::store_at_offset(emitter, reg, offset); + } + PhpType::Str => { + let (ptr_reg, len_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let (ptr_spill_reg, len_spill_reg) = descriptor_entry_int_spill_pair(emitter); + abi::load_from_caller_stack(emitter, ptr_spill_reg, caller_offset); + abi::load_from_caller_stack(emitter, len_spill_reg, caller_offset + 8); + (ptr_spill_reg, len_spill_reg) + }; + abi::store_at_offset(emitter, ptr_reg, offset); + abi::store_at_offset(emitter, len_reg, offset - 8); + } + PhpType::TaggedScalar => { + let (payload_reg, tag_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let (payload_spill_reg, tag_spill_reg) = descriptor_entry_int_spill_pair(emitter); + abi::load_from_caller_stack(emitter, payload_spill_reg, caller_offset); + abi::load_from_caller_stack(emitter, tag_spill_reg, caller_offset + 8); + (payload_spill_reg, tag_spill_reg) + }; + abi::store_at_offset(emitter, payload_reg, offset); + abi::store_at_offset(emitter, tag_reg, offset - 8); + } + PhpType::Void | PhpType::Never => {} + _ => { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let spill_reg = abi::secondary_scratch_reg(emitter); + abi::load_from_caller_stack(emitter, spill_reg, caller_offset); + spill_reg + }; + abi::store_at_offset(emitter, reg, offset); + } + } +} + +/// Loads one spilled descriptor entry argument into its real method ABI assignment. +fn load_descriptor_entry_actual_arg( + emitter: &mut Emitter, + ty: &PhpType, + assignment: &abi::OutgoingArgAssignment, + offset: usize, + stack_offset: Option, +) { + match ty.codegen_repr() { + PhpType::Float => { + let reg = if assignment.in_register() { + abi::float_arg_reg_name(emitter.target, assignment.start_reg) + } else { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => "d15", + crate::codegen::platform::Arch::X86_64 => "xmm15", + } + }; + abi::load_at_offset(emitter, reg, offset); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } + } + PhpType::Str => { + let (ptr_reg, len_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + descriptor_entry_int_spill_pair(emitter) + }; + abi::load_at_offset(emitter, ptr_reg, offset); + abi::load_at_offset(emitter, len_reg, offset - 8); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, ptr_reg, out_offset); + abi::emit_store_to_sp(emitter, len_reg, out_offset + 8); + } + } + PhpType::TaggedScalar => { + let (payload_reg, tag_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + descriptor_entry_int_spill_pair(emitter) + }; + abi::load_at_offset(emitter, payload_reg, offset); + abi::load_at_offset(emitter, tag_reg, offset - 8); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, payload_reg, out_offset); + abi::emit_store_to_sp(emitter, tag_reg, out_offset + 8); + } + } + PhpType::Void | PhpType::Never => {} + _ => { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + abi::secondary_scratch_reg(emitter) + }; + abi::load_at_offset(emitter, reg, offset); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } + } + } +} + +/// Rounds `value` up to a 16-byte multiple. +fn align16(value: usize) -> usize { + (value + 15) & !15 +} + +/// Converts the ARM64 boxed eval argument slot into a callable descriptor pointer. +pub(super) fn emit_aarch64_cast_eval_callable_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, +) { + let string_label = format!("{}_callable_string", label_prefix); + let array_label = format!("{}_callable_array", label_prefix); + let object_label = format!("{}_callable_object", label_prefix); + let dynamic_label = format!("{}_callable_dynamic", label_prefix); + let miss_label = if support.has_dynamic_descriptor() { + dynamic_label.as_str() + } else { + fail_label + }; + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for callable validation + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval callable tag and payload words + emitter.instruction("cmp x0, #1"); // runtime tag 1 means a string callable name + emitter.instruction(&format!("b.eq {}", string_label)); // resolve string callables through descriptor metadata + emitter.instruction("cmp x0, #4"); // runtime tag 4 means an indexed callable array + emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays through descriptor metadata + emitter.instruction("cmp x0, #5"); // runtime tag 5 means an associative callable array + emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays with numeric keys + emitter.instruction("cmp x0, #6"); // runtime tag 6 means an invokable object candidate + emitter.instruction(&format!("b.eq {}", object_label)); // resolve invokable objects through descriptor metadata + abi::emit_jump(emitter, miss_label); + emitter.label(&string_label); + abi::emit_push_reg_pair(emitter, "x1", "x2"); + emit_eval_string_callable_descriptor_lookup( + module, + emitter, + data, + support, + label_prefix, + miss_label, + "x0", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&array_label); + emit_aarch64_eval_callable_array_selector_slots(module, emitter); + emit_eval_callable_array_descriptor_lookup( + emitter, + data, + support, + label_prefix, + miss_label, + "x0", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&object_label); + abi::emit_push_reg(emitter, "x1"); + emit_eval_invokable_object_descriptor_lookup( + emitter, + support, + label_prefix, + miss_label, + "x0", + ); + if support.has_dynamic_descriptor() { + emitter.label(&dynamic_label); + emit_aarch64_eval_dynamic_callable_descriptor( + module, + emitter, + support, + fail_label, + "x0", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + } + emitter.label(&format!("{}_callable_cast_done", label_prefix)); +} + +/// Converts the x86_64 boxed eval argument slot into a callable descriptor pointer. +pub(super) fn emit_x86_64_cast_eval_callable_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + context_frame_offset: usize, +) { + let string_label = format!("{}_callable_string", label_prefix); + let array_label = format!("{}_callable_array", label_prefix); + let object_label = format!("{}_callable_object", label_prefix); + let dynamic_label = format!("{}_callable_dynamic", label_prefix); + let miss_label = if support.has_dynamic_descriptor() { + dynamic_label.as_str() + } else { + fail_label + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for callable validation + emitter.instruction("call __rt_mixed_unbox"); // expose the eval callable tag and payload words + emitter.instruction("cmp rax, 1"); // runtime tag 1 means a string callable name + emitter.instruction(&format!("je {}", string_label)); // resolve string callables through descriptor metadata + emitter.instruction("cmp rax, 4"); // runtime tag 4 means an indexed callable array + emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays through descriptor metadata + emitter.instruction("cmp rax, 5"); // runtime tag 5 means an associative callable array + emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays with numeric keys + emitter.instruction("cmp rax, 6"); // runtime tag 6 means an invokable object candidate + emitter.instruction(&format!("je {}", object_label)); // resolve invokable objects through descriptor metadata + abi::emit_jump(emitter, miss_label); + emitter.label(&string_label); + abi::emit_push_reg_pair(emitter, "rdi", "rdx"); + emit_eval_string_callable_descriptor_lookup( + module, + emitter, + data, + support, + label_prefix, + miss_label, + "rax", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&array_label); + emit_x86_64_eval_callable_array_selector_slots(module, emitter); + emit_eval_callable_array_descriptor_lookup( + emitter, + data, + support, + label_prefix, + miss_label, + "rax", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&object_label); + abi::emit_push_reg(emitter, "rdi"); + emit_eval_invokable_object_descriptor_lookup( + emitter, + support, + label_prefix, + miss_label, + "rax", + ); + if support.has_dynamic_descriptor() { + emitter.label(&dynamic_label); + emit_x86_64_eval_dynamic_callable_descriptor( + module, + emitter, + support, + fail_label, + "rax", + context_frame_offset, + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + } + emitter.label(&format!("{}_callable_cast_done", label_prefix)); +} + +/// Materializes a runtime descriptor that dispatches a callable through magician on ARM64. +fn emit_aarch64_eval_dynamic_callable_descriptor( + module: &Module, + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + fail_label: &str, + result_reg: &str, +) { + let descriptor_label = support + .dynamic_descriptor_label + .as_deref() + .expect("dynamic eval callable descriptor must exist"); + let is_callable_symbol = module.target.extern_symbol("__elephc_eval_is_callable"); + emitter.instruction(&format!( + "ldr x0, [x29, #{}]", + AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET + )); // load the active eval context for dynamic callable validation + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject dynamic eval callables when no context is active + emitter.instruction("ldr x1, [x29, #-16]"); // pass the original boxed eval callback value + abi::emit_call_label(emitter, &is_callable_symbol); + emitter.instruction("cmp w0, #0"); // check whether magician accepts the callback value + emitter.instruction(&format!("b.eq {}", fail_label)); // reject non-callable eval values + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed callback value to retain it + emitter.instruction("bl __rt_incref"); // retain the callback for descriptor capture ownership + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_int_immediate( + emitter, + "x0", + (callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + + EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES) as i64, + ); + emitter.instruction("bl __rt_heap_alloc"); // allocate runtime descriptor storage with eval captures + callable_descriptor::emit_copy_static_descriptor_to_runtime(emitter, "x0", descriptor_label); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET + )); // reload the active eval context for descriptor capture 0 + abi::emit_store_to_address( + emitter, + "x10", + "x0", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE), + ); + abi::emit_load_temporary_stack_slot(emitter, "x10", 0); + abi::emit_store_to_address( + emitter, + "x10", + "x0", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE), + ); + abi::emit_release_temporary_stack(emitter, 16); + if result_reg != "x0" { + emitter.instruction(&format!("mov {}, x0", result_reg)); // return the dynamic eval callable descriptor + } +} + +/// Materializes a runtime descriptor that dispatches a callable through magician on x86_64. +fn emit_x86_64_eval_dynamic_callable_descriptor( + module: &Module, + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + fail_label: &str, + result_reg: &str, + context_frame_offset: usize, +) { + let descriptor_label = support + .dynamic_descriptor_label + .as_deref() + .expect("dynamic eval callable descriptor must exist"); + let is_callable_symbol = module.target.extern_symbol("__elephc_eval_is_callable"); + emitter.instruction(&format!( + "mov rdi, QWORD PTR [rbp - {}]", + context_frame_offset + )); // load the active eval context for dynamic callable validation + emitter.instruction("test rdi, rdi"); // check whether a context was passed by magician + emitter.instruction(&format!("jz {}", fail_label)); // reject dynamic eval callables when no context is active + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the original boxed eval callback value + abi::emit_call_label(emitter, &is_callable_symbol); + emitter.instruction("test eax, eax"); // check whether magician accepts the callback value + emitter.instruction(&format!("jz {}", fail_label)); // reject non-callable eval values + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed callback value to retain it + emitter.instruction("call __rt_incref"); // retain the callback for descriptor capture ownership + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_int_immediate( + emitter, + "rax", + (callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + + EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES) as i64, + ); + emitter.instruction("call __rt_heap_alloc"); // allocate runtime descriptor storage with eval captures + callable_descriptor::emit_copy_static_descriptor_to_runtime(emitter, "rax", descriptor_label); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + context_frame_offset + )); // reload the active eval context for descriptor capture 0 + abi::emit_store_to_address( + emitter, + "r10", + "rax", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE), + ); + abi::emit_load_temporary_stack_slot(emitter, "r10", 0); + abi::emit_store_to_address( + emitter, + "r10", + "rax", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE), + ); + abi::emit_release_temporary_stack(emitter, 16); + if result_reg != "rax" { + emitter.instruction(&format!("mov {}, rax", result_reg)); // return the dynamic eval callable descriptor + } +} + +/// Looks up a descriptor by the string callable currently saved on the temp stack. +fn emit_eval_string_callable_descriptor_lookup( + _module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_done", label_prefix); + let miss_label = format!("{}_callable_missing", label_prefix); + if support.is_empty() { + abi::emit_release_temporary_stack(emitter, 16); + abi::emit_jump(emitter, fail_label); + return; + } + let selector = RuntimeCallableSelector::StringNameStack { + ptr_offset: 0, + len_offset: 8, + call_reg: result_reg, + }; + for (index, case) in support + .string_cases + .iter() + .chain(support.static_array_cases.iter().map(|case| &case.case)) + .enumerate() + { + let next_case = format!("{}_eval_callable_next_{}", label_prefix, index); + let matched_label = format!("{}_eval_callable_match_{}", label_prefix, index); + callable_dispatch::emit_branch_if_callable_case_mismatch( + &selector, + case, + &next_case, + emitter, + &matched_label, + data, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, 16); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Saves the receiver and method slots from an ARM64 boxed eval callable array. +fn emit_aarch64_eval_callable_array_selector_slots(module: &Module, emitter: &mut Emitter) { + emit_aarch64_eval_callable_array_slot(module, emitter, 0); + emit_aarch64_push_mixed_unbox_payload(emitter); + emit_aarch64_eval_callable_array_slot(module, emitter, 1); + emit_aarch64_push_mixed_unbox_payload(emitter); +} + +/// Saves the receiver and method slots from an x86_64 boxed eval callable array. +fn emit_x86_64_eval_callable_array_selector_slots(module: &Module, emitter: &mut Emitter) { + emit_x86_64_eval_callable_array_slot(module, emitter, 0); + emit_x86_64_push_mixed_unbox_payload(emitter); + emit_x86_64_eval_callable_array_slot(module, emitter, 1); + emit_x86_64_push_mixed_unbox_payload(emitter); +} + +/// Loads and unboxes one ARM64 callable-array slot through eval's array reader. +fn emit_aarch64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "x0", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed callable-array index + emitter.instruction("ldr x1, [sp]"); // pass the boxed selector index to eval's array reader + emitter.instruction("ldr x0, [x29, #-16]"); // pass the boxed callable array to eval's array reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("add sp, sp, #16"); // discard the temporary boxed selector index + emitter.instruction("bl __rt_mixed_unbox"); // expose the callable-array selector slot tag and payload +} + +/// Loads and unboxes one x86_64 callable-array slot through eval's array reader. +fn emit_x86_64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "rdi", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + abi::emit_push_reg(emitter, "rax"); + emitter.instruction("mov rsi, QWORD PTR [rsp]"); // pass the boxed selector index to eval's array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // pass the boxed callable array to eval's array reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("add rsp, 16"); // discard the temporary boxed selector index + emitter.instruction("call __rt_mixed_unbox"); // expose the callable-array selector slot tag and payload +} + +/// Preserves the current ARM64 mixed-unbox tag and payload on the temp stack. +fn emit_aarch64_push_mixed_unbox_payload(emitter: &mut Emitter) { + abi::emit_push_reg_pair(emitter, "x1", "x2"); + abi::emit_push_reg(emitter, "x0"); +} + +/// Preserves the current x86_64 mixed-unbox tag and payload on the temp stack. +fn emit_x86_64_push_mixed_unbox_payload(emitter: &mut Emitter) { + abi::emit_push_reg_pair(emitter, "rdi", "rdx"); + abi::emit_push_reg(emitter, "rax"); +} + +/// Looks up a descriptor from saved callable-array selector slots. +fn emit_eval_callable_array_descriptor_lookup( + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_array_done", label_prefix); + let miss_label = format!("{}_callable_array_missing", label_prefix); + if support.array_cases_empty() { + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); + abi::emit_jump(emitter, fail_label); + return; + } + for (index, case) in support.instance_array_cases.iter().enumerate() { + let next_case = format!("{}_callable_array_instance_next_{}", label_prefix, index); + emit_branch_if_instance_callable_array_case_mismatch( + emitter, + data, + case, + &next_case, + ); + emit_runtime_descriptor_with_saved_receiver_capture( + emitter, + &case.case.descriptor_label, + MIXED_RECEIVER_PAYLOAD_OFFSET, + result_reg, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + for (index, case) in support.static_array_cases.iter().enumerate() { + let next_case = format!("{}_callable_array_next_{}", label_prefix, index); + emit_branch_if_static_callable_array_case_mismatch( + emitter, + data, + case, + &next_case, + ); + abi::emit_symbol_address(emitter, result_reg, &case.case.descriptor_label); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); +} + +/// Looks up a descriptor from a saved invokable-object receiver. +fn emit_eval_invokable_object_descriptor_lookup( + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_object_done", label_prefix); + let miss_label = format!("{}_callable_object_missing", label_prefix); + if support.object_cases_empty() { + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); + abi::emit_jump(emitter, fail_label); + return; + } + for (index, case) in support.object_cases.iter().enumerate() { + let next_case = format!("{}_callable_object_next_{}", label_prefix, index); + emit_branch_if_receiver_class_id_mismatch( + emitter, + case.class_id, + 0, + &next_case, + ); + emit_runtime_descriptor_with_saved_receiver_capture( + emitter, + &case.case.descriptor_label, + 0, + result_reg, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); +} + +/// Branches unless saved selector slots match one instance callable-array case. +fn emit_branch_if_instance_callable_array_case_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + case: &EvalInstanceMethodCallableCase, + next_label: &str, +) { + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_RECEIVER_TAG_OFFSET, + MIXED_TAG_OBJECT, + next_label, + ); + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_METHOD_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); + emit_branch_if_receiver_class_id_mismatch( + emitter, + case.class_id, + MIXED_RECEIVER_PAYLOAD_OFFSET, + next_label, + ); + emit_branch_if_stack_string_mismatch( + emitter, + data, + MIXED_METHOD_PAYLOAD_OFFSET, + MIXED_METHOD_PAYLOAD_OFFSET + 8, + case.method_name.as_bytes(), + next_label, + ); +} + +/// Branches unless saved selector slots match one static callable-array case. +fn emit_branch_if_static_callable_array_case_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + case: &RuntimeStaticMethodCallableCase, + next_label: &str, +) { + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_RECEIVER_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_METHOD_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); + emit_branch_if_static_class_string_mismatch( + emitter, + data, + MIXED_RECEIVER_PAYLOAD_OFFSET, + MIXED_RECEIVER_PAYLOAD_OFFSET + 8, + &case.class_name, + next_label, + ); + emit_branch_if_stack_string_mismatch( + emitter, + data, + MIXED_METHOD_PAYLOAD_OFFSET, + MIXED_METHOD_PAYLOAD_OFFSET + 8, + case.method_name.as_bytes(), + next_label, + ); +} + +/// Branches when the saved receiver object's class id does not match `class_id`. +fn emit_branch_if_receiver_class_id_mismatch( + emitter: &mut Emitter, + class_id: u64, + receiver_offset: usize, + next_label: &str, +) { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x9", receiver_offset); + emitter.instruction(&format!("cbz x9, {}", next_label)); // reject null receiver pointers before reading their class id + emitter.instruction("ldr x10, [x9]"); // load the receiver runtime class id from the object header + abi::emit_load_int_immediate(emitter, "x11", class_id as i64); + emitter.instruction("cmp x10, x11"); // compare receiver class id against this callable case + emitter.instruction(&format!("b.ne {}", next_label)); // try the next callable target when the receiver class differs + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "r10", receiver_offset); + emitter.instruction("test r10, r10"); // reject null receiver pointers before reading their class id + emitter.instruction(&format!("je {}", next_label)); // try the next callable target when the receiver pointer is null + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the receiver runtime class id from the object header + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this callable case + emitter.instruction(&format!("jne {}", next_label)); // try the next callable target when the receiver class differs + } + } +} + +/// Allocates a runtime descriptor and captures the saved receiver object. +fn emit_runtime_descriptor_with_saved_receiver_capture( + emitter: &mut Emitter, + descriptor_label: &str, + receiver_offset: usize, + result_reg: &str, +) { + let total_bytes = callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + 16; + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x0", receiver_offset); + emitter.instruction("bl __rt_incref"); // retain the receiver for the runtime callable descriptor + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_int_immediate(emitter, "x0", total_bytes as i64); + emitter.instruction("bl __rt_heap_alloc"); // allocate receiver-bound callable descriptor storage + callable_descriptor::emit_copy_static_descriptor_to_runtime( + emitter, + "x0", + descriptor_label, + ); + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_temporary_stack_slot(emitter, "x11", 0); + abi::emit_load_temporary_stack_slot(emitter, "x0", 16); + callable_descriptor::emit_store_current_result_to_runtime_capture( + emitter, + "x11", + 0, + &PhpType::Object(String::new()), + ); + abi::emit_pop_reg(emitter, result_reg); + abi::emit_release_temporary_stack(emitter, 16); + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "rax", receiver_offset); + emitter.instruction("call __rt_incref"); // retain the receiver for the runtime callable descriptor + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_int_immediate(emitter, "rax", total_bytes as i64); + emitter.instruction("call __rt_heap_alloc"); // allocate receiver-bound callable descriptor storage + callable_descriptor::emit_copy_static_descriptor_to_runtime( + emitter, + "rax", + descriptor_label, + ); + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_temporary_stack_slot(emitter, "rcx", 0); + abi::emit_load_temporary_stack_slot(emitter, "rax", 16); + callable_descriptor::emit_store_current_result_to_runtime_capture( + emitter, + "rcx", + 0, + &PhpType::Object(String::new()), + ); + abi::emit_pop_reg(emitter, result_reg); + abi::emit_release_temporary_stack(emitter, 16); + } + } +} + +/// Branches when a saved Mixed tag stack slot does not equal `expected_tag`. +fn emit_branch_if_stack_tag_mismatch( + emitter: &mut Emitter, + tag_offset: usize, + expected_tag: i64, + next_label: &str, +) { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x9", tag_offset); + emitter.instruction(&format!("cmp x9, #{}", expected_tag)); // compare the callable-array selector runtime tag + emitter.instruction(&format!("b.ne {}", next_label)); // try the next callable-array target when the tag differs + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "r10", tag_offset); + emitter.instruction(&format!("cmp r10, {}", expected_tag)); // compare the callable-array selector runtime tag + emitter.instruction(&format!("jne {}", next_label)); // try the next callable-array target when the tag differs + } + } +} + +/// Branches when a saved stack string does not match the expected PHP name. +fn emit_branch_if_stack_string_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + expected: &[u8], + next_label: &str, +) { + let matched_label = format!("{}_matched", next_label); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + expected, + &matched_label, + ); + abi::emit_jump(emitter, next_label); + emitter.label(&matched_label); +} + +/// Branches when a saved class string does not match bare or leading-slash forms. +fn emit_branch_if_static_class_string_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + class_name: &str, + next_label: &str, +) { + let matched_label = format!("{}_class_matched", next_label); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + class_name.as_bytes(), + &matched_label, + ); + let leading_slash = format!("\\{}", class_name); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + leading_slash.as_bytes(), + &matched_label, + ); + abi::emit_jump(emitter, next_label); + emitter.label(&matched_label); +} + +/// Compares a saved stack string with `expected` and branches on equality. +fn emit_stack_string_compare_branch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + expected: &[u8], + matched_label: &str, +) { + let (expected_label, expected_len) = data.add_string(expected); + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x1", ptr_offset); + abi::emit_load_temporary_stack_slot(emitter, "x2", len_offset); + abi::emit_symbol_address(emitter, "x3", &expected_label); + abi::emit_load_int_immediate(emitter, "x4", expected_len as i64); + abi::emit_call_label(emitter, "__rt_strcasecmp"); + emitter.instruction("cmp x0, #0"); // check whether the runtime method string matched + emitter.instruction(&format!("b.eq {}", matched_label)); // select this callable-array target when names match + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "rdi", ptr_offset); + abi::emit_load_temporary_stack_slot(emitter, "rsi", len_offset); + abi::emit_symbol_address(emitter, "rdx", &expected_label); + abi::emit_load_int_immediate(emitter, "rcx", expected_len as i64); + abi::emit_call_label(emitter, "__rt_strcasecmp"); + emitter.instruction("test rax, rax"); // check whether the runtime method string matched + emitter.instruction(&format!("je {}", matched_label)); // select this callable-array target when names match + } + } +} diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs new file mode 100644 index 0000000000..9bbd90476c --- /dev/null +++ b/src/codegen/eval_class_constant_helpers.rs @@ -0,0 +1,1519 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician read generated/AOT +//! class-like constants and their Reflection metadata from eval fragments. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - Direct `Aot::CONST` reads enforce PHP visibility with the active eval class scope. +//! - Reflection probes intentionally bypass visibility, matching PHP Reflection. +//! - Values are limited to the same scalar and enum-case constant forms emitted by static Reflection metadata. + +use std::collections::HashSet; + +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{enum_case_symbol, php_symbol_key}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Visibility}; +use crate::types::{ClassInfo, InterfaceInfo, PhpType}; + +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; + +/// Constant slot metadata needed by eval direct reads and Reflection probes. +#[derive(Clone)] +struct EvalClassConstantSlot { + reflected_class: String, + declaring_class: String, + allowed_scopes: Vec, + constant: String, + visibility: Visibility, + is_final: bool, + is_enum_case: bool, + value: EvalClassConstantValue, +} + +/// Constant value forms the eval bridge can materialize as boxed Mixed cells. +#[derive(Clone)] +enum EvalClassConstantValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, + EnumCase { + enum_name: String, + case_name: String, + }, +} + +/// Emits eval class-constant helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_class_constant_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_class_constant_slots(module); + emit_class_constant_get_helper(module, emitter, data, &slots); + emit_reflection_constant_value_helper(module, emitter, data, &slots); + emit_reflection_constant_names_helper(module, emitter, data, &slots); + emit_reflection_constant_flags_helper(module, emitter, data, &slots); + emit_reflection_constant_declaring_class_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects class-like constants visible to direct eval reads and Reflection APIs. +fn collect_eval_class_constant_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, _) in classes { + collect_reflected_class_constant_slots(module, class_name, &mut slots); + } + + let mut interfaces = module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, _) in interfaces { + collect_reflected_interface_constant_slots(module, interface_name, &mut slots); + } + + let mut traits = module.declared_trait_names.clone(); + traits.sort(); + for trait_name in traits { + collect_reflected_trait_constant_slots(module, &trait_name, &mut slots); + } + slots +} + +/// Adds constants visible when reflecting or reading one generated/AOT class. +fn collect_reflected_class_constant_slots( + module: &Module, + class_name: &str, + slots: &mut Vec, +) { + for constant_name in class_constant_names(module, class_name) { + let slot = if let Some(slot) = enum_case_slot(module, class_name, &constant_name) { + Some(slot) + } else { + resolve_class_constant_slot(module, class_name, &constant_name) + }; + if let Some(mut slot) = slot { + slot.reflected_class = class_name.to_string(); + slots.push(slot); + } + } +} + +/// Adds constants visible when reflecting or reading one generated/AOT interface. +fn collect_reflected_interface_constant_slots( + module: &Module, + interface_name: &str, + slots: &mut Vec, +) { + for constant_name in interface_constant_names(module, interface_name) { + if let Some(mut slot) = + resolve_interface_constant_slot(module, interface_name, &constant_name) + { + slot.reflected_class = interface_name.to_string(); + slots.push(slot); + } + } +} + +/// Adds constants visible when reflecting or reading one generated/AOT trait. +fn collect_reflected_trait_constant_slots( + module: &Module, + trait_name: &str, + slots: &mut Vec, +) { + let Some(constants) = module.declared_trait_constants.get(trait_name) else { + return; + }; + let mut names = module + .declared_trait_constant_names + .get(trait_name) + .cloned() + .unwrap_or_else(|| constants.keys().cloned().collect()); + names.sort(); + for constant_name in names { + if let Some(mut slot) = trait_constant_slot(module, trait_name, &constant_name) { + slot.reflected_class = trait_name.to_string(); + slots.push(slot); + } + } +} + +/// Returns class constant names in the same order as eval-backed ReflectionClass. +fn class_constant_names(module: &Module, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_info) = module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_name(&case.name, &mut names, &mut seen); + } + } + for (declaring_class, class_info) in class_chain(module, class_name) { + let mut own = class_info.constants.keys().cloned().collect::>(); + own.sort(); + for constant_name in own { + if is_private_inherited_class_constant( + class_name, + declaring_class, + class_info, + &constant_name, + ) { + continue; + } + push_unique_constant_name(&constant_name, &mut names, &mut seen); + } + for interface_name in &class_info.interfaces { + for constant_name in interface_constant_names(module, interface_name) { + push_unique_constant_name(&constant_name, &mut names, &mut seen); + } + } + } + names +} + +/// Returns whether a parent private constant is hidden from a reflected child. +fn is_private_inherited_class_constant( + reflected_class: &str, + declaring_class: &str, + declaring_info: &ClassInfo, + constant_name: &str, +) -> bool { + php_symbol_key(reflected_class) != php_symbol_key(declaring_class) + && declaring_info + .constant_visibilities + .get(constant_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Private)) +} + +/// Returns the inheritance chain from reflected class toward its parents. +fn class_chain<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut result = Vec::new(); + let mut current = Some(class_name); + let mut seen = HashSet::new(); + while let Some(name) = current { + let Some((resolved_name, info)) = resolve_class(module, name) else { + break; + }; + if !seen.insert(php_symbol_key(resolved_name)) { + break; + } + result.push((resolved_name, info)); + current = info.parent.as_deref(); + } + result +} + +/// Returns interface constant names with parent interfaces first. +fn interface_constant_names(module: &Module, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + collect_interface_constant_names(module, interface_name, &mut names, &mut seen); + names +} + +/// Recursively collects interface constant names without duplicates. +fn collect_interface_constant_names( + module: &Module, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, +) { + let Some((_, interface_info)) = resolve_interface(module, interface_name) else { + return; + }; + for parent in &interface_info.parents { + collect_interface_constant_names(module, parent, names, seen); + } + let mut own = interface_info.constants.keys().cloned().collect::>(); + own.sort(); + for constant_name in own { + push_unique_constant_name(&constant_name, names, seen); + } +} + +/// Pushes one case-sensitive PHP constant name once. +fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Resolves one class constant against class parents and implemented interfaces. +fn resolve_class_constant_slot( + module: &Module, + class_name: &str, + constant_name: &str, +) -> Option { + let (resolved_name, class_info) = resolve_class(module, class_name)?; + if let Some(value_expr) = class_info.constants.get(constant_name) { + return class_constant_slot(module, resolved_name, class_info, constant_name, value_expr); + } + if let Some(parent) = class_info.parent.as_deref() { + if let Some(slot) = resolve_class_constant_slot(module, parent, constant_name) { + return Some(slot); + } + } + for interface_name in &class_info.interfaces { + if let Some(slot) = resolve_interface_constant_slot(module, interface_name, constant_name) { + return Some(slot); + } + } + None +} + +/// Builds metadata for one class-declared constant. +fn class_constant_slot( + module: &Module, + declaring_class: &str, + class_info: &ClassInfo, + constant_name: &str, + value_expr: &Expr, +) -> Option { + let value = + eval_class_constant_value(module, declaring_class, Some(class_info), value_expr, 0)?; + let visibility = class_info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public); + Some(EvalClassConstantSlot { + reflected_class: declaring_class.to_string(), + declaring_class: declaring_class.to_string(), + allowed_scopes: visibility_scope_names(module, declaring_class, &visibility), + constant: constant_name.to_string(), + visibility, + is_final: class_info.final_constants.contains(constant_name), + is_enum_case: false, + value, + }) +} + +/// Resolves one interface constant and preserves its original declaring interface. +fn resolve_interface_constant_slot( + module: &Module, + interface_name: &str, + constant_name: &str, +) -> Option { + let (resolved_name, interface_info) = resolve_interface(module, interface_name)?; + let value_expr = interface_info.constants.get(constant_name)?; + let declaring_interface = + interface_constant_declaring_interface(interface_info, resolved_name, constant_name); + let value = eval_class_constant_value(module, declaring_interface, None, value_expr, 0)?; + Some(EvalClassConstantSlot { + reflected_class: resolved_name.to_string(), + declaring_class: declaring_interface.to_string(), + allowed_scopes: Vec::new(), + constant: constant_name.to_string(), + visibility: Visibility::Public, + is_final: module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)), + is_enum_case: false, + value, + }) +} + +/// Builds metadata for one trait-declared constant. +fn trait_constant_slot( + module: &Module, + trait_name: &str, + constant_name: &str, +) -> Option { + let value_expr = module + .declared_trait_constants + .get(trait_name) + .and_then(|constants| constants.get(constant_name))?; + let value = eval_class_constant_value(module, trait_name, None, value_expr, 0)?; + let visibility = module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public); + Some(EvalClassConstantSlot { + reflected_class: trait_name.to_string(), + declaring_class: trait_name.to_string(), + allowed_scopes: visibility_scope_names(module, trait_name, &visibility), + constant: constant_name.to_string(), + visibility, + is_final: module + .declared_trait_final_constants + .get(trait_name) + .is_some_and(|constants| constants.contains(constant_name)), + is_enum_case: false, + value, + }) +} + +/// Builds metadata for one enum case exposed as a class constant. +fn enum_case_slot( + module: &Module, + enum_name: &str, + case_name: &str, +) -> Option { + let enum_info = module.enum_infos.get(enum_name)?; + enum_info + .cases + .iter() + .any(|case| case.name == case_name) + .then(|| EvalClassConstantSlot { + reflected_class: enum_name.to_string(), + declaring_class: enum_name.to_string(), + allowed_scopes: Vec::new(), + constant: case_name.to_string(), + visibility: Visibility::Public, + is_final: false, + is_enum_case: true, + value: EvalClassConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case_name.to_string(), + }, + }) +} + +/// Returns the interface that originally declared one inherited constant. +fn interface_constant_declaring_interface<'a>( + info: &'a InterfaceInfo, + fallback_interface: &'a str, + constant_name: &str, +) -> &'a str { + info.constant_declaring_interfaces + .get(constant_name) + .map(String::as_str) + .unwrap_or(fallback_interface) +} + +/// Evaluates one supported AOT class-like constant expression. +fn eval_class_constant_value( + module: &Module, + current_class: &str, + current_info: Option<&ClassInfo>, + expr: &Expr, + depth: usize, +) -> Option { + if depth > 16 { + return None; + } + match &expr.kind { + ExprKind::IntLiteral(value) => Some(EvalClassConstantValue::Int(*value)), + ExprKind::BoolLiteral(value) => Some(EvalClassConstantValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Some(EvalClassConstantValue::Float(*value)), + ExprKind::StringLiteral(value) => Some(EvalClassConstantValue::Str(value.clone())), + ExprKind::Null => Some(EvalClassConstantValue::Null), + ExprKind::Negate(inner) => { + match eval_class_constant_value(module, current_class, current_info, inner, depth + 1)? + { + EvalClassConstantValue::Int(value) => { + value.checked_neg().map(EvalClassConstantValue::Int) + } + EvalClassConstantValue::Float(value) => Some(EvalClassConstantValue::Float(-value)), + _ => None, + } + } + ExprKind::BinaryOp { left, op, right } => eval_binary_class_constant_value( + module, + current_class, + current_info, + left, + op, + right, + depth + 1, + ), + ExprKind::ClassConstant { receiver } => { + let class_name = static_receiver_name(current_class, current_info, receiver)?; + Some(EvalClassConstantValue::Str(class_name)) + } + ExprKind::ScopedConstantAccess { receiver, name } => { + let class_name = static_receiver_name(current_class, current_info, receiver)?; + eval_scoped_class_constant_value(module, &class_name, name, depth + 1) + } + _ => None, + } +} + +/// Evaluates one supported binary class-constant expression. +fn eval_binary_class_constant_value( + module: &Module, + current_class: &str, + current_info: Option<&ClassInfo>, + left: &Expr, + op: &BinOp, + right: &Expr, + depth: usize, +) -> Option { + let left = eval_class_constant_value(module, current_class, current_info, left, depth)?; + let right = eval_class_constant_value(module, current_class, current_info, right, depth)?; + match (&left, op, &right) { + (EvalClassConstantValue::Int(left), BinOp::Add, EvalClassConstantValue::Int(right)) => { + (*left).checked_add(*right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Sub, EvalClassConstantValue::Int(right)) => { + (*left).checked_sub(*right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Mul, EvalClassConstantValue::Int(right)) => { + (*left).checked_mul(*right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Mod, EvalClassConstantValue::Int(right)) => { + (*left).checked_rem(*right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Pow, EvalClassConstantValue::Int(right)) + if *right >= 0 => + { + let exponent = u32::try_from(*right).ok()?; + (*left) + .checked_pow(exponent) + .map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Str(left), BinOp::Concat, EvalClassConstantValue::Str(right)) => { + Some(EvalClassConstantValue::Str(format!("{}{}", left, right))) + } + (left, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, right) => { + eval_float_binary_class_constant_value(left, op, right) + } + _ => None, + } +} + +/// Evaluates a numeric class-constant expression that must produce a float. +fn eval_float_binary_class_constant_value( + left: &EvalClassConstantValue, + op: &BinOp, + right: &EvalClassConstantValue, +) -> Option { + let left = eval_class_constant_value_as_float(left)?; + let right = eval_class_constant_value_as_float(right)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + Some(EvalClassConstantValue::Float(value)) +} + +/// Returns the float representation of numeric eval class-constant metadata. +fn eval_class_constant_value_as_float(value: &EvalClassConstantValue) -> Option { + match value { + EvalClassConstantValue::Int(value) => Some(*value as f64), + EvalClassConstantValue::Float(value) => Some(*value), + _ => None, + } +} + +/// Evaluates a scoped class-like constant expression. +fn eval_scoped_class_constant_value( + module: &Module, + class_name: &str, + constant_name: &str, + depth: usize, +) -> Option { + if let Some((resolved_name, info)) = resolve_class(module, class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return eval_class_constant_value(module, resolved_name, Some(info), value_expr, depth); + } + if let Some(parent) = info.parent.as_deref() { + if let Some(value) = + eval_scoped_class_constant_value(module, parent, constant_name, depth) + { + return Some(value); + } + } + for interface_name in &info.interfaces { + if let Some(value) = + eval_scoped_class_constant_value(module, interface_name, constant_name, depth) + { + return Some(value); + } + } + } + if let Some((resolved_name, info)) = resolve_interface(module, class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return eval_class_constant_value(module, resolved_name, None, value_expr, depth); + } + } + if let Some(value_expr) = module + .declared_trait_constants + .get(class_name) + .and_then(|constants| constants.get(constant_name)) + { + return eval_class_constant_value(module, class_name, None, value_expr, depth); + } + if module + .enum_infos + .get(class_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == constant_name)) + { + return Some(EvalClassConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: constant_name.to_string(), + }); + } + None +} + +/// Resolves `self`, `static`, `parent`, or named receivers in constant expressions. +fn static_receiver_name( + current_class: &str, + current_info: Option<&ClassInfo>, + receiver: &StaticReceiver, +) -> Option { + match receiver { + StaticReceiver::Named(name) => Some(name.as_str().trim_start_matches('\\').to_string()), + StaticReceiver::Self_ | StaticReceiver::Static => Some(current_class.to_string()), + StaticReceiver::Parent => current_info.and_then(|info| info.parent.clone()), + } +} + +/// Looks up class metadata by PHP-style case-insensitive name. +fn resolve_class<'a>(module: &'a Module, class_name: &str) -> Option<(&'a str, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_interface<'a>( + module: &'a Module, + interface_name: &str, +) -> Option<(&'a str, &'a InterfaceInfo)> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + module + .interface_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Emits `__elephc_eval_value_class_constant_get(class, constant, scope) -> Mixed*`. +fn emit_class_constant_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user class constant get ---"); + label_c_global(module, emitter, "__elephc_eval_value_class_constant_get"); + match module.target.arch { + Arch::AArch64 => { + emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::DirectGet) + } + Arch::X86_64 => { + emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::DirectGet) + } + } +} + +/// Emits `__elephc_eval_reflection_constant_value(class, constant) -> Mixed*`. +fn emit_reflection_constant_value_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant value ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_value"); + match module.target.arch { + Arch::AArch64 => emit_value_helper_aarch64( + module, + emitter, + data, + slots, + ValueHelperMode::ReflectionValue, + ), + Arch::X86_64 => emit_value_helper_x86_64( + module, + emitter, + data, + slots, + ValueHelperMode::ReflectionValue, + ), + } +} + +/// Distinguishes direct class-constant reads from Reflection value probes. +#[derive(Clone, Copy)] +enum ValueHelperMode { + DirectGet, + ReflectionValue, +} + +impl ValueHelperMode { + /// Returns the shared label suffix for this value helper mode. + const fn suffix(self) -> &'static str { + match self { + Self::DirectGet => "get", + Self::ReflectionValue => "reflection_value", + } + } + + /// Returns whether this helper must enforce constant visibility. + const fn checks_visibility(self) -> bool { + matches!(self, Self::DirectGet) + } +} + +/// Emits an ARM64 class-constant value helper body. +fn emit_value_helper_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + let done_label = format!("__elephc_eval_class_constant_{}_done", mode.suffix()); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, constant, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + if mode.checks_visibility() { + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + } + emit_aarch64_constant_value_dispatch(module, emitter, data, slots, mode); + emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); + emitter.label(&done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed class constant value to Rust +} + +/// Emits an x86_64 class-constant value helper body. +fn emit_value_helper_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + let done_label = format!("__elephc_eval_class_constant_{}_done_x", mode.suffix()); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, constant, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + if mode.checks_visibility() { + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + } + emit_x86_64_constant_value_dispatch(module, emitter, data, slots, mode); + emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); + emitter.label(&done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed class constant value to Rust +} + +/// Emits ARM64 class-name and constant-name dispatch for value helpers. +fn emit_aarch64_constant_value_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + for slot in slots { + let next_label = slot_miss_label(module, slot, mode.suffix()); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_constant_name_compare(module, emitter, data, slot, mode, &next_label); + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and constant-name dispatch for value helpers. +fn emit_x86_64_constant_value_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + for slot in slots { + let next_label = slot_miss_label(module, slot, mode.suffix()); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_constant_name_compare(module, emitter, data, slot, mode, &next_label); + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison. +fn emit_aarch64_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // continue dispatch when class names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison. +fn emit_x86_64_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // continue dispatch when class names differ +} + +/// Emits one ARM64 case-sensitive constant-name comparison. +fn emit_aarch64_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + mode: ValueHelperMode, + next_label: &str, +) { + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + let target_label = slot_body_label(module, slot, mode.suffix()); + if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the constant value body when names match + return; + } + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + emit_aarch64_constant_scope_check(emitter, data, slot, &target_label, next_label); +} + +/// Emits one x86_64 case-sensitive constant-name comparison. +fn emit_x86_64_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + mode: ValueHelperMode, + next_label: &str, +) { + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + let target_label = slot_body_label(module, slot, mode.suffix()); + if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the constant value body when names match + return; + } + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + emit_x86_64_constant_scope_check(emitter, data, slot, &target_label, next_label); +} + +/// Emits ARM64 visibility checks for a protected/private constant bridge hit. +fn emit_aarch64_constant_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + target_label: &str, + next_label: &str, +) { + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", next_label)); // reject scoped constants outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", next_label)); // continue dispatch after a visibility miss +} + +/// Emits x86_64 visibility checks for a protected/private constant bridge hit. +fn emit_x86_64_constant_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + target_label: &str, + next_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", next_label)); // reject scoped constants outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", next_label)); // continue dispatch after a visibility miss +} + +/// Emits ARM64 value bodies for all class-constant slots. +fn emit_aarch64_value_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, mode.suffix())); + emit_aarch64_constant_value(emitter, data, &slot.value); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the constant value + } +} + +/// Emits x86_64 value bodies for all class-constant slots. +fn emit_x86_64_value_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, mode.suffix())); + emit_x86_64_constant_value(emitter, data, &slot.value); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the constant value + } +} + +/// Emits one ARM64 boxed Mixed value for a supported class constant. +fn emit_aarch64_constant_value( + emitter: &mut Emitter, + data: &mut DataSection, + value: &EvalClassConstantValue, +) { + match value { + EvalClassConstantValue::Int(value) => { + abi::emit_load_int_immediate(emitter, "x0", *value); + emit_box_current_value_as_mixed(emitter, &PhpType::Int); + } + EvalClassConstantValue::Bool(value) => { + abi::emit_load_int_immediate(emitter, "x0", i64::from(*value)); + emit_box_current_value_as_mixed(emitter, &PhpType::Bool); + } + EvalClassConstantValue::Float(value) => { + let label = data.add_float(*value); + abi::emit_symbol_address(emitter, "x9", &label); + emitter.instruction("ldr d0, [x9]"); // load the float constant through the data-section symbol + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + EvalClassConstantValue::Str(value) => { + let (label, len) = data.add_string(value.as_bytes()); + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + EvalClassConstantValue::Null => { + abi::emit_load_int_immediate(emitter, "x0", 0x7fff_ffff_ffff_fffe); + emit_box_current_value_as_mixed(emitter, &PhpType::Void); + } + EvalClassConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg(emitter, "x0", &case_label, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Emits one x86_64 boxed Mixed value for a supported class constant. +fn emit_x86_64_constant_value( + emitter: &mut Emitter, + data: &mut DataSection, + value: &EvalClassConstantValue, +) { + match value { + EvalClassConstantValue::Int(value) => { + abi::emit_load_int_immediate(emitter, "rax", *value); + emit_box_current_value_as_mixed(emitter, &PhpType::Int); + } + EvalClassConstantValue::Bool(value) => { + abi::emit_load_int_immediate(emitter, "rax", i64::from(*value)); + emit_box_current_value_as_mixed(emitter, &PhpType::Bool); + } + EvalClassConstantValue::Float(value) => { + let label = data.add_float(*value); + abi::emit_symbol_address(emitter, "r10", &label); + emitter.instruction("movsd xmm0, QWORD PTR [r10]"); // load the float constant through the data-section symbol + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + EvalClassConstantValue::Str(value) => { + let (label, len) = data.add_string(value.as_bytes()); + abi::emit_symbol_address(emitter, "rax", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + EvalClassConstantValue::Null => { + abi::emit_load_int_immediate(emitter, "rax", 0x7fff_ffff_ffff_fffe); + emit_box_current_value_as_mixed(emitter, &PhpType::Void); + } + EvalClassConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg(emitter, "rax", &case_label, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Emits `__elephc_eval_reflection_constant_names(class) -> string-array Mixed*`. +fn emit_reflection_constant_names_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant names ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_names"); + match module.target.arch { + Arch::AArch64 => emit_reflection_constant_names_aarch64(emitter, data, slots), + Arch::X86_64 => emit_reflection_constant_names_x86_64(emitter, data, slots), + } +} + +/// Emits the ARM64 Reflection constant-name array helper. +fn emit_reflection_constant_names_aarch64( + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_names_done"; + let fail_label = "__elephc_eval_reflection_constant_names_fail"; + let array_new = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class slice, array, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_load_int_immediate(emitter, "x0", slots.len() as i64); + emitter.instruction(&format!("bl {}", array_new)); // allocate the boxed string-array result + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("str x0, [sp, #16]"); // save the accumulated boxed string array + for (index, slot) in slots.iter().enumerate() { + emit_aarch64_constant_name_push(emitter, data, slot, index, fail_label); + } + emitter.instruction("ldr x0, [sp, #16]"); // return the accumulated boxed string array + emitter.instruction(&format!("b {}", done_label)); // skip null failure after a successful scan + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return null when allocation or append fails + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed constant-name array to Rust +} + +/// Emits the x86_64 Reflection constant-name array helper. +fn emit_reflection_constant_names_x86_64( + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_names_done_x"; + let fail_label = "__elephc_eval_reflection_constant_names_fail_x"; + let array_new = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class slice and result array + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_load_int_immediate(emitter, "rdi", slots.len() as i64); + emitter.instruction(&format!("call {}", array_new)); // allocate the boxed string-array result + emitter.instruction("test rax, rax"); // check whether allocation returned a boxed array + emitter.instruction(&format!("jz {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the accumulated boxed string array + for (index, slot) in slots.iter().enumerate() { + emit_x86_64_constant_name_push(emitter, data, slot, index, fail_label); + } + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated boxed string array + emitter.instruction(&format!("jmp {}", done_label)); // skip null failure after a successful scan + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return null when allocation or append fails + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed constant-name array to Rust +} + +/// Emits one ARM64 conditional append for a reflected constant name. +fn emit_aarch64_constant_name_push( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + index: usize, + fail_label: &str, +) { + let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}", index); + let push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed result string array + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction(&format!("bl {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("str x0, [sp, #16]"); // save the updated boxed string array + emitter.label(&skip_label); +} + +/// Emits one x86_64 conditional append for a reflected constant name. +fn emit_x86_64_constant_name_push( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + index: usize, + fail_label: &str, +) { + let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}_x", index); + let push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the boxed result string array + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction(&format!("call {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction("test rax, rax"); // check whether append returned an updated array + emitter.instruction(&format!("jz {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the updated boxed string array + emitter.label(&skip_label); +} + +/// Emits `__elephc_eval_reflection_constant_flags(class, constant) -> flags`. +fn emit_reflection_constant_flags_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant flags ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_flags"); + match module.target.arch { + Arch::AArch64 => emit_reflection_constant_flags_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_reflection_constant_flags_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 Reflection constant flags helper. +fn emit_reflection_constant_flags_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_flags_done"; + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "flags"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_flags_constant_name_compare(module, emitter, data, slot, &next_label); + emitter.label(&next_label); + } + emitter.instruction("mov x0, #0"); // return zero when no constant metadata matched + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the matched flags, or zero, to Rust +} + +/// Emits the x86_64 Reflection constant flags helper. +fn emit_reflection_constant_flags_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_flags_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "flags_x"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_flags_constant_name_compare(module, emitter, data, slot, &next_label); + emitter.label(&next_label); + } + emitter.instruction("xor eax, eax"); // return zero when no constant metadata matched + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the matched flags, or zero, to Rust +} + +/// Emits an ARM64 constant-name comparison that returns flags on a match. +fn emit_aarch64_flags_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, +) { + let done_label = "__elephc_eval_reflection_constant_flags_done"; + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + abi::emit_load_int_immediate(emitter, "x0", slot_flags(slot) as i64); + emitter.instruction(&format!("b {}", done_label)); // return the matched constant flags + let _ = module; +} + +/// Emits an x86_64 constant-name comparison that returns flags on a match. +fn emit_x86_64_flags_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, +) { + let done_label = "__elephc_eval_reflection_constant_flags_done_x"; + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + abi::emit_load_int_immediate(emitter, "rax", slot_flags(slot) as i64); + emitter.instruction(&format!("jmp {}", done_label)); // return the matched constant flags + let _ = module; +} + +/// Emits `__elephc_eval_reflection_constant_declaring_class(class, constant) -> string Mixed*`. +fn emit_reflection_constant_declaring_class_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant declaring class ---"); + label_c_global( + module, + emitter, + "__elephc_eval_reflection_constant_declaring_class", + ); + match module.target.arch { + Arch::AArch64 => { + emit_reflection_constant_declaring_class_aarch64(module, emitter, data, slots) + } + Arch::X86_64 => { + emit_reflection_constant_declaring_class_x86_64(module, emitter, data, slots) + } + } +} + +/// Emits the ARM64 Reflection constant declaring-class helper. +fn emit_reflection_constant_declaring_class_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_declaring_class_done"; + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "declaring"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); + emitter.label(&next_label); + } + emitter.instruction("mov x0, xzr"); // return null when no constant metadata matched + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the declaring class string, or null, to Rust +} + +/// Emits the x86_64 Reflection constant declaring-class helper. +fn emit_reflection_constant_declaring_class_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_declaring_class_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "declaring_x"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); + emitter.label(&next_label); + } + emitter.instruction("xor eax, eax"); // return null when no constant metadata matched + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null, to Rust +} + +/// Emits an ARM64 constant-name comparison that returns declaring class on a match. +fn emit_aarch64_declaring_constant_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, + done_label: &str, +) { + let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &constant_label); + abi::emit_load_int_immediate(emitter, "x4", constant_len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); + abi::emit_symbol_address(emitter, "x1", &class_label); + abi::emit_load_int_immediate(emitter, "x2", class_len as i64); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("b {}", done_label)); // return the matched declaring class name +} + +/// Emits an x86_64 constant-name comparison that returns declaring class on a match. +fn emit_x86_64_declaring_constant_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, + done_label: &str, +) { + let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &constant_label); + abi::emit_load_int_immediate(emitter, "rcx", constant_len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); + abi::emit_symbol_address(emitter, "rdi", &class_label); + abi::emit_load_int_immediate(emitter, "rsi", class_len as i64); + abi::emit_load_int_immediate(emitter, "rax", 1); + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("jmp {}", done_label)); // return the matched declaring class name +} + +/// Returns ReflectionClassConstant-style member flags for one slot. +fn slot_flags(slot: &EvalClassConstantSlot) -> u64 { + let mut flags = match slot.visibility { + Visibility::Public => EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, + }; + if slot.is_final { + flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } + if slot.is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + flags +} + +/// Returns class scopes that satisfy one constant visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Returns a platform-safe body label for one class-constant slot. +fn slot_body_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_class_constant_{}_{}_{}_{}{}", + mode, + label_fragment(&slot.reflected_class), + label_fragment(&slot.declaring_class), + label_fragment(&slot.constant), + suffix + ) +} + +/// Returns a platform-safe label for continuing after one slot miss. +fn slot_miss_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> String { + format!("{}_miss", slot_body_label(module, slot, mode)) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-global label using the target's symbol spelling. +fn label_c_global(module: &Module, emitter: &mut Emitter, symbol: &str) { + let label = module.target.extern_symbol(symbol); + emitter.label_global(&label); +} diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs new file mode 100644 index 0000000000..129832fc6f --- /dev/null +++ b/src/codegen/eval_constructor_helpers.rs @@ -0,0 +1,1687 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician run native +//! constructors after allocating AOT objects by class name. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object can allocate by name, but only user assembly +//! knows constructor symbols and parameter ABI shapes. +//! - Classes without constructors are treated as successful no-ops, matching PHP. +//! - Constructors are bridged for scalar/Mixed/array/object arguments, including +//! generated variadic array slots and supported scalar/Mixed by-reference parameters. +//! - Non-public constructors are accepted when the active eval class scope +//! satisfies PHP visibility. + +use std::collections::BTreeMap; + +use crate::codegen::abi; +use crate::codegen_support::try_handlers::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::intrinsics::IntrinsicCall; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{method_symbol, php_symbol_key}; +use crate::parser::ast::{ExprKind, Visibility}; +use crate::types::{ClassInfo, FunctionSig, PhpType}; + +use super::eval_ref_arg_helpers::{ + EvalRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, + emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, +}; +use super::eval_callable_helpers::EvalCallableDescriptorSupport; + +const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ + "Error", + "TypeError", + "ValueError", + "Exception", + "LogicException", + "BadFunctionCallException", + "BadMethodCallException", + "DomainException", + "InvalidArgumentException", + "LengthException", + "OutOfRangeException", + "RuntimeException", + "OutOfBoundsException", + "OverflowException", + "RangeException", + "UnderflowException", + "UnexpectedValueException", + "ReflectionException", + "JsonException", + "FiberError", +]; +const CONSTRUCTOR_HELPER_BASE_FRAME_SIZE: usize = 80; +const CONSTRUCTOR_HELPER_HANDLER_OFFSET: usize = CONSTRUCTOR_HELPER_BASE_FRAME_SIZE; +const CONSTRUCTOR_HELPER_FRAME_SIZE: usize = + CONSTRUCTOR_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const X86_64_CONSTRUCTOR_CONTEXT_FRAME_OFFSET: usize = 64; + +/// Constructor metadata needed by the eval constructor bridge. +#[derive(Clone)] +struct EvalConstructorSlot { + class_id: u64, + class_name: String, + impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, + params: Vec, + ref_params: Vec, + supported: bool, + runtime_helper: Option<&'static str>, + zero_default_first_arg: bool, +} + +/// Emits eval constructor helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_constructor_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_constructor_slots(module); + let builtin_throwable_class_ids = collect_builtin_throwable_constructor_class_ids(module); + emit_constructor_helper( + module, + emitter, + data, + &slots, + &builtin_throwable_class_ids, + callable_support, + ); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects AOT and runtime-backed constructors in stable class-id order. +fn collect_eval_constructor_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_constructor_slot(module, class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + +/// Collects compact builtin Throwable class ids that eval can initialize directly. +fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec { + let mut class_ids = BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES + .iter() + .filter_map(|class_name| module.class_infos.get(*class_name)) + .map(|class_info| class_info.class_id) + .collect::>(); + class_ids.sort_unstable(); + class_ids.dedup(); + class_ids +} + +/// Adds one constructor slot for a class when the constructor has emitted code or a runtime helper. +fn collect_class_constructor_slot( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + let method_key = php_symbol_key("__construct"); + let Some(sig) = class_info.methods.get(&method_key) else { + return; + }; + let impl_class = class_info + .method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + let runtime_helper = eval_runtime_backed_constructor_helper(class_name, &method_key); + if runtime_helper.is_none() + && !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) + { + return; + } + let visibility = constructor_visibility(class_info, &method_key); + let supported = + constructor_visibility_supported(visibility) && constructor_signature_supported(sig); + let params = if supported { + sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() + } else { + Vec::new() + }; + let ref_params = if supported { + eval_normalized_ref_params(sig.params.len(), &sig.ref_params) + } else { + Vec::new() + }; + slots.push(EvalConstructorSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), + params, + ref_params, + supported, + runtime_helper, + zero_default_first_arg: constructor_uses_zero_default_first_arg( + class_name, + &method_key, + sig, + runtime_helper, + ), + }); +} + +/// Returns a normal-ABI runtime helper for builtin constructors that eval can bridge. +fn eval_runtime_backed_constructor_helper( + class_name: &str, + method_key: &str, +) -> Option<&'static str> { + if class_name.trim_start_matches('\\') != "SplFixedArray" || method_key != "__construct" { + return None; + } + IntrinsicCall::instance_method(class_name, method_key)?.runtime_helper() +} + +/// Returns true when the first constructor parameter has PHP's builtin zero default. +fn constructor_uses_zero_default_first_arg( + class_name: &str, + method_key: &str, + sig: &FunctionSig, + runtime_helper: Option<&'static str>, +) -> bool { + runtime_helper.is_some() + && class_name.trim_start_matches('\\') == "SplFixedArray" + && method_key == "__construct" + && matches!(sig.params.first().map(|(_, ty)| ty.codegen_repr()), Some(PhpType::Int)) + && matches!( + sig.defaults.first().and_then(Option::as_ref).map(|expr| &expr.kind), + Some(ExprKind::IntLiteral(0)) + ) +} + +/// Returns the declared constructor visibility, defaulting to public metadata. +fn constructor_visibility<'a>(class_info: &'a ClassInfo, method_key: &str) -> &'a Visibility { + class_info + .method_visibilities + .get(method_key) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval constructor bridge can enforce this visibility. +fn constructor_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) +} + +/// Returns true for constructor signatures supported by this eval bridge slice. +fn constructor_signature_supported(sig: &FunctionSig) -> bool { + eval_signature_ref_params_supported(sig) + && sig + .params + .iter() + .all(|(_, ty)| constructor_param_supported(ty)) +} + +/// Returns true for one constructor argument type supported by the bridge. +fn constructor_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Iterable + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + ) +} + +/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*, scope, scope_len, ctx) -> bool`. +fn emit_constructor_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + emitter.blank(); + emitter.comment("--- eval bridge: user constructor call ---"); + label_c_global(module, emitter, "__elephc_eval_value_construct_object"); + match module.target.arch { + Arch::AArch64 => { + emit_constructor_aarch64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) + } + Arch::X86_64 => { + emit_constructor_x86_64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) + } + } + emit_take_pending_throwable_helper(module, emitter); +} + +/// Emits the ARM64 constructor helper body. +fn emit_constructor_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + let success_label = "__elephc_eval_value_construct_success"; + let fail_label = "__elephc_eval_value_construct_fail"; + let done_label = "__elephc_eval_value_construct_done"; + emitter.instruction(&format!("sub sp, sp, #{}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //reserve helper frame plus a boundary exception handler + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x2, [sp, #0]"); // save the active eval class-scope pointer + emitter.instruction("str x3, [sp, #8]"); // save the active eval class-scope length + emitter.instruction("str x1, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x4, [sp, #64]"); // save the active eval context for callable descriptors + emitter.instruction(&format!("cbz x0, {}", success_label)); // a null object pointer means there is nothing to construct + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", success_label)); // non-object values have no constructor to run + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for constructor calls + emit_aarch64_builtin_throwable_constructor_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + fail_label, + success_label, + callable_support, + ); + emit_aarch64_constructor_dispatch( + module, + emitter, + data, + slots, + fail_label, + success_label, + callable_support, + ); + emitter.instruction(&format!("b {}", success_label)); // no constructor metadata matched this class id + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report constructor dispatch failure to Rust + emitter.instruction(&format!("b {}", done_label)); // skip the success result after a failure + emitter.label(success_label); + emitter.instruction("mov x0, #1"); // report successful construction or no-op + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction(&format!("add sp, sp, #{}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //release the constructor helper frame and boundary handler + emitter.instruction("ret"); // return the constructor status flag to Rust +} + +/// Emits the x86_64 constructor helper body. +fn emit_constructor_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + let success_label = "__elephc_eval_value_construct_success_x"; + let fail_label = "__elephc_eval_value_construct_fail_x"; + let done_label = "__elephc_eval_value_construct_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction(&format!("sub rsp, {}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //reserve aligned slots plus a boundary exception handler + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the active eval class-scope length + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // save the active eval context for callable descriptors + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", success_label)); // a null object pointer means there is nothing to construct + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", success_label)); // non-object values have no constructor to run + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for constructor calls + emit_x86_64_builtin_throwable_constructor_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + fail_label, + success_label, + callable_support, + ); + emit_x86_64_constructor_dispatch( + module, + emitter, + data, + slots, + fail_label, + success_label, + callable_support, + ); + emitter.instruction(&format!("jmp {}", success_label)); // no constructor metadata matched this class id + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report constructor dispatch failure to Rust + emitter.instruction(&format!("jmp {}", done_label)); // skip the success result after a failure + emitter.label(success_label); + emitter.instruction("mov eax, 1"); // report successful construction or no-op + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the constructor status flag to Rust +} + +/// Emits an ARM64 boundary handler so native constructor throws return to magician. +fn emit_aarch64_constructor_exception_boundary_push(emitter: &mut Emitter, escape_label: &str) { + let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; + emitter.comment("push eval constructor exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across constructor unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "str x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "add x0, x29, #{}", + handler_offset + TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped +} + +/// Emits an ARM64 boundary pop that preserves the constructor status in x0. +fn emit_aarch64_constructor_exception_boundary_pop(emitter: &mut Emitter) { + let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; + emitter.comment("pop eval constructor exception boundary"); + emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); +} + +/// Emits an x86_64 boundary handler so native constructor throws return to magician. +fn emit_x86_64_constructor_exception_boundary_push(emitter: &mut Emitter, escape_label: &str) { + let handler_base = CONSTRUCTOR_HELPER_FRAME_SIZE; + emitter.comment("push eval constructor exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); //save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); //preserve the caller activation frame across constructor unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped +} + +/// Emits an x86_64 boundary pop that preserves the constructor status in rax. +fn emit_x86_64_constructor_exception_boundary_pop(emitter: &mut Emitter) { + let handler_base = CONSTRUCTOR_HELPER_FRAME_SIZE; + emitter.comment("pop eval constructor exception boundary"); + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); //reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); +} + +/// Emits a C helper that transfers `_exc_value` ownership to magician. +fn emit_take_pending_throwable_helper(module: &Module, emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- eval bridge: take pending throwable ---"); + label_c_global(module, emitter, "__elephc_eval_value_take_pending_throwable"); + match module.target.arch { + Arch::AArch64 => { + abi::emit_load_symbol_to_reg(emitter, "x0", "_exc_value", 0); + abi::emit_store_zero_to_symbol(emitter, "_exc_value", 0); + emitter.instruction("ret"); // return the pending Throwable pointer to magician + } + Arch::X86_64 => { + abi::emit_load_symbol_to_reg(emitter, "rax", "_exc_value", 0); + abi::emit_store_zero_to_symbol(emitter, "_exc_value", 0); + emitter.instruction("ret"); // return the pending Throwable pointer to magician + } + } +} + +/// Emits ARM64 dispatch for compact builtin Throwable constructors. +fn emit_aarch64_builtin_throwable_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this builtin class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for builtin constructor dispatch + abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_aarch64_builtin_throwable_constructor_body( + module, + emitter, + data, + fail_label, + success_label, + callable_support, + ); + emitter.label(&next_label); + } +} + +/// Emits x86_64 dispatch for compact builtin Throwable constructors. +fn emit_x86_64_builtin_throwable_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this builtin class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for builtin constructor dispatch + abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("jne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_x86_64_builtin_throwable_constructor_body( + module, + emitter, + data, + fail_label, + success_label, + callable_support, + ); + emitter.label(&next_label); + } +} + +/// Initializes the compact Throwable payload for eval-created ARM64 builtin exceptions. +fn emit_aarch64_builtin_throwable_constructor_body( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + emit_aarch64_validate_builtin_throwable_arg_count(module, emitter, fail_label); + emit_aarch64_default_builtin_throwable_fields(emitter); + emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the message argument + emitter.instruction("cmp x9, #0"); // did the eval call pass a message argument? + emitter.instruction(&format!("b.eq {}", success_label)); // keep the empty Throwable defaults when no message was supplied + emit_aarch64_load_eval_arg(module, emitter, 0); + emit_aarch64_cast_eval_arg( + module, + emitter, + &PhpType::Str, + "__elephc_eval_builtin_throwable_message", + fail_label, + data, + callable_support, + ); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization + emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload + emitter.instruction("str x2, [x9, #16]"); // store the message length in the compact Throwable payload + emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the code argument + emitter.instruction("cmp x9, #1"); // did the eval call pass a code argument? + emitter.instruction(&format!("b.le {}", success_label)); // keep code zero when only the message was supplied + emit_aarch64_load_eval_arg(module, emitter, 1); + emit_aarch64_cast_eval_arg( + module, + emitter, + &PhpType::Int, + "__elephc_eval_builtin_throwable_code", + fail_label, + data, + callable_support, + ); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization + emitter.instruction("str x0, [x9, #24]"); // store the integer exception code + emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed +} + +/// Initializes the compact Throwable payload for eval-created x86_64 builtin exceptions. +fn emit_x86_64_builtin_throwable_constructor_body( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + emit_x86_64_validate_builtin_throwable_arg_count(module, emitter, fail_label); + emit_x86_64_default_builtin_throwable_fields(emitter); + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload constructor argc before testing the message argument + emitter.instruction("cmp r11, 0"); // did the eval call pass a message argument? + emitter.instruction(&format!("je {}", success_label)); // keep the empty Throwable defaults when no message was supplied + emit_x86_64_load_eval_arg(module, emitter, 0); + emit_x86_64_cast_eval_arg( + module, + emitter, + &PhpType::Str, + "__elephc_eval_builtin_throwable_message_x", + fail_label, + data, + callable_support, + ); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for message initialization + emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store the message pointer in the compact Throwable payload + emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store the message length in the compact Throwable payload + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload constructor argc before testing the code argument + emitter.instruction("cmp r11, 1"); // did the eval call pass a code argument? + emitter.instruction(&format!("jle {}", success_label)); // keep code zero when only the message was supplied + emit_x86_64_load_eval_arg(module, emitter, 1); + emit_x86_64_cast_eval_arg( + module, + emitter, + &PhpType::Int, + "__elephc_eval_builtin_throwable_code_x", + fail_label, + data, + callable_support, + ); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization + emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code + emitter.instruction(&format!("jmp {}", success_label)); // builtin Throwable construction completed +} + +/// Emits ARM64 arity validation for compact builtin Throwable constructors. +fn emit_aarch64_validate_builtin_throwable_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for builtin Throwable arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("str x0, [sp, #32]"); // save constructor argc for message/code initialization + emitter.instruction("cmp x0, #2"); // compact Throwable initialization supports message and code arguments + emitter.instruction(&format!("b.gt {}", fail_label)); // reject unsupported previous-Throwable arguments from eval +} + +/// Emits x86_64 arity validation for compact builtin Throwable constructors. +fn emit_x86_64_validate_builtin_throwable_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for builtin Throwable arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save constructor argc for message/code initialization + emitter.instruction("cmp rax, 2"); // compact Throwable initialization supports message and code arguments + emitter.instruction(&format!("jg {}", fail_label)); // reject unsupported previous-Throwable arguments from eval +} + +/// Writes ARM64 empty-message and zero-code defaults into the compact Throwable payload. +fn emit_aarch64_default_builtin_throwable_fields(emitter: &mut Emitter) { + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for default initialization + emitter.instruction("str xzr, [x9, #8]"); // default the message pointer to an empty string payload + emitter.instruction("str xzr, [x9, #16]"); // default the message length to zero + emitter.instruction("str xzr, [x9, #24]"); // default the exception code to zero +} + +/// Writes x86_64 empty-message and zero-code defaults into the compact Throwable payload. +fn emit_x86_64_default_builtin_throwable_fields(emitter: &mut Emitter) { + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for default initialization + emitter.instruction("mov QWORD PTR [r11 + 8], 0"); // default the message pointer to an empty string payload + emitter.instruction("mov QWORD PTR [r11 + 16], 0"); // default the message length to zero + emitter.instruction("mov QWORD PTR [r11 + 24], 0"); // default the exception code to zero +} + +/// Emits ARM64 class-id dispatch for supported constructor bodies. +fn emit_aarch64_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalConstructorSlot], + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_constructor_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for constructor dispatch + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this constructor class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next constructor class when ids differ + for slot in class_slots { + emit_aarch64_constructor_body( + module, + emitter, + data, + slot, + fail_label, + success_label, + callable_support, + ); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id dispatch for supported constructor bodies. +fn emit_x86_64_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalConstructorSlot], + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_constructor_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for constructor dispatch + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this constructor class + emitter.instruction(&format!("jne {}", next_label)); // try the next constructor class when ids differ + for slot in class_slots { + emit_x86_64_constructor_body( + module, + emitter, + data, + slot, + fail_label, + success_label, + callable_support, + ); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 constructor body or failure branch for an unsupported constructor. +fn emit_aarch64_constructor_body( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + if !slot.supported { + emitter.instruction(&format!("b {}", fail_label)); // reject constructors outside this bridge's supported ABI slice + return; + } + if !matches!(slot.visibility, Visibility::Public) { + let scope_ok_label = constructor_scope_ok_label(module, slot); + emit_aarch64_constructor_scope_check(emitter, data, slot, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + } + emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); + let body_label = constructor_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); + let (arg_temp_bytes, ref_slots) = + emit_aarch64_prepare_constructor_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape", body_label); + emit_aarch64_constructor_exception_boundary_push(emitter, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, "__construct")); + abi::emit_call_label(emitter, &callee); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_aarch64_write_back_ref_args( + emitter, + &ref_slots, + 0, + &body_label, + ); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("b {}", success_label)); // constructor returned normally + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_constructor_prep_fail_cleanup(emitter, fail_label); +} + +/// Emits one x86_64 constructor body or failure branch for an unsupported constructor. +fn emit_x86_64_constructor_body( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + fail_label: &str, + success_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + if !slot.supported { + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructors outside this bridge's supported ABI slice + return; + } + if !matches!(slot.visibility, Visibility::Public) { + let scope_ok_label = constructor_scope_ok_label(module, slot); + emit_x86_64_constructor_scope_check(emitter, data, slot, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + } + emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); + let body_label = constructor_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); + let (arg_temp_bytes, ref_slots) = + emit_x86_64_prepare_constructor_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape_x", body_label); + emit_x86_64_constructor_exception_boundary_push(emitter, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, "__construct")); + abi::emit_call_label(emitter, &callee); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_x86_64_write_back_ref_args( + emitter, + &ref_slots, + 0, + &body_label, + ); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_constructor_prep_fail_cleanup(emitter, fail_label); +} + +/// Restores an ARM64 constructor-helper frame before reporting an argument-prep fatal. +fn emit_aarch64_constructor_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("sub sp, x29, #48"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("b {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Restores an x86_64 constructor-helper frame before reporting an argument-prep fatal. +fn emit_x86_64_constructor_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("mov rsp, rbp"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("jmp {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Emits ARM64 visibility checks for a protected/private constructor bridge hit. +fn emit_aarch64_constructor_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + success_label: &str, + fail_label: &str, +) { + emitter.instruction("ldr x1, [sp, #0]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped constructor access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // run the constructor when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", fail_label)); // reject constructor access from unrelated classes +} + +/// Emits x86_64 visibility checks for a protected/private constructor bridge hit. +fn emit_x86_64_constructor_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + success_label: &str, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped constructor access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // run the constructor when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructor access from unrelated classes +} + +/// Emits ARM64 arity validation for one constructor body. +fn emit_aarch64_validate_constructor_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("str x0, [sp, #32]"); // save the supplied constructor argument count + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the constructor signature + if slot.zero_default_first_arg { + let ok_label = format!("{}_argc_ok", constructor_body_label(module, slot)); + emitter.instruction(&format!("b.eq {}", ok_label)); // explicit argument count matches the constructor signature + emitter.instruction("cmp x0, #0"); // did eval omit the optional builtin constructor argument? + emitter.instruction(&format!("b.eq {}", ok_label)); // accept the builtin zero-default constructor call + emitter.instruction(&format!("b {}", fail_label)); // reject constructor dispatch when arity differs + emitter.label(&ok_label); + } else { + emitter.instruction(&format!("b.ne {}", fail_label)); // reject constructor dispatch when arity differs + } +} + +/// Emits x86_64 arity validation for one constructor body. +fn emit_x86_64_validate_constructor_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the supplied constructor argument count + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the constructor signature + if slot.zero_default_first_arg { + let ok_label = format!("{}_argc_ok", constructor_body_label(module, slot)); + emitter.instruction(&format!("je {}", ok_label)); // explicit argument count matches the constructor signature + emitter.instruction("test rax, rax"); // did eval omit the optional builtin constructor argument? + emitter.instruction(&format!("je {}", ok_label)); // accept the builtin zero-default constructor call + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructor dispatch when arity differs + emitter.label(&ok_label); + } else { + emitter.instruction(&format!("jne {}", fail_label)); // reject constructor dispatch when arity differs + } +} + +/// Prepares ARM64 constructor argument temporaries for the supported argument shapes. +fn emit_aarch64_prepare_constructor_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = constructor_body_label(module, slot); + let ref_slots = emit_aarch64_constructor_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + callable_support, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("ldr x0, [x29, #-32]"); // load the unboxed receiver as the first constructor argument + abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + if slot.zero_default_first_arg && index == 0 { + let default_label = format!("{}_arg_{}_default", body_label, index); + let done_label = format!("{}_arg_{}_done", body_label, index); + emitter.instruction("ldr x9, [sp, #32]"); // reload argc before selecting the optional constructor default + emitter.instruction(&format!("cbz x9, {}", default_label)); // omitted SplFixedArray size uses PHP's zero default + emit_aarch64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_aarch64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + emitter.instruction(&format!("b {}", done_label)); // skip default materialization after an explicit argument + emitter.label(&default_label); + abi::emit_load_int_immediate(emitter, "x0", 0); + abi::emit_push_result_value(emitter, &PhpType::Int); + emitter.label(&done_label); + } else if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_aarch64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Prepares x86_64 constructor argument temporaries for the supported argument shapes. +fn emit_x86_64_prepare_constructor_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = constructor_body_label(module, slot); + let ref_slots = emit_x86_64_constructor_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + callable_support, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first constructor argument + abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + if slot.zero_default_first_arg && index == 0 { + let default_label = format!("{}_arg_{}_default", body_label, index); + let done_label = format!("{}_arg_{}_done", body_label, index); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload argc before selecting the optional constructor default + emitter.instruction("test r10, r10"); // did eval pass an explicit constructor argument? + emitter.instruction(&format!("jz {}", default_label)); // omitted SplFixedArray size uses PHP's zero default + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_x86_64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + emitter.instruction(&format!("jmp {}", done_label)); // skip default materialization after an explicit argument + emitter.label(&default_label); + abi::emit_load_int_immediate(emitter, "rax", 0); + abi::emit_push_result_value(emitter, &PhpType::Int); + emitter.label(&done_label); + } else if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_x86_64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Materializes the pushed receiver and eval arguments into the target method ABI. +fn materialize_constructor_args( + module: &Module, + emitter: &mut Emitter, + receiver_ty: &PhpType, + params: &[PhpType], + ref_params: &[bool], +) -> usize { + let mut arg_types = Vec::with_capacity(params.len() + 1); + arg_types.push(receiver_ty.clone()); + arg_types.extend(eval_abi_param_types_for_refs(params, ref_params)); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + +/// Prepares ARM64 stack cells for eval-supplied by-reference constructor arguments. +fn emit_aarch64_constructor_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], + ref_params: &[bool], + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); + for slot in &ref_slots { + emit_aarch64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_aarch64_cast_eval_arg( + module, + emitter, + &slot.param_ty, + &arg_label, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } + } + ref_slots +} + +/// Prepares x86_64 stack cells for eval-supplied by-reference constructor arguments. +fn emit_x86_64_constructor_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], + ref_params: &[bool], + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); + for slot in &ref_slots { + emit_x86_64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_x86_64_cast_eval_arg( + module, + emitter, + &slot.param_ty, + &arg_label, + fail_label, + data, + callable_support, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } + } + ref_slots +} + +/// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. +fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "x0", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array + emitter.instruction("ldr x1, [x29, #-16]"); // pass the boxed index to the eval array reader + emitter.instruction("ldr x0, [x29, #-24]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed eval argument for coercion +} + +/// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. +fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "rdi", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the boxed index to the eval array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed eval argument for coercion +} + +/// Casts one boxed eval argument into ARM64 result registers for temporary staging. +fn emit_aarch64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, +) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("bl __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in d0 + } + PhpType::Str => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 + } + PhpType::Callable => { + super::eval_callable_helpers::emit_aarch64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } + PhpType::TaggedScalar => { + emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } + PhpType::Mixed => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed constructor parameter + } + PhpType::Object(_) => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the eval argument is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // reject malformed non-object constructor arguments + emitter.instruction("mov x0, x1"); // move the unboxed object payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); + } + PhpType::Array(_) => { + emit_aarch64_cast_eval_array_arg(emitter, param_ty, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); + } + PhpType::Iterable => { + emit_aarch64_cast_eval_iterable_arg(module, emitter, param_ty, label_prefix, fail_label); + } + _ => {} + } +} + +/// Coerces one ARM64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_aarch64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed eval argument across tag inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("ldr x0, [sp]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("add sp, sp, #16"); // discard the preserved boxed eval argument +} + +/// Validates and unboxes one ARM64 array-typed eval argument for native constructors. +fn emit_aarch64_cast_eval_array_arg( + emitter: &mut Emitter, + param_ty: &PhpType, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for array unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval array payload for the constructor ABI + abi::emit_load_int_immediate(emitter, "x9", expected_tag); + emitter.instruction("cmp x0, x9"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed array payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates and unboxes one ARM64 iterable-typed eval argument for native constructors. +fn emit_aarch64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array + emitter.instruction(&format!("b.eq {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp x0, #6"); // runtime tag 6 means object + emitter.instruction(&format!("b.eq {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("b {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov x0, x1"); // move the array payload into the result register + emitter.instruction(&format!("b {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_aarch64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + emitter.instruction("ldr x0, [sp], #16"); // restore the iterable object pointer as the result + emitter.label(&done); + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates the ARM64 object payload saved in `x1` against Traversable interfaces. +fn emit_aarch64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("b {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + emitter.instruction("str x1, [sp, #-16]!"); // preserve the object payload across Traversable checks + for interface_id in interface_ids { + emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "x1", interface_id as i64); + abi::emit_load_int_immediate(emitter, "x2", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("cmp x0, #0"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("b.ne {}", object_ok)); // matching Iterator metadata accepts the object + } + emitter.instruction("add sp, sp, #16"); // discard the rejected object payload + emitter.instruction(&format!("b {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + +/// Casts one boxed eval argument into x86_64 result registers for temporary staging. +fn emit_x86_64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, +) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("call __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in xmm0 + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair + } + PhpType::Callable => { + super::eval_callable_helpers::emit_x86_64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + X86_64_CONSTRUCTOR_CONTEXT_FRAME_OFFSET, + ); + } + PhpType::TaggedScalar => { + emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } + PhpType::Mixed => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed constructor parameter + } + PhpType::Object(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the eval argument is an object + emitter.instruction(&format!("jne {}", fail_label)); // reject malformed non-object constructor arguments + emitter.instruction("mov rax, rdi"); // move the unboxed object payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); + } + PhpType::Array(_) => { + emit_x86_64_cast_eval_array_arg(emitter, param_ty, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); + } + PhpType::Iterable => { + emit_x86_64_cast_eval_iterable_arg(module, emitter, param_ty, label_prefix, fail_label); + } + _ => {} + } +} + +/// Coerces one x86_64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_x86_64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); +} + +/// Validates and unboxes one x86_64 array-typed eval argument for native constructors. +fn emit_x86_64_cast_eval_array_arg( + emitter: &mut Emitter, + param_ty: &PhpType, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for array unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the eval array payload for the constructor ABI + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed array payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates and unboxes one x86_64 iterable-typed eval argument for native constructors. +fn emit_x86_64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array + emitter.instruction(&format!("je {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp rax, 6"); // runtime tag 6 means object + emitter.instruction(&format!("je {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("jmp {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov rax, rdi"); // move the array payload into the result register + emitter.instruction(&format!("jmp {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_x86_64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + abi::emit_pop_reg(emitter, "rax"); + emitter.label(&done); + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates the x86_64 object payload saved in `rdi` against Traversable interfaces. +fn emit_x86_64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("jmp {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + abi::emit_push_reg(emitter, "rdi"); + for interface_id in interface_ids { + emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "rsi", interface_id as i64); + abi::emit_load_int_immediate(emitter, "rdx", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("test rax, rax"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("jne {}", object_ok)); // matching Iterator metadata accepts the object + } + abi::emit_pop_reg(emitter, "r10"); + emitter.instruction(&format!("jmp {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + +/// Groups constructor slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Returns a label-safe constructor body prefix for bridge-local writeback branches. +fn constructor_body_label(module: &Module, slot: &EvalConstructorSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("__elephc_eval_constructor_{}{}", slot.class_id, suffix) +} + +/// Returns a platform-safe label for a successful scoped constructor access check. +fn constructor_scope_ok_label(module: &Module, slot: &EvalConstructorSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("__elephc_eval_constructor_{}_scope_ok{}", slot.class_id, suffix) +} + +/// Returns runtime interface ids for object values accepted by PHP iterable parameters. +fn traversable_interface_ids(module: &Module) -> Vec { + ["Iterator", "IteratorAggregate"] + .into_iter() + .filter_map(|name| module.interface_infos.get(name).map(|info| info.interface_id)) + .collect() +} + +/// Returns class scopes that satisfy one constructor visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Emits a platform-C global label for a user assembly helper. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + emitter.label_global(&module.target.extern_symbol(name)); +} diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs new file mode 100644 index 0000000000..a9b146f586 --- /dev/null +++ b/src/codegen/eval_method_helpers.rs @@ -0,0 +1,2414 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician call native instance and +//! static methods known to the current module. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user class ids, method symbols, +//! or return types, so this bridge is emitted into the user assembly. +//! - This method-call slice supports public methods plus protected/private +//! methods when the active eval class scope satisfies PHP visibility. + +use std::collections::BTreeMap; + +use crate::codegen::abi; +use crate::codegen_support::try_handlers::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::emit_box_current_value_as_mixed; +use crate::codegen::platform::Arch; +use crate::intrinsics::IntrinsicCall; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{method_symbol, static_method_symbol}; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +use super::eval_ref_arg_helpers::{ + EvalRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, + emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, +}; +use super::eval_callable_helpers::EvalCallableDescriptorSupport; + +/// Method metadata needed by eval method-call bridge dispatch. +#[derive(Clone)] +struct EvalMethodSlot { + class_id: u64, + class_name: String, + method: String, + impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, + params: Vec, + ref_params: Vec, + return_ty: PhpType, + is_hidden_shadow: bool, + runtime_helper: Option<&'static str>, +} + +/// Static method metadata needed by eval static method-call bridge dispatch. +#[derive(Clone)] +struct EvalStaticMethodSlot { + class_id: u64, + class_name: String, + method: String, + impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, + params: Vec, + ref_params: Vec, + return_ty: PhpType, +} + +const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ + "Error", + "TypeError", + "ValueError", + "Exception", + "LogicException", + "BadFunctionCallException", + "BadMethodCallException", + "DomainException", + "InvalidArgumentException", + "LengthException", + "OutOfRangeException", + "RuntimeException", + "OutOfBoundsException", + "OverflowException", + "RangeException", + "UnderflowException", + "UnexpectedValueException", + "ReflectionException", + "JsonException", + "FiberError", +]; +const BUILTIN_THROWABLE_GET_MESSAGE_LABEL: &str = "__elephc_eval_builtin_throwable_getmessage"; +const BUILTIN_THROWABLE_GET_CODE_LABEL: &str = "__elephc_eval_builtin_throwable_getcode"; +const METHOD_HELPER_BASE_FRAME_SIZE: usize = 80; +const METHOD_HELPER_HANDLER_OFFSET: usize = METHOD_HELPER_BASE_FRAME_SIZE; +const METHOD_HELPER_FRAME_SIZE: usize = METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const STATIC_METHOD_HELPER_BASE_FRAME_SIZE: usize = 96; +const STATIC_METHOD_HELPER_HANDLER_OFFSET: usize = STATIC_METHOD_HELPER_BASE_FRAME_SIZE; +const STATIC_METHOD_HELPER_FRAME_SIZE: usize = + STATIC_METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const X86_64_METHOD_CONTEXT_FRAME_OFFSET: usize = 64; +const X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET: usize = 72; + +/// Emits eval method-call helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_method_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_method_slots(module); + let static_slots = collect_eval_static_method_slots(module); + let builtin_throwable_class_ids = collect_builtin_throwable_method_class_ids(module); + emit_method_call_helper( + module, + emitter, + data, + &slots, + &builtin_throwable_class_ids, + callable_support, + ); + emit_static_method_call_helper(module, emitter, data, &static_slots, callable_support); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects bridge-supported instance methods backed by emitted EIR symbols. +fn collect_eval_method_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_method_slots(module, class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + +/// Collects bridge-supported static methods backed by emitted EIR symbols. +fn collect_eval_static_method_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_static_method_slots(module, class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + +/// Collects compact builtin Throwable class ids that eval can inspect directly. +fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { + let mut class_ids = BUILTIN_THROWABLE_METHOD_CLASSES + .iter() + .filter_map(|class_name| module.class_infos.get(*class_name)) + .map(|class_info| class_info.class_id) + .collect::>(); + class_ids.sort_unstable(); + class_ids.dedup(); + class_ids +} + +/// Adds bridge-supported instance methods for one class. +fn collect_class_method_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + collect_hidden_private_ancestor_method_slots( + module, + class_name, + class_info, + emitted_methods, + slots, + ); + let mut methods = class_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + let visibility = method_visibility(class_info, method); + if !method_visibility_supported(visibility) + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = class_info + .method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(class_name); + let runtime_helper = eval_runtime_backed_instance_method_helper(class_name, method); + if runtime_helper.is_none() + && !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) + { + continue; + } + slots.push(EvalMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), + return_ty: sig.return_type.codegen_repr(), + is_hidden_shadow: false, + runtime_helper, + }); + } +} + +/// Adds private ancestor instance methods hidden by descendant class method lookup. +fn collect_hidden_private_ancestor_method_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + for (ancestor_name, ancestor_info) in class_ancestry(module, class_name) { + if ancestor_name == class_name { + continue; + } + let mut methods = ancestor_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + let visibility = method_visibility(ancestor_info, method); + if visibility != &Visibility::Private + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = ancestor_info + .method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(ancestor_name); + if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) { + continue; + } + slots.push(EvalMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), + return_ty: sig.return_type.codegen_repr(), + is_hidden_shadow: true, + runtime_helper: None, + }); + } + } +} + +/// Returns a normal-ABI runtime helper for builtin instance methods that eval can bridge. +fn eval_runtime_backed_instance_method_helper( + class_name: &str, + method_name: &str, +) -> Option<&'static str> { + if !matches!( + class_name.trim_start_matches('\\'), + "SplDoublyLinkedList" | "SplStack" | "SplQueue" | "SplFixedArray" + ) { + return None; + } + IntrinsicCall::instance_method(class_name, method_name)?.runtime_helper() +} + +/// Adds bridge-supported static methods for one class. +fn collect_class_static_method_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + let mut methods = class_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + let visibility = static_method_visibility(class_info, method); + if !method_visibility_supported(visibility) + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = class_info + .static_method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(class_name); + if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), true)) { + continue; + } + slots.push(EvalStaticMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), + return_ty: sig.return_type.codegen_repr(), + }); + } +} + +/// Returns the declared instance-method visibility, defaulting to public metadata. +fn method_visibility<'a>(class_info: &'a ClassInfo, method: &str) -> &'a Visibility { + class_info + .method_visibilities + .get(method) + .unwrap_or(&Visibility::Public) +} + +/// Returns the declared static-method visibility, defaulting to public metadata. +fn static_method_visibility<'a>(class_info: &'a ClassInfo, method: &str) -> &'a Visibility { + class_info + .static_method_visibilities + .get(method) + .unwrap_or(&Visibility::Public) +} + +/// Returns class metadata from root parent to the requested class. +fn class_ancestry<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut chain = Vec::new(); + collect_class_ancestry(module, class_name, &mut chain); + chain +} + +/// Recursively collects class metadata from parent to child. +fn collect_class_ancestry<'a>( + module: &'a Module, + class_name: &'a str, + chain: &mut Vec<(&'a str, &'a ClassInfo)>, +) { + let Some(class_info) = module.class_infos.get(class_name) else { + return; + }; + if let Some(parent) = class_info.parent.as_deref() { + collect_class_ancestry(module, parent, chain); + } + chain.push((class_name, class_info)); +} + +/// Returns true when the eval method bridge can enforce this visibility. +fn method_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) +} + +/// Returns true for method signatures supported by the eval bridge. +fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { + eval_signature_ref_params_supported(sig) + && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) +} + +/// Returns true for an eval-supplied method argument type supported by this bridge. +fn method_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Iterable + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + ) +} + +/// Returns true for return storage shapes the bridge can box for eval. +fn method_return_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Void + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Iterable + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*, scope, scope_len, ctx) -> Mixed*`. +fn emit_method_call_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + emitter.blank(); + emitter.comment("--- eval bridge: user method call ---"); + label_c_global(module, emitter, "__elephc_eval_value_method_call"); + match module.target.arch { + Arch::AArch64 => { + emit_method_call_aarch64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) + } + Arch::X86_64 => { + emit_method_call_x86_64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) + } + } +} + +/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*, scope, scope_len, ctx) -> Mixed*`. +fn emit_static_method_call_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static method call ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_method_call"); + match module.target.arch { + Arch::AArch64 => { + emit_static_method_call_aarch64(module, emitter, data, slots, callable_support) + } + Arch::X86_64 => { + emit_static_method_call_x86_64(module, emitter, data, slots, callable_support) + } + } +} + +/// Emits the ARM64 static method-call helper body. +fn emit_static_method_call_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, +) { + let fail_label = "__elephc_eval_value_static_method_call_fail"; + let done_label = "__elephc_eval_value_static_method_call_done"; + emitter.instruction(&format!("sub sp, sp, #{}", STATIC_METHOD_HELPER_FRAME_SIZE)); // reserve helper frame plus a boundary exception handler + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x4, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x5, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x3, [sp, #40]"); // save the requested method-name length + emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length + emitter.instruction("str x7, [sp, #80]"); // save the active eval context for callable descriptors + emit_aarch64_static_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported static method matched the request + emit_aarch64_static_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction(&format!("add sp, sp, #{}", STATIC_METHOD_HELPER_FRAME_SIZE)); // release the helper frame and boundary handler + emitter.instruction("ret"); // return the boxed static method result to Rust +} + +/// Emits the x86_64 static method-call helper body. +fn emit_static_method_call_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, +) { + let fail_label = "__elephc_eval_value_static_method_call_fail_x"; + let done_label = "__elephc_eval_value_static_method_call_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction(&format!("sub rsp, {}", STATIC_METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the requested method-name length + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval class-scope length + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the active eval context stack argument + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the active eval context for callable descriptors + emit_x86_64_static_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported static method matched the request + emit_x86_64_static_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed static method result to Rust +} + +/// Emits the ARM64 method-call helper body. +fn emit_method_call_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + let fail_label = "__elephc_eval_value_method_call_fail"; + let done_label = "__elephc_eval_value_method_call_done"; + emitter.instruction(&format!("sub sp, sp, #{}", METHOD_HELPER_FRAME_SIZE)); // reserve helper frame plus a boundary exception handler + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested method-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested method-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emitter.instruction("str x6, [sp, #64]"); // save the active eval context for callable descriptors + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot dispatch a method + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot dispatch instance methods + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for method calls + emit_aarch64_builtin_throwable_method_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + ); + emit_aarch64_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported method matched the request + emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); + emit_aarch64_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction(&format!("add sp, sp, #{}", METHOD_HELPER_FRAME_SIZE)); // release the helper frame and boundary handler + emitter.instruction("ret"); // return the boxed method result to Rust +} + +/// Emits the x86_64 method-call helper body. +fn emit_method_call_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, +) { + let fail_label = "__elephc_eval_value_method_call_fail_x"; + let done_label = "__elephc_eval_value_method_call_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction(&format!("sub rsp, {}", METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested method-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval context stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval context for callable descriptors + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot dispatch a method + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot dispatch instance methods + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for method calls + emit_x86_64_builtin_throwable_method_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + ); + emit_x86_64_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported method matched the request + emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); + emit_x86_64_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed method result to Rust +} + +/// Emits an ARM64 boundary handler so native method throws return to magician. +fn emit_aarch64_method_exception_boundary_push( + emitter: &mut Emitter, + handler_offset: usize, + escape_label: &str, +) { + emitter.comment("push eval method exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across method unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "str x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "add x0, x29, #{}", + handler_offset + TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native methods + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a method Throwable escaped +} + +/// Emits an ARM64 boundary pop after a native method call returns to magician. +fn emit_aarch64_method_exception_boundary_pop(emitter: &mut Emitter, handler_offset: usize) { + emitter.comment("pop eval method exception boundary"); + emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); +} + +/// Emits an x86_64 boundary handler so native method throws return to magician. +fn emit_x86_64_method_exception_boundary_push( + emitter: &mut Emitter, + handler_base: usize, + escape_label: &str, +) { + emitter.comment("push eval method exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); // preserve the caller activation frame across method unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native methods + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a method Throwable escaped +} + +/// Emits an x86_64 boundary pop after a native method call returns to magician. +fn emit_x86_64_method_exception_boundary_pop(emitter: &mut Emitter, handler_base: usize) { + emitter.comment("pop eval method exception boundary"); + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); +} + +/// Emits ARM64 class-id and method-name dispatch for helper method bodies. +fn emit_aarch64_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + fail_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_method_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for method dispatch + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_aarch64_method_name_compare(module, emitter, data, slot, fail_label); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and method-name dispatch for helper method bodies. +fn emit_x86_64_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + fail_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_method_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for method dispatch + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_x86_64_method_name_compare(module, emitter, data, slot, fail_label); + } + emitter.label(&next_label); + } +} + +/// Emits ARM64 class-name and method-name dispatch for static method helper bodies. +fn emit_aarch64_static_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + fail_label: &str, +) { + for (class_name, class_slots) in grouped_static_slots(slots) { + let next_label = format!( + "__elephc_eval_static_method_next_{}", + label_fragment(class_name) + ); + emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_aarch64_static_method_name_compare(module, emitter, data, slot, fail_label); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and method-name dispatch for static method helper bodies. +fn emit_x86_64_static_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + fail_label: &str, +) { + for (class_name, class_slots) in grouped_static_slots(slots) { + let next_label = format!( + "__elephc_eval_static_method_next_{}_x", + label_fragment(class_name) + ); + emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_x86_64_static_method_name_compare(module, emitter, data, slot, fail_label); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison for a static method group. +fn emit_aarch64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // try the next class when names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison for a static method group. +fn emit_x86_64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // try the next class when names differ +} + +/// Emits ARM64 class-id and method-name dispatch for compact Throwable methods. +fn emit_aarch64_builtin_throwable_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_method_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this builtin Throwable test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for builtin Throwable dispatch + abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_aarch64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getmessage", + BUILTIN_THROWABLE_GET_MESSAGE_LABEL, + ); + emit_aarch64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getcode", + BUILTIN_THROWABLE_GET_CODE_LABEL, + ); + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and method-name dispatch for compact Throwable methods. +fn emit_x86_64_builtin_throwable_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_method_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this builtin Throwable test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for builtin Throwable dispatch + abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("jne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_x86_64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getmessage", + BUILTIN_THROWABLE_GET_MESSAGE_LABEL, + ); + emit_x86_64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getcode", + BUILTIN_THROWABLE_GET_CODE_LABEL, + ); + emitter.label(&next_label); + } +} + +/// Emits one ARM64 method-name comparison for a compact Throwable method. +fn emit_aarch64_builtin_throwable_method_name_branch( + _module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + method_key: &str, + target_label: &str, +) { + let (label, len) = data.add_string(method_key.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested method-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare Throwable method names with PHP case-insensitive rules + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the compact Throwable method when names match +} + +/// Emits one x86_64 method-name comparison for a compact Throwable method. +fn emit_x86_64_builtin_throwable_method_name_branch( + _module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + method_key: &str, + target_label: &str, +) { + let (label, len) = data.add_string(method_key.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare Throwable method names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the method names matched + emitter.instruction(&format!("je {}", target_label)); // dispatch to the compact Throwable method when names match +} + +/// Emits one ARM64 method-name comparison and branch to the matching body. +fn emit_aarch64_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested method-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + let target_label = method_body_label(module, slot); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the method body when the names match + return; + } + let miss_label = method_access_miss_label(module, slot); + emitter.instruction(&format!("cbnz x0, {}", miss_label)); // continue method dispatch when names differ + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_aarch64_method_scope_check( + emitter, + data, + &slot.allowed_scopes, + false, + &target_label, + scope_fail_label, + ); + emitter.label(&miss_label); +} + +/// Emits one x86_64 method-name comparison and branch to the matching body. +fn emit_x86_64_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the method names matched + let target_label = method_body_label(module, slot); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("je {}", target_label)); // dispatch to the method body when the names match + return; + } + let miss_label = method_access_miss_label(module, slot); + emitter.instruction(&format!("jne {}", miss_label)); // continue method dispatch when names differ + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_x86_64_method_scope_check( + emitter, + data, + &slot.allowed_scopes, + false, + &target_label, + scope_fail_label, + ); + emitter.label(&miss_label); +} + +/// Emits one ARM64 static method-name comparison and branch to the matching body. +fn emit_aarch64_static_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested static method-name pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload requested static method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules + let target_label = static_method_body_label(module, slot); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the static method body when the names match + return; + } + let miss_label = static_method_access_miss_label(module, slot); + emitter.instruction(&format!("cbnz x0, {}", miss_label)); // continue static method dispatch when names differ + emit_aarch64_method_scope_check(emitter, data, &slot.allowed_scopes, true, &target_label, fail_label); + emitter.label(&miss_label); +} + +/// Emits one x86_64 static method-name comparison and branch to the matching body. +fn emit_x86_64_static_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested static method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload requested static method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the static method names matched + let target_label = static_method_body_label(module, slot); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("je {}", target_label)); // dispatch to the static method body when the names match + return; + } + let miss_label = static_method_access_miss_label(module, slot); + emitter.instruction(&format!("jne {}", miss_label)); // continue static method dispatch when names differ + emit_x86_64_method_scope_check(emitter, data, &slot.allowed_scopes, true, &target_label, fail_label); + emitter.label(&miss_label); +} + +/// Emits ARM64 visibility checks for a protected/private method bridge hit. +fn emit_aarch64_method_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + allowed_scopes: &[String], + is_static: bool, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_method_scope_offsets(is_static); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped method access outside a class scope + for scope_name in allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", fail_label)); // reject scoped method access from unrelated classes +} + +/// Emits x86_64 visibility checks for a protected/private method bridge hit. +fn emit_x86_64_method_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + allowed_scopes: &[String], + is_static: bool, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_method_scope_offsets(is_static); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped method access outside a class scope + for scope_name in allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped method access from unrelated classes +} + +/// Returns ARM64 stack offsets for method class-scope pointer and length. +fn aarch64_method_scope_offsets(is_static: bool) -> (usize, usize) { + if is_static { + (32, 48) + } else { + (32, 40) + } +} + +/// Returns x86_64 frame offsets for method class-scope pointer and length. +fn x86_64_method_scope_offsets(is_static: bool) -> (usize, usize) { + if is_static { + (56, 64) + } else { + (48, 56) + } +} + +/// Emits ARM64 bodies for compact Throwable methods used by eval. +fn emit_aarch64_builtin_throwable_method_bodies( + module: &Module, + emitter: &mut Emitter, + done_label: &str, + fail_label: &str, +) { + emitter.label(BUILTIN_THROWABLE_GET_MESSAGE_LABEL); + emit_aarch64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for getMessage() + emitter.instruction("ldr x1, [x9, #8]"); // load Throwable message pointer + emitter.instruction("ldr x2, [x9, #16]"); // load Throwable message length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // box the Throwable message as a Mixed string + emitter.instruction(&format!("b {}", done_label)); // return the boxed Throwable method result + + emitter.label(BUILTIN_THROWABLE_GET_CODE_LABEL); + emit_aarch64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for getCode() + emitter.instruction("ldr x1, [x9, #24]"); // load Throwable integer code + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the Throwable code as a Mixed integer + emitter.instruction(&format!("b {}", done_label)); // return the boxed Throwable method result +} + +/// Emits x86_64 bodies for compact Throwable methods used by eval. +fn emit_x86_64_builtin_throwable_method_bodies( + module: &Module, + emitter: &mut Emitter, + done_label: &str, + fail_label: &str, +) { + emitter.label(BUILTIN_THROWABLE_GET_MESSAGE_LABEL); + emit_x86_64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for getMessage() + emitter.instruction("mov rdi, QWORD PTR [r10 + 8]"); // load Throwable message pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 16]"); // load Throwable message length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the Throwable message as a Mixed string + emitter.instruction(&format!("jmp {}", done_label)); // return the boxed Throwable method result + + emitter.label(BUILTIN_THROWABLE_GET_CODE_LABEL); + emit_x86_64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for getCode() + emitter.instruction("mov rdi, QWORD PTR [r10 + 24]"); // load Throwable integer code + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("xor eax, eax"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the Throwable code as a Mixed integer + emitter.instruction(&format!("jmp {}", done_label)); // return the boxed Throwable method result +} + +/// Emits ARM64 zero-argument validation for compact Throwable eval methods. +fn emit_aarch64_validate_builtin_throwable_method_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for Throwable method arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("cmp x0, #0"); // compact Throwable methods accept no eval arguments + emitter.instruction(&format!("b.ne {}", fail_label)); // reject unsupported Throwable method arguments from eval +} + +/// Emits x86_64 zero-argument validation for compact Throwable eval methods. +fn emit_x86_64_validate_builtin_throwable_method_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for Throwable method arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("test rax, rax"); // compact Throwable methods accept no eval arguments + emitter.instruction(&format!("jne {}", fail_label)); // reject unsupported Throwable method arguments from eval +} + +/// Emits ARM64 method-call bodies for every bridge-supported method. +fn emit_aarch64_method_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + done_label: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for slot in slots { + let body_label = method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); + emitter.label(&body_label); + emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); + let (arg_temp_bytes, ref_slots) = + emit_aarch64_prepare_method_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape", body_label); + emit_aarch64_method_exception_boundary_push( + emitter, + METHOD_HELPER_HANDLER_OFFSET - 48, + &escape_label, + ); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_method_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, &slot.method)); + abi::emit_call_label(emitter, &callee); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_aarch64_ref_args(emitter, &ref_slots, &body_label); + emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_method_prep_fail_cleanup(emitter, 48, fail_label); + } +} + +/// Emits x86_64 method-call bodies for every bridge-supported method. +fn emit_x86_64_method_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], + done_label: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for slot in slots { + let body_label = method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); + emitter.label(&body_label); + emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); + let (arg_temp_bytes, ref_slots) = + emit_x86_64_prepare_method_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape_x", body_label); + emit_x86_64_method_exception_boundary_push(emitter, METHOD_HELPER_FRAME_SIZE, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_method_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, &slot.method)); + abi::emit_call_label(emitter, &callee); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_x86_64_ref_args(emitter, &ref_slots, &body_label); + emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_method_prep_fail_cleanup(emitter, fail_label); + } +} + +/// Emits ARM64 static method-call bodies for every bridge-supported static method. +fn emit_aarch64_static_method_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + done_label: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for slot in slots { + let body_label = static_method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); + emitter.label(&body_label); + emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); + let (arg_temp_bytes, ref_slots) = + emit_aarch64_prepare_static_method_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape", body_label); + emit_aarch64_method_exception_boundary_push( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + &escape_label, + ); + let overflow_bytes = materialize_static_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_call_label( + emitter, + &static_method_symbol(&slot.impl_class, &slot.method), + ); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_aarch64_ref_args(emitter, &ref_slots, &body_label); + emit_aarch64_method_exception_boundary_pop( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + ); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_method_exception_boundary_pop( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + ); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_method_prep_fail_cleanup(emitter, 64, fail_label); + } +} + +/// Emits x86_64 static method-call bodies for every bridge-supported static method. +fn emit_x86_64_static_method_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], + done_label: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + for slot in slots { + let body_label = static_method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); + emitter.label(&body_label); + emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); + let (arg_temp_bytes, ref_slots) = + emit_x86_64_prepare_static_method_args( + module, + emitter, + data, + slot, + &prep_fail_label, + callable_support, + ); + let escape_label = format!("{}_escape_x", body_label); + emit_x86_64_method_exception_boundary_push( + emitter, + STATIC_METHOD_HELPER_FRAME_SIZE, + &escape_label, + ); + let overflow_bytes = materialize_static_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_call_label( + emitter, + &static_method_symbol(&slot.impl_class, &slot.method), + ); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_x86_64_ref_args(emitter, &ref_slots, &body_label); + emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_method_prep_fail_cleanup(emitter, fail_label); + } +} + +/// Restores an ARM64 method-helper frame before reporting an argument-prep fatal. +fn emit_aarch64_method_prep_fail_cleanup( + emitter: &mut Emitter, + frame_pointer_offset: usize, + fail_label: &str, +) { + emitter.instruction(&format!("sub sp, x29, #{}", frame_pointer_offset)); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("b {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Restores an x86_64 method-helper frame before reporting an argument-prep fatal. +fn emit_x86_64_method_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("mov rsp, rbp"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("jmp {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Emits ARM64 arity validation for one method body. +fn emit_aarch64_validate_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the method signature + emitter.instruction(&format!("b.ne {}", fail_label)); // reject method dispatch when arity differs +} + +/// Emits x86_64 arity validation for one method body. +fn emit_x86_64_validate_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the method signature + emitter.instruction(&format!("jne {}", fail_label)); // reject method dispatch when arity differs +} + +/// Emits ARM64 arity validation for one static method body. +fn emit_aarch64_validate_static_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for static arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the static method signature + emitter.instruction(&format!("b.ne {}", fail_label)); // reject static method dispatch when arity differs +} + +/// Emits x86_64 arity validation for one static method body. +fn emit_x86_64_validate_static_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for static arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the static method signature + emitter.instruction(&format!("jne {}", fail_label)); // reject static method dispatch when arity differs +} + +/// Prepares ARM64 method ABI registers for the supported argument shapes. +fn emit_aarch64_prepare_method_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = method_body_label(module, slot); + let ref_slots = emit_aarch64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + 24, + &body_label, + fail_label, + callable_support, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("ldr x0, [x29, #-32]"); // load the unboxed receiver as the first method argument + abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index, 24); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Prepares ARM64 static method ABI registers for the supported argument shapes. +fn emit_aarch64_prepare_static_method_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = static_method_body_label(module, slot); + let ref_slots = emit_aarch64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + 40, + &body_label, + fail_label, + callable_support, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); + abi::emit_push_result_value(emitter, &PhpType::Int); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&PhpType::Int); + for (index, param_ty) in slot.params.iter().enumerate() { + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index, 40); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Prepares x86_64 method ABI registers for the supported argument shapes. +fn emit_x86_64_prepare_method_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = method_body_label(module, slot); + let ref_slots = emit_x86_64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + callable_support, + X86_64_METHOD_CONTEXT_FRAME_OFFSET, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first method argument + abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + X86_64_METHOD_CONTEXT_FRAME_OFFSET, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Prepares x86_64 static method ABI registers for the supported argument shapes. +fn emit_x86_64_prepare_static_method_args( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> (usize, Vec) { + let body_label = static_method_body_label(module, slot); + let ref_slots = emit_x86_64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + callable_support, + X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); + abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); + abi::emit_push_result_value(emitter, &PhpType::Int); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&PhpType::Int); + for (index, param_ty) in slot.params.iter().enumerate() { + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET, + ); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); + } + (arg_temp_bytes, ref_slots) +} + +/// Materializes the pushed receiver and eval arguments into the target method ABI. +fn materialize_method_args( + module: &Module, + emitter: &mut Emitter, + receiver_ty: &PhpType, + params: &[PhpType], + ref_params: &[bool], +) -> usize { + let mut arg_types = Vec::with_capacity(params.len() + 1); + arg_types.push(receiver_ty.clone()); + arg_types.extend(eval_abi_param_types_for_refs(params, ref_params)); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + +/// Materializes pushed eval arguments into the target static method ABI. +fn materialize_static_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, +) -> usize { + let mut arg_types = Vec::with_capacity(slot.params.len() + 1); + arg_types.push(PhpType::Int); + arg_types.extend(eval_abi_param_types_for_refs(&slot.params, &slot.ref_params)); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + +/// Prepares ARM64 stack cells for eval-supplied by-reference arguments. +fn emit_aarch64_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], + ref_params: &[bool], + arg_array_frame_offset: usize, + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); + for slot in &ref_slots { + emit_aarch64_load_eval_arg(module, emitter, slot.param_index, arg_array_frame_offset); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + &slot.param_ty, + &arg_label, + fail_label, + callable_support, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } + } + ref_slots +} + +/// Prepares x86_64 stack cells for eval-supplied by-reference arguments. +fn emit_x86_64_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], + ref_params: &[bool], + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, + context_frame_offset: usize, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); + for slot in &ref_slots { + emit_x86_64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + &slot.param_ty, + &arg_label, + fail_label, + callable_support, + context_frame_offset, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } + } + ref_slots +} + +/// Preserves an ARM64 boxed return value while by-reference eval args are written back. +fn preserve_result_and_write_back_aarch64_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalRefArgSlot], + label_prefix: &str, +) { + if ref_slots.is_empty() { + return; + } + abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); + emit_aarch64_write_back_ref_args(emitter, ref_slots, 16, label_prefix); + abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); +} + +/// Preserves an x86_64 boxed return value while by-reference eval args are written back. +fn preserve_result_and_write_back_x86_64_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalRefArgSlot], + label_prefix: &str, +) { + if ref_slots.is_empty() { + return; + } + abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); + emit_x86_64_write_back_ref_args(emitter, ref_slots, 16, label_prefix); + abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); +} + +/// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. +fn emit_aarch64_load_eval_arg( + module: &Module, + emitter: &mut Emitter, + index: usize, + arg_array_frame_offset: usize, +) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "x0", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array + emitter.instruction("ldr x1, [x29, #-16]"); // pass the boxed index to the eval array reader + emitter.instruction(&format!("ldr x0, [x29, #-{}]", arg_array_frame_offset)); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed eval argument for coercion +} + +/// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. +fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "rdi", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the boxed index to the eval array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed eval argument for coercion +} + +/// Casts one boxed eval argument into ARM64 result registers for temporary staging. +fn emit_aarch64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, +) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("bl __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in d0 + } + PhpType::Str => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 + } + PhpType::Callable => { + super::eval_callable_helpers::emit_aarch64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } + PhpType::TaggedScalar => { + emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } + PhpType::Mixed => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed method parameter + } + PhpType::Object(class_name) => { + emit_aarch64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); + } + PhpType::Array(_) => { + emit_aarch64_cast_eval_array_arg(emitter, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_cast_eval_array_arg(emitter, 5, fail_label); + } + PhpType::Iterable => { + emit_aarch64_cast_eval_iterable_arg(module, emitter, label_prefix, fail_label); + } + _ => {} + } +} + +/// Coerces one ARM64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_aarch64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed eval argument across tag inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("ldr x0, [sp]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("add sp, sp, #16"); // discard the preserved boxed eval argument +} + +/// Validates and unboxes one ARM64 object-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_object_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object type hint + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the object payload for the native method call + emitter.instruction("cmp x0, #6"); // object type hints require an object payload, not a class string + emitter.instruction(&format!("b.ne {}", fail_label)); // reject malformed non-object payloads + emitter.instruction("mov x0, x1"); // place the unboxed object pointer in the result register +} + +/// Validates and unboxes one ARM64 array-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fail_label: &str) { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for array unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the array payload for the native method call + abi::emit_load_int_immediate(emitter, "x9", expected_tag); + emitter.instruction("cmp x0, x9"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // place the unboxed array pointer in the result register +} + +/// Validates and unboxes one ARM64 iterable-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array + emitter.instruction(&format!("b.eq {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp x0, #6"); // runtime tag 6 means object + emitter.instruction(&format!("b.eq {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("b {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov x0, x1"); // place the array payload pointer in the result register + emitter.instruction(&format!("b {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_aarch64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + emitter.instruction("ldr x0, [sp], #16"); // restore the iterable object pointer as the result + emitter.label(&done); +} + +/// Validates the ARM64 object payload saved in `x1` against Traversable interfaces. +fn emit_aarch64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("b {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + emitter.instruction("str x1, [sp, #-16]!"); // preserve the object payload across Traversable checks + for interface_id in interface_ids { + emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "x1", interface_id as i64); + abi::emit_load_int_immediate(emitter, "x2", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("cmp x0, #0"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("b.ne {}", object_ok)); // matching Iterator metadata accepts the object + } + emitter.instruction("add sp, sp, #16"); // discard the rejected object payload + emitter.instruction(&format!("b {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + +/// Casts one boxed eval argument into x86_64 result registers for temporary staging. +fn emit_x86_64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, + context_frame_offset: usize, +) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("call __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in xmm0 + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair + } + PhpType::Callable => { + super::eval_callable_helpers::emit_x86_64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + context_frame_offset, + ); + } + PhpType::TaggedScalar => { + emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } + PhpType::Mixed => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed method parameter + } + PhpType::Object(class_name) => { + emit_x86_64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); + } + PhpType::Array(_) => { + emit_x86_64_cast_eval_array_arg(emitter, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_cast_eval_array_arg(emitter, 5, fail_label); + } + PhpType::Iterable => { + emit_x86_64_cast_eval_iterable_arg(module, emitter, label_prefix, fail_label); + } + _ => {} + } +} + +/// Coerces one x86_64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_x86_64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); +} + +/// Validates and unboxes one x86_64 object-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_object_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the object type hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object type hint + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the object payload for the native method call + emitter.instruction("cmp rax, 6"); // object type hints require an object payload, not a class string + emitter.instruction(&format!("jne {}", fail_label)); // reject malformed non-object payloads + emitter.instruction("mov rax, rdi"); // place the unboxed object pointer in the result register +} + +/// Validates and unboxes one x86_64 array-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fail_label: &str) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for array unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the array payload for the native method call + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // place the unboxed array pointer in the result register +} + +/// Validates and unboxes one x86_64 iterable-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array + emitter.instruction(&format!("je {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp rax, 6"); // runtime tag 6 means object + emitter.instruction(&format!("je {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("jmp {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov rax, rdi"); // place the array payload pointer in the result register + emitter.instruction(&format!("jmp {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_x86_64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + abi::emit_pop_reg(emitter, "rax"); + emitter.label(&done); +} + +/// Validates the x86_64 object payload saved in `rdi` against Traversable interfaces. +fn emit_x86_64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("jmp {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + abi::emit_push_reg(emitter, "rdi"); + for interface_id in interface_ids { + emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "rsi", interface_id as i64); + abi::emit_load_int_immediate(emitter, "rdx", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("test rax, rax"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("jne {}", object_ok)); // matching Iterator metadata accepts the object + } + abi::emit_pop_reg(emitter, "r10"); + emitter.instruction(&format!("jmp {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + +/// Boxes the current native method result as the Mixed cell expected by eval. +fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &PhpType) { + if return_ty.codegen_repr() == PhpType::Void { + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } else { + emit_box_current_value_as_mixed(emitter, return_ty); + } +} + +/// Returns runtime interface ids for object values accepted by PHP iterable parameters. +fn traversable_interface_ids(module: &Module) -> Vec { + ["Iterator", "IteratorAggregate"] + .into_iter() + .filter_map(|name| module.interface_infos.get(name).map(|info| info.interface_id)) + .collect() +} + +/// Groups method slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalMethodSlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Groups static method slots by class name while preserving sorted class order. +fn grouped_static_slots( + slots: &[EvalStaticMethodSlot], +) -> BTreeMap<&str, Vec<&EvalStaticMethodSlot>> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_name.as_str()) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a method slot. +fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_method_{}_{}_{}{}", + label_fragment(&slot.class_name), + label_fragment(&slot.impl_class), + label_fragment(&slot.method), + suffix + ) +} + +/// Returns a platform-safe label for continuing after a scoped method name miss. +fn method_access_miss_label(module: &Module, slot: &EvalMethodSlot) -> String { + format!("{}_access_miss", method_body_label(module, slot)) +} + +/// Returns a platform-safe body label for a static method slot. +fn static_method_body_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_static_method_{}_{}_{}{}", + label_fragment(&slot.class_name), + label_fragment(&slot.impl_class), + label_fragment(&slot.method), + suffix + ) +} + +/// Returns a platform-safe label for continuing after a scoped static method name miss. +fn static_method_access_miss_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { + format!("{}_access_miss", static_method_body_label(module, slot)) +} + +/// Returns class scopes that satisfy one method visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs new file mode 100644 index 0000000000..5364daccc6 --- /dev/null +++ b/src/codegen/eval_property_helpers.rs @@ -0,0 +1,1433 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician access properties on +//! native objects using the current module's class metadata. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user class ids or property +//! offsets, so these C-ABI symbols are emitted into the user assembly. +//! - Supported slots include public properties plus protected/private +//! properties when the active eval class scope satisfies PHP visibility. + +use std::collections::BTreeMap; + +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::runtime_value_tag; +use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::ir::{Function, LocalKind, Module}; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +/// Property slot metadata needed by eval property bridge dispatch. +#[derive(Clone)] +struct EvalPropertySlot { + class_id: u64, + class_name: String, + declaring_class: String, + allowed_scopes: Vec, + property: String, + visibility: Visibility, + offset: usize, + ty: PhpType, + is_declared: bool, + is_hidden_shadow: bool, +} + +/// Emits eval property helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_property_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_property_slots(module); + emit_property_get_helper(module, emitter, data, &slots); + emit_property_is_initialized_helper(module, emitter, data, &slots); + emit_property_set_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects declared properties with storage layouts and visibility rules the bridge can access. +fn collect_eval_property_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_property_slots(module, class_name, class_info, &mut slots); + } + slots +} + +/// Adds bridge-supported properties for one class. +fn collect_class_property_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + slots: &mut Vec, +) { + for (index, (property, ty)) in class_info.properties.iter().enumerate() { + let visible_index = class_info.visible_property_index(property); + let (declaring_class, visibility, is_hidden_shadow) = if visible_index == Some(index) { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name) + .to_string(); + ( + declaring_class, + property_visibility(class_info, property).clone(), + false, + ) + } else { + let Some((declaring_class, visibility)) = + hidden_private_property_slot_metadata(module, class_name, index, property) + else { + continue; + }; + (declaring_class, visibility, true) + }; + if !property_visibility_supported(&visibility) || !property_type_supported(ty) { + continue; + } + slots.push(EvalPropertySlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + declaring_class: declaring_class.clone(), + allowed_scopes: visibility_scope_names(module, &declaring_class, &visibility), + property: property.clone(), + visibility, + offset: 8 + index * 16, + ty: ty.codegen_repr(), + is_declared: class_info.property_slot_is_declared(index, property), + is_hidden_shadow, + }); + } +} + +/// Returns metadata for a private parent slot hidden by a same-named child property. +fn hidden_private_property_slot_metadata( + module: &Module, + class_name: &str, + index: usize, + property: &str, +) -> Option<(String, Visibility)> { + for (ancestor_name, ancestor_info) in class_ancestry(module, class_name) { + let Some((ancestor_property, _)) = ancestor_info.properties.get(index) else { + continue; + }; + if ancestor_property != property + || ancestor_info.visible_property_index(property) != Some(index) + { + continue; + } + let visibility = property_visibility(ancestor_info, property).clone(); + if visibility != Visibility::Private { + continue; + } + let declaring_class = ancestor_info + .property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| ancestor_name.to_string()); + return Some((declaring_class, visibility)); + } + None +} + +/// Returns class metadata from root parent to the requested class. +fn class_ancestry<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut chain = Vec::new(); + collect_class_ancestry(module, class_name, &mut chain); + chain +} + +/// Recursively collects class metadata from parent to child. +fn collect_class_ancestry<'a>( + module: &'a Module, + class_name: &'a str, + chain: &mut Vec<(&'a str, &'a ClassInfo)>, +) { + let Some(class_info) = module.class_infos.get(class_name) else { + return; + }; + if let Some(parent) = class_info.parent.as_deref() { + collect_class_ancestry(module, parent, chain); + } + chain.push((class_name, class_info)); +} + +/// Returns the declared property visibility, defaulting to public metadata. +fn property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Visibility { + class_info + .property_visibilities + .get(property) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval property bridge can enforce this visibility. +fn property_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) +} + +/// Returns true for property storage shapes the bridge can box and update. +fn property_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Emits `__elephc_eval_value_property_get(Mixed*, name, len, scope, scope_len) -> Mixed*`. +fn emit_property_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property get ---"); + label_c_global(module, emitter, "__elephc_eval_value_property_get"); + match module.target.arch { + Arch::AArch64 => emit_property_get_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_get_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_property_is_initialized(Mixed*, name, len, scope, scope_len) -> bool`. +fn emit_property_is_initialized_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property initialization probe ---"); + label_c_global( + module, + emitter, + "__elephc_eval_value_property_is_initialized", + ); + match module.target.arch { + Arch::AArch64 => emit_property_is_initialized_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_is_initialized_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_property_set(Mixed*, name, len, Mixed*, scope, scope_len) -> bool`. +fn emit_property_set_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property set ---"); + label_c_global(module, emitter, "__elephc_eval_value_property_set"); + match module.target.arch { + Arch::AArch64 => emit_property_set_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_set_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 property-get helper body. +fn emit_property_get_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let null_label = "__elephc_eval_value_property_get_null"; + let fail_label = "__elephc_eval_value_property_get_fail"; + let done_label = "__elephc_eval_value_property_get_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x0, [sp, #24]"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "get", fail_label); + emit_aarch64_stdclass_property_get_fallback(emitter); + emitter.instruction(&format!("b {}", done_label)); // return after stdClass fallback get or null result + emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after access failure + emitter.label(null_label); + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the boxed property value to Rust +} + +/// Emits the x86_64 property-get helper body. +fn emit_property_get_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let null_label = "__elephc_eval_value_property_get_null_x"; + let fail_label = "__elephc_eval_value_property_get_fail_x"; + let done_label = "__elephc_eval_value_property_get_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length + emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "get", fail_label); + emit_x86_64_stdclass_property_get_fallback(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // return after stdClass fallback get or null result + emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after access failure + emitter.label(null_label); + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed property value to Rust +} + +/// Emits the ARM64 property-initialization helper body. +fn emit_property_is_initialized_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_is_initialized_fail"; + let done_label = "__elephc_eval_value_property_is_initialized_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for marker loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported declared property matched the request + emit_aarch64_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report an initialization miss to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the x86_64 property-initialization helper body. +fn emit_property_is_initialized_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_is_initialized_fail_x"; + let done_label = "__elephc_eval_value_property_is_initialized_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, object, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for marker loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported declared property matched the request + emit_x86_64_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report an initialization miss to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the ARM64 property-set helper body. +fn emit_property_set_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_set_fail"; + let done_label = "__elephc_eval_value_property_set_done"; + emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned + emitter.instruction("str x0, [sp, #32]"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #48]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "set", fail_label); + emit_aarch64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); + emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits the x86_64 property-set helper body. +fn emit_property_set_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_set_fail_x"; + let done_label = "__elephc_eval_value_property_set_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, value, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "set", fail_label); + emit_x86_64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); + emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits an ARM64 fallback read for stdClass dynamic properties. +fn emit_aarch64_stdclass_property_get_fallback(emitter: &mut Emitter) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("bl __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) +} + +/// Emits an x86_64 fallback read for stdClass dynamic properties. +fn emit_x86_64_stdclass_property_get_fallback(emitter: &mut Emitter) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("call __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) +} + +/// Emits an ARM64 fallback write for stdClass dynamic properties. +fn emit_aarch64_stdclass_property_set_fallback( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + done_label: &str, +) { + let Some(class_id) = stdclass_class_id(module) else { + emitter.instruction(&format!("b {}", fail_label)); // reject writes when stdClass metadata is unavailable + return; + }; + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("ldr x9, [x9]"); // load the object's runtime class id + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // check whether the receiver is stdClass + emitter.instruction(&format!("b.ne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("ldr x3, [sp, #24]"); // reload the boxed value being assigned + emitter.instruction("bl __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after stdClass write +} + +/// Emits an x86_64 fallback write for stdClass dynamic properties. +fn emit_x86_64_stdclass_property_set_fallback( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + done_label: &str, +) { + let Some(class_id) = stdclass_class_id(module) else { + emitter.instruction(&format!("jmp {}", fail_label)); // reject writes when stdClass metadata is unavailable + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the object's runtime class id + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // check whether the receiver is stdClass + emitter.instruction(&format!("jne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the boxed value being assigned + emitter.instruction("call __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after stdClass write +} + +/// Returns the runtime class id for builtin `stdClass` in this module. +fn stdclass_class_id(module: &Module) -> Option { + module + .class_infos + .iter() + .find(|(class_name, _)| crate::types::checker::builtin_stdclass::is_stdclass(class_name)) + .map(|(_, class_info)| class_info.class_id) +} + +/// Emits ARM64 class-id and property-name dispatch for helper slot bodies. +fn emit_aarch64_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + mode: &str, + fail_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let class_label = format!("__elephc_eval_property_{}_class_{}", mode, class_id); + let next_label = format!("__elephc_eval_property_{}_next_{}", mode, class_id); + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_aarch64_property_name_compare(module, emitter, data, slot, mode, fail_label); + } + emitter.label(&class_label); + emitter.instruction(&format!("b {}", next_label)); // fall through to the next class after a name miss + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and property-name dispatch for helper slot bodies. +fn emit_x86_64_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + mode: &str, + fail_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let class_label = format!("__elephc_eval_property_{}_class_{}_x", mode, class_id); + let next_label = format!("__elephc_eval_property_{}_next_{}_x", mode, class_id); + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_x86_64_property_name_compare(module, emitter, data, slot, mode, fail_label); + } + emitter.label(&class_label); + emitter.instruction(&format!("jmp {}", next_label)); // fall through to the next class after a name miss + emitter.label(&next_label); + } +} + +/// Emits one ARM64 property-name comparison and branch to the matching body. +fn emit_aarch64_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property + let target_label = slot_body_label(module, slot, mode); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ + let scope_ok_label = slot_scope_ok_label(module, slot, mode); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_aarch64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); + emitter.label(&scope_ok_label); + emitter.instruction(&format!("b {}", target_label)); // dispatch after scoped visibility is satisfied + emitter.label(&miss_label); +} + +/// Emits one x86_64 property-name comparison and branch to the matching body. +fn emit_x86_64_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare requested property name with this declared property + emitter.instruction("test rax, rax"); // check whether the property names matched + let target_label = slot_body_label(module, slot, mode); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the property body when the names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ + let scope_ok_label = slot_scope_ok_label(module, slot, mode); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_x86_64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); + emitter.label(&scope_ok_label); + emitter.instruction(&format!("jmp {}", target_label)); // dispatch after scoped visibility is satisfied + emitter.label(&miss_label); +} + +/// Emits ARM64 visibility checks for a protected/private property bridge hit. +fn emit_aarch64_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped property access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // accept access when the current scope is allowed + } + emitter.instruction(&format!("b {}", fail_label)); // reject scoped property access from unrelated classes +} + +/// Emits x86_64 visibility checks for a protected/private property bridge hit. +fn emit_x86_64_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped property access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // accept access when the current scope is allowed + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped property access from unrelated classes +} + +/// Returns ARM64 stack offsets for the class-scope pointer and length. +fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" | "is_initialized" => (32, 40), + "set" => (40, 48), + _ => unreachable!("eval property helpers only use get/set/is_initialized modes"), + } +} + +/// Returns x86_64 frame offsets for the class-scope pointer and length. +fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" | "is_initialized" => (40, 48), + "set" => (48, 56), + _ => unreachable!("eval property helpers only use get/set/is_initialized modes"), + } +} + +/// Emits ARM64 property-get bodies for every bridge-supported property slot. +fn emit_aarch64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_aarch64_uninitialized_property_get_guard(emitter, slot, done_label); + emit_aarch64_box_property_slot(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the declared property value + } +} + +/// Emits x86_64 property-get bodies for every bridge-supported property slot. +fn emit_x86_64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_x86_64_uninitialized_property_get_guard(emitter, slot, done_label); + emit_x86_64_box_property_slot(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the declared property value + } +} + +/// Emits ARM64 property-initialization bodies for every bridge-supported property slot. +fn emit_aarch64_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_aarch64_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after materializing the initialization flag + } +} + +/// Emits x86_64 property-initialization bodies for every bridge-supported property slot. +fn emit_x86_64_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_x86_64_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the initialization flag + } +} + +/// Emits ARM64 property-set bodies for every bridge-supported property slot. +fn emit_aarch64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_aarch64_store_property_slot(module, emitter, data, slot, fail_label); + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // return after storing the declared property value + } +} + +/// Emits x86_64 property-set bodies for every bridge-supported property slot. +fn emit_x86_64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_x86_64_store_property_slot(module, emitter, data, slot, fail_label); + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // return after storing the declared property value + } +} + +/// Emits an ARM64 boolean for one declared property's initialized state. +fn emit_aarch64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if !slot.is_declared { + emitter.instruction("mov x0, #1"); // non-typed declared properties are always initialized + return; + } + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized +} + +/// Emits an x86_64 boolean for one declared property's initialized state. +fn emit_x86_64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if !slot.is_declared { + emitter.instruction("mov rax, 1"); // non-typed declared properties are always initialized + return; + } + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "r10", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp rax, r10"); // compare the property marker against the uninitialized sentinel + emitter.instruction("setne al"); // materialize true when the instance property is initialized + emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register +} + +/// Emits an ARM64 typed-property guard before boxing an eval bridge property read. +fn emit_aarch64_uninitialized_property_get_guard( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer for marker inspection + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("mov x0, xzr"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Emits an x86_64 typed-property guard before boxing an eval bridge property read. +fn emit_x86_64_uninitialized_property_get_guard( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for marker inspection + emitter.instruction(&format!("mov rax, QWORD PTR [r10 + {}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp rax, r11"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("xor eax, eax"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Boxes a property value loaded from an ARM64 object slot into a Mixed cell. +fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer + match slot.ty.codegen_repr() { + PhpType::Int + | PhpType::Bool + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } => { + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the property payload low word + emitter.instruction("mov x2, xzr"); // heap/scalar property payloads do not use a high word here + abi::emit_load_int_immediate(emitter, "x0", runtime_value_tag(&slot.ty) as i64); + emitter.instruction("bl __rt_mixed_from_value"); // box the property payload as a Mixed cell + } + PhpType::Float => { + emitter.instruction(&format!("ldr d0, [x9, #{}]", slot.offset)); // load the floating property payload + emitter.instruction("fmov x1, d0"); // move float bits into the Mixed low payload word + emitter.instruction("mov x2, xzr"); // float payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = float + emitter.instruction("bl __rt_mixed_from_value"); // box the floating property payload as Mixed + } + PhpType::Str => { + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the string property pointer + emitter.instruction(&format!("ldr x2, [x9, #{}]", slot.offset + 8)); // load the string property length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property payload + } + PhpType::TaggedScalar => { + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the nullable integer property payload + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset + 8)); //load the nullable integer property tag + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!( + "{}_mixed_null", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the stored Mixed property cell + emitter.instruction(&format!("cbz x0, {}", null_label)); // null property storage reads as PHP null + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Boxes a property value loaded from an x86_64 object slot into a Mixed cell. +fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + emitter.instruction(&format!("mov rdi, QWORD PTR [r11 + {}]", slot.offset)); // load the property payload low word + emitter.instruction("xor esi, esi"); // heap/scalar property payloads do not use a high word here + abi::emit_load_int_immediate(emitter, "rax", runtime_value_tag(&slot.ty) as i64); + emitter.instruction("call __rt_mixed_from_value"); // box the property payload as a Mixed cell + } + PhpType::Float => { + emitter.instruction(&format!("movsd xmm0, QWORD PTR [r11 + {}]", slot.offset)); // load the floating property payload + emitter.instruction("movq rdi, xmm0"); // move float bits into the Mixed low payload word + emitter.instruction("xor esi, esi"); // float payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = float + emitter.instruction("call __rt_mixed_from_value"); // box the floating property payload as Mixed + } + PhpType::Str => { + emitter.instruction(&format!("mov rdi, QWORD PTR [r11 + {}]", slot.offset)); // load the string property pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [r11 + {}]", slot.offset + 8)); // load the string property length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the string property payload + } + PhpType::TaggedScalar => { + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset)); //load the nullable integer property payload + emitter.instruction(&format!("mov rdx, QWORD PTR [r11 + {}]", slot.offset + 8)); //load the nullable integer property tag + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!("{}_mixed_null_x", label_fragment(&slot_body_label_raw(slot, "get"))); + let done_label = format!("{}_mixed_done_x", label_fragment(&slot_body_label_raw(slot, "get"))); + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset)); // load the stored Mixed property cell + emitter.instruction("test rax, rax"); // check whether the property storage is initialized + emitter.instruction(&format!("jz {}", null_label)); // null property storage reads as PHP null + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("jmp {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Stores a boxed Mixed eval value into an ARM64 object property slot. +fn emit_aarch64_store_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + fail_label: &str, +) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), + PhpType::Bool => { + emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0") + } + PhpType::Float => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str d0, [x9, #{}]", slot.offset)); // store the coerced float into the property slot + emit_aarch64_clear_scalar_property_marker(emitter, slot); + } + PhpType::Str => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset)); // store the coerced string pointer into the property slot + emitter.instruction(&format!("str x2, [x9, #{}]", slot.offset + 8)); // store the coerced string length into the property slot + } + PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_property(emitter, slot), + PhpType::Array(_) => emit_aarch64_store_heap_property_slot(emitter, slot, 4, fail_label), + PhpType::AssocArray { .. } => { + emit_aarch64_store_heap_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_aarch64_store_object_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value being assigned + emitter.instruction("bl __rt_incref"); // retain the Mixed cell for property ownership + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained Mixed cell into the property slot + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the unused property high word + } + _ => {} + } +} + +/// Stores a boxed Mixed eval value into an x86_64 object property slot. +fn emit_x86_64_store_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + fail_label: &str, +) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), + PhpType::Bool => { + emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax") + } + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("movsd QWORD PTR [r11 + {}], xmm0", slot.offset)); // store the coerced float into the property slot + emit_x86_64_clear_scalar_property_marker(emitter, slot); + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); // store the coerced string pointer into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); // store the coerced string length into the property slot + } + PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_property(emitter, slot), + PhpType::Array(_) => emit_x86_64_store_heap_property_slot(emitter, slot, 4, fail_label), + PhpType::AssocArray { .. } => { + emit_x86_64_store_heap_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_x86_64_store_object_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value being assigned + emitter.instruction("call __rt_incref"); // retain the Mixed cell for property ownership + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); // store the retained Mixed cell into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); // clear the unused property high word + } + _ => {} + } +} + +/// Emits an ARM64 scalar property store after Mixed coercion. +fn emit_aarch64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("bl {}", helper)); // coerce the eval value to the declared property type + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str {}, [x9, #{}]", result_reg, slot.offset)); // store the coerced scalar into the property slot + emit_aarch64_clear_scalar_property_marker(emitter, slot); +} + +/// Clears an ARM64 one-word scalar typed-property marker after a successful store. +fn emit_aarch64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if slot.is_declared { + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker + } +} + +/// Stores a boxed eval value into an ARM64 nullable-int tagged-scalar property slot. +fn emit_aarch64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { + let null_label = format!( + "{}_tagged_scalar_null", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the nullable integer payload into the property slot + emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset + 8)); // store the nullable integer tag into the property slot +} + +/// Emits an x86_64 scalar property store after Mixed coercion. +fn emit_x86_64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("call {}", helper)); // coerce the eval value to the declared property type + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], {}", slot.offset, result_reg)); // store the coerced scalar into the property slot + emit_x86_64_clear_scalar_property_marker(emitter, slot); +} + +/// Clears an x86_64 one-word scalar typed-property marker after a successful store. +fn emit_x86_64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if slot.is_declared { + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); + // clear the typed-property initialization marker + } +} + +/// Stores a boxed ARM64 eval heap value into an array-like property slot. +fn emit_aarch64_store_heap_property_slot( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "x10", expected_tag); + emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the heap store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained heap pointer into the property slot + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker +} + +/// Validates and stores a boxed ARM64 eval object into an object property slot. +fn emit_aarch64_store_object_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object property type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object property type hint + } + emit_aarch64_store_heap_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed x86_64 eval heap value into an array-like property slot. +fn emit_x86_64_store_heap_property_slot( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the heap store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the retained heap pointer into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); + //clear the typed-property initialization marker +} + +/// Validates and stores a boxed x86_64 eval object into an object property slot. +fn emit_x86_64_store_object_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object property type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the object property type hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object property type hint + } + emit_x86_64_store_heap_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed eval value into an x86_64 nullable-int tagged-scalar property slot. +fn emit_x86_64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { + let null_label = format!( + "{}_tagged_scalar_null_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the nullable integer payload into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); + //store the nullable integer tag into the property slot +} + +/// Groups property slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalPropertySlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a get/set property slot. +fn slot_body_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("{}{}", slot_body_label_raw(slot, mode), suffix) +} + +/// Returns a platform-safe label for continuing after a scoped property name miss. +fn slot_access_miss_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + format!("{}_access_miss", slot_body_label(module, slot, mode)) +} + +/// Returns a platform-safe label for a successful scoped property visibility check. +fn slot_scope_ok_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + format!("{}_scope_ok", slot_body_label(module, slot, mode)) +} + +/// Returns the architecture-independent body label stem for a property slot. +fn slot_body_label_raw(slot: &EvalPropertySlot, mode: &str) -> String { + format!( + "__elephc_eval_property_{}_{}_{}_{}", + mode, + label_fragment(&slot.class_name), + label_fragment(&slot.declaring_class), + label_fragment(&slot.property) + ) +} + +/// Returns class scopes that satisfy one member visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs new file mode 100644 index 0000000000..d864f1e73d --- /dev/null +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -0,0 +1,705 @@ +//! Purpose: +//! Shares the by-reference argument bridge helpers used by eval-dispatched AOT +//! methods and constructors. +//! +//! Called from: +//! - `crate::codegen::eval_method_helpers` +//! - `crate::codegen::eval_constructor_helpers` +//! - `crate::codegen::lower_inst::builtins::eval` +//! +//! Key details: +//! - The generated eval bridge writes back through the original eval `Mixed` +//! cells after native AOT methods mutate by-reference argument storage. +//! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar, +//! string, array, iterable, and object references use raw ABI storage that is +//! boxed again during writeback. + +use crate::codegen::emit::Emitter; +use crate::codegen::{abi, emit_box_current_value_as_mixed, runtime_value_tag}; +use crate::types::{FunctionSig, PhpType}; + +/// Describes the stack storage for one eval-supplied by-reference argument. +#[derive(Clone)] +pub(crate) struct EvalRefArgSlot { + pub(crate) param_index: usize, + pub(crate) param_ty: PhpType, + pub(crate) raw_offset: usize, + pub(crate) original_offset: usize, + pub(crate) raw_refcounted_owned: bool, +} + +const EVAL_REF_ARG_BYTES: usize = 32; + +/// Returns true when an eval bridge by-reference parameter can be staged safely. +pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Mixed + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) + | PhpType::TaggedScalar + ) +} + +/// Returns true when every by-reference parameter in a native signature is bridgeable. +pub(crate) fn eval_signature_ref_params_supported(signature: &FunctionSig) -> bool { + signature.params.iter().enumerate().all(|(index, (_, ty))| { + !signature.ref_params.get(index).copied().unwrap_or(false) + || eval_ref_param_supported(ty) + }) +} + +/// Normalizes a sparse ref-parameter vector to the visible parameter count. +pub(crate) fn eval_normalized_ref_params(param_count: usize, ref_params: &[bool]) -> Vec { + (0..param_count) + .map(|index| ref_params.get(index).copied().unwrap_or(false)) + .collect() +} + +/// Converts visible parameter types to the ABI shape used by eval bridge calls. +pub(crate) fn eval_abi_param_types_for_refs( + param_types: &[PhpType], + ref_params: &[bool], +) -> Vec { + param_types + .iter() + .enumerate() + .map(|(index, ty)| { + if ref_params.get(index).copied().unwrap_or(false) { + PhpType::Int + } else { + ty.codegen_repr() + } + }) + .collect() +} + +/// Plans the stack offsets for eval by-reference argument cells. +pub(crate) fn eval_ref_arg_slots( + param_types: &[PhpType], + ref_params: &[bool], + raw_refcounted_owned: bool, +) -> Vec { + let total = ref_params.iter().filter(|is_ref| **is_ref).count(); + let mut seen = 0usize; + let mut slots = Vec::with_capacity(total); + for (param_index, is_ref) in ref_params.iter().enumerate() { + if !*is_ref { + continue; + } + let reverse_index = total - seen - 1; + let base_offset = reverse_index * EVAL_REF_ARG_BYTES; + slots.push(EvalRefArgSlot { + param_index, + param_ty: param_types[param_index].codegen_repr(), + raw_offset: base_offset, + original_offset: base_offset + 16, + raw_refcounted_owned, + }); + seen += 1; + } + slots +} + +/// Returns the temporary outgoing-argument stack slot size for one ABI argument. +pub(crate) fn eval_arg_temp_slot_size(ty: &PhpType) -> usize { + if matches!(ty.codegen_repr(), PhpType::Void | PhpType::Never) { + 0 + } else { + 16 + } +} + +/// Writes changed ARM64 ref-argument cells back into the original eval cells. +pub(crate) fn emit_aarch64_write_back_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalRefArgSlot], + stack_offset: usize, + label_prefix: &str, +) { + for slot in ref_slots { + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emit_aarch64_write_back_mixed_ref_arg(emitter, slot, stack_offset, label_prefix); + } else { + emit_aarch64_write_back_typed_ref_arg(emitter, slot, stack_offset, label_prefix); + } + } +} + +/// Writes changed x86_64 ref-argument cells back into the original eval cells. +pub(crate) fn emit_x86_64_write_back_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalRefArgSlot], + stack_offset: usize, + label_prefix: &str, +) { + for slot in ref_slots { + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emit_x86_64_write_back_mixed_ref_arg(emitter, slot, stack_offset, label_prefix); + } else { + emit_x86_64_write_back_typed_ref_arg(emitter, slot, stack_offset, label_prefix); + } + } +} + +/// Writes one ARM64 boxed `Mixed` ref slot back when native code replaced the cell pointer. +fn emit_aarch64_write_back_mixed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let done_label = format!("{}_ref_{}_done", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("cmp x9, x10"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("b.eq {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + emitter.label(&done_label); +} + +/// Writes one x86_64 boxed `Mixed` ref slot back when native code replaced the cell pointer. +fn emit_x86_64_write_back_mixed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let done_label = format!("{}_ref_{}_done_x", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("cmp r10, r11"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("je {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + emitter.label(&done_label); +} + +/// Boxes one ARM64 typed raw ref slot and replaces the original eval Mixed cell. +fn emit_aarch64_write_back_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + if typed_ref_arg_is_refcounted_heap(&slot.param_ty) { + emit_aarch64_write_back_refcounted_typed_ref_arg( + emitter, + slot, + stack_offset, + label_prefix, + ); + return; + } + let done_label = format!("{}_ref_{}_typed_done", label_prefix, slot.param_index); + emit_aarch64_skip_unchanged_typed_ref_arg(emitter, slot, stack_offset, &done_label); + emit_aarch64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov x10, x0"); // keep the newly boxed ref value available for cell replacement + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + emit_aarch64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); + emitter.label(&done_label); +} + +/// Boxes one x86_64 typed raw ref slot and replaces the original eval Mixed cell. +fn emit_x86_64_write_back_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + if typed_ref_arg_is_refcounted_heap(&slot.param_ty) { + emit_x86_64_write_back_refcounted_typed_ref_arg( + emitter, + slot, + stack_offset, + label_prefix, + ); + return; + } + let done_label = format!("{}_ref_{}_typed_done_x", label_prefix, slot.param_index); + emit_x86_64_skip_unchanged_typed_ref_arg(emitter, slot, stack_offset, &done_label); + emit_x86_64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov r11, rax"); // keep the newly boxed ref value available for cell replacement + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + emit_x86_64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); + emitter.label(&done_label); +} + +/// Writes one ARM64 refcounted raw ref slot back with borrowed/owned slot semantics. +fn emit_aarch64_write_back_refcounted_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let changed_label = format!( + "{}_ref_{}_typed_changed", + label_prefix, slot.param_index + ); + let unchanged_label = format!( + "{}_ref_{}_typed_unchanged", + label_prefix, slot.param_index + ); + let done_label = format!("{}_ref_{}_typed_done", label_prefix, slot.param_index); + emit_aarch64_branch_on_refcounted_raw_slot_change( + emitter, + slot, + stack_offset, + &changed_label, + &unchanged_label, + ); + emitter.label(&changed_label); + emit_aarch64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov x10, x0"); // keep the boxed replacement while updating the eval ref cell + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + if slot.raw_refcounted_owned { + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + } else { + emit_aarch64_replace_mixed_cell_without_releasing_old(emitter, "x9", "x10"); + } + emit_aarch64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + emitter.instruction(&format!("b {}", done_label)); // finish after transferring the changed raw ref payload + emitter.label(&unchanged_label); + if slot.raw_refcounted_owned { + emit_aarch64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + } + emitter.label(&done_label); +} + +/// Writes one x86_64 refcounted raw ref slot back with borrowed/owned slot semantics. +fn emit_x86_64_write_back_refcounted_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let changed_label = format!( + "{}_ref_{}_typed_changed_x", + label_prefix, slot.param_index + ); + let unchanged_label = format!( + "{}_ref_{}_typed_unchanged_x", + label_prefix, slot.param_index + ); + let done_label = format!("{}_ref_{}_typed_done_x", label_prefix, slot.param_index); + emit_x86_64_branch_on_refcounted_raw_slot_change( + emitter, + slot, + stack_offset, + &changed_label, + &unchanged_label, + ); + emitter.label(&changed_label); + emit_x86_64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov r11, rax"); // keep the boxed replacement while updating the eval ref cell + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + if slot.raw_refcounted_owned { + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + } else { + emit_x86_64_replace_mixed_cell_without_releasing_old(emitter, "r10", "r11"); + } + emit_x86_64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + emitter.instruction(&format!("jmp {}", done_label)); // finish after transferring the changed raw ref payload + emitter.label(&unchanged_label); + if slot.raw_refcounted_owned { + emit_x86_64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + } + emitter.label(&done_label); +} + +/// Branches on whether an ARM64 refcounted raw ref slot differs from the eval cell. +fn emit_aarch64_branch_on_refcounted_raw_slot_change( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + changed_label: &str, + unchanged_label: &str, +) { + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9, #8]"); // load the eval cell's current refcounted payload pointer + emitter.instruction("cmp x10, x11"); // did the native by-ref call replace the payload pointer? + emitter.instruction(&format!("b.ne {}", changed_label)); // changed raw slots need boxing and eval-cell replacement + emitter.instruction(&format!("b {}", unchanged_label)); // unchanged raw slots keep the existing eval cell payload +} + +/// Branches on whether an x86_64 refcounted raw ref slot differs from the eval cell. +fn emit_x86_64_branch_on_refcounted_raw_slot_change( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + changed_label: &str, + unchanged_label: &str, +) { + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the eval cell's current refcounted payload pointer + emitter.instruction("cmp r11, r9"); // did the native by-ref call replace the payload pointer? + emitter.instruction(&format!("jne {}", changed_label)); // changed raw slots need boxing and eval-cell replacement + emitter.instruction(&format!("jmp {}", unchanged_label)); // unchanged raw slots keep the existing eval cell payload +} + +/// Releases the current ARM64 refcounted raw slot value. +fn emit_aarch64_release_refcounted_raw_slot_value( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, +) { + abi::emit_load_temporary_stack_slot(emitter, "x0", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, &slot.param_ty.codegen_repr()); +} + +/// Releases the current x86_64 refcounted raw slot value. +fn emit_x86_64_release_refcounted_raw_slot_value( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, +) { + abi::emit_load_temporary_stack_slot(emitter, "rax", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, &slot.param_ty.codegen_repr()); +} + +/// Skips ARM64 typed writeback when the raw slot still matches the original Mixed payload. +fn emit_aarch64_skip_unchanged_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + done_label: &str, +) { + let Some(expected_tag) = typed_ref_arg_unchanged_runtime_tag(&slot.param_ty) else { + return; + }; + let changed_label = format!("{}_changed", done_label); + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9]"); // load the original eval cell runtime tag before skipping writeback + emitter.instruction(&format!("cmp x11, #{}", expected_tag)); // only skip when the eval cell already has the target scalar tag + emitter.instruction(&format!("b.ne {}", changed_label)); // coerce or rewrite cells whose original tag differs + emitter.instruction("ldr x11, [x9, #8]"); // load the original eval cell scalar payload word + emitter.instruction("cmp x10, x11"); // did the native call leave the raw ref slot unchanged? + emitter.instruction(&format!("b.eq {}", done_label)); // keep the existing Mixed cell when no replacement is needed + emitter.label(&changed_label); +} + +/// Skips x86_64 typed writeback when the raw slot still matches the original Mixed payload. +fn emit_x86_64_skip_unchanged_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + done_label: &str, +) { + let Some(expected_tag) = typed_ref_arg_unchanged_runtime_tag(&slot.param_ty) else { + return; + }; + let changed_label = format!("{}_changed", done_label); + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10]"); // load the original eval cell runtime tag before skipping writeback + emitter.instruction(&format!("cmp r9, {}", expected_tag)); // only skip when the eval cell already has the target scalar tag + emitter.instruction(&format!("jne {}", changed_label)); // coerce or rewrite cells whose original tag differs + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the original eval cell scalar payload word + emitter.instruction("cmp r11, r9"); // did the native call leave the raw ref slot unchanged? + emitter.instruction(&format!("je {}", done_label)); // keep the existing Mixed cell when no replacement is needed + emitter.label(&changed_label); +} + +/// Returns the runtime scalar tag that can skip writeback on exact tag and payload match. +fn typed_ref_arg_unchanged_runtime_tag(ty: &PhpType) -> Option { + match ty.codegen_repr() { + ty @ (PhpType::Int | PhpType::Bool | PhpType::Float) => Some(runtime_value_tag(&ty)), + _ => None, + } +} + +/// Returns true when a typed raw ref slot stores a refcounted heap payload pointer. +fn typed_ref_arg_is_refcounted_heap(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable | PhpType::Object(_) + ) +} + +/// Loads one ARM64 typed raw ref slot into the canonical result registers. +fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + match ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "x1", offset); + abi::emit_load_temporary_stack_slot(emitter, "x2", offset + 8); + } + PhpType::Float => { + abi::emit_load_temporary_stack_slot(emitter, "d0", offset); + } + PhpType::TaggedScalar => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + abi::emit_load_temporary_stack_slot(emitter, "x1", offset + 8); + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + } + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + } + _ => {} + } +} + +/// Loads one x86_64 typed raw ref slot into the canonical result registers. +fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + match ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + abi::emit_load_temporary_stack_slot(emitter, "rdx", offset + 8); + } + PhpType::Float => { + abi::emit_load_temporary_stack_slot(emitter, "xmm0", offset); + } + PhpType::TaggedScalar => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + abi::emit_load_temporary_stack_slot(emitter, "rdx", offset + 8); + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + } + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + } + _ => {} + } +} + +/// Releases any owned ARM64 payload left in one typed raw ref slot after writeback. +fn emit_aarch64_release_typed_ref_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let raw_offset = stack_offset + slot.raw_offset; + match slot.param_ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "x0", raw_offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } + ty @ (PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_)) => { + emit_aarch64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); + } + _ => {} + } +} + +/// Releases any owned x86_64 payload left in one typed raw ref slot after writeback. +fn emit_x86_64_release_typed_ref_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let raw_offset = stack_offset + slot.raw_offset; + match slot.param_ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "rax", raw_offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } + ty @ (PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_)) => { + emit_x86_64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); + } + _ => {} + } +} + +/// Releases an ARM64 raw refcounted slot when it owns a value not retained by the eval cell. +fn emit_aarch64_release_refcounted_raw_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, + ty: &PhpType, +) { + let release_label = format!("{}_ref_{}_release_raw", label_prefix, slot.param_index); + let done_label = format!("{}_ref_{}_release_raw_done", label_prefix, slot.param_index); + if !slot.raw_refcounted_owned { + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9, #8]"); // load the original eval cell payload for borrowed-slot comparison + emitter.instruction("cmp x10, x11"); // changed raw pointers are owned by the native assignment path + emitter.instruction(&format!("b.ne {}", release_label)); // release only raw heap values introduced by the native call + emitter.instruction(&format!("b {}", done_label)); // keep borrowed original payloads owned by the eval cell + } + emitter.label(&release_label); + abi::emit_load_temporary_stack_slot(emitter, "x0", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, ty); + emitter.label(&done_label); +} + +/// Releases an x86_64 raw refcounted slot when it owns a value not retained by the eval cell. +fn emit_x86_64_release_refcounted_raw_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, + ty: &PhpType, +) { + let release_label = format!("{}_ref_{}_release_raw_x", label_prefix, slot.param_index); + let done_label = format!("{}_ref_{}_release_raw_done_x", label_prefix, slot.param_index); + if !slot.raw_refcounted_owned { + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the original eval cell payload for borrowed-slot comparison + emitter.instruction("cmp r11, r9"); // changed raw pointers are owned by the native assignment path + emitter.instruction(&format!("jne {}", release_label)); // release only raw heap values introduced by the native call + emitter.instruction(&format!("jmp {}", done_label)); // keep borrowed original payloads owned by the eval cell + } + emitter.label(&release_label); + abi::emit_load_temporary_stack_slot(emitter, "rax", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, ty); + emitter.label(&done_label); +} + +/// Copies a replacement ARM64 Mixed cell into an existing target cell without releasing old payload. +fn emit_aarch64_replace_mixed_cell_without_releasing_old( + emitter: &mut Emitter, + target_reg: &str, + replacement_reg: &str, +) { + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for direct replacement + emitter.instruction("ldr x10, [sp, #8]"); // reload the replacement Mixed cell pointer + emitter.instruction("ldr x11, [x10]"); // copy the replacement runtime tag + emitter.instruction("str x11, [x9]"); // overwrite the target cell tag + emitter.instruction("ldr x11, [x10, #8]"); // copy the replacement low payload word + emitter.instruction("str x11, [x9, #8]"); // overwrite the target cell low payload word + emitter.instruction("ldr x11, [x10, #16]"); // copy the replacement high payload word + emitter.instruction("str x11, [x9, #16]"); // overwrite the target cell high payload word + emitter.instruction("mov x0, x10"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Copies a replacement x86_64 Mixed cell into an existing target cell without releasing old payload. +fn emit_x86_64_replace_mixed_cell_without_releasing_old( + emitter: &mut Emitter, + target_reg: &str, + replacement_reg: &str, +) { + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for direct replacement + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // reload the replacement Mixed cell pointer + emitter.instruction("mov r9, QWORD PTR [r11]"); // copy the replacement runtime tag + emitter.instruction("mov QWORD PTR [r10], r9"); // overwrite the target cell tag + emitter.instruction("mov r9, QWORD PTR [r11 + 8]"); // copy the replacement low payload word + emitter.instruction("mov QWORD PTR [r10 + 8], r9"); // overwrite the target cell low payload word + emitter.instruction("mov r9, QWORD PTR [r11 + 16]"); // copy the replacement high payload word + emitter.instruction("mov QWORD PTR [r10 + 16], r9"); // overwrite the target cell high payload word + emitter.instruction("mov rax, r11"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Copies one replacement ARM64 Mixed cell payload into an existing target cell. +fn emit_aarch64_replace_mixed_cell( + emitter: &mut Emitter, + label_prefix: &str, + param_index: usize, + target_reg: &str, + replacement_reg: &str, +) { + let release_string = format!("{}_ref_{}_release_string", label_prefix, param_index); + let copy_new = format!("{}_ref_{}_copy_new", label_prefix, param_index); + let done = format!("{}_ref_{}_assign_done", label_prefix, param_index); + + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for old-payload inspection + emitter.instruction("ldr x11, [x9]"); // inspect the old payload tag before overwriting the cell + emitter.instruction("cmp x11, #1"); // strings own a persisted heap payload that needs safe free + emitter.instruction(&format!("b.eq {}", release_string)); // release string payloads through the string-safe free path + emitter.instruction("cmp x11, #4"); // tags below array/hash/object/mixed are scalar payloads + emitter.instruction(&format!("b.lo {}", copy_new)); // scalar payloads can be overwritten directly + emitter.instruction("cmp x11, #7"); // tags above the refcounted payload range are not released here + emitter.instruction(&format!("b.hi {}", copy_new)); // unknown/null payload tags can be overwritten directly + emitter.instruction("ldr x0, [x9, #8]"); // pass the old refcounted child payload to the generic release helper + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction(&format!("b {}", copy_new)); // continue with replacement after releasing the old child + emitter.label(&release_string); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell before reading its string payload + emitter.instruction("ldr x0, [x9, #8]"); // pass the old string payload pointer to the safe free helper + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + emitter.label(©_new); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for replacement + emitter.instruction("ldr x10, [sp, #8]"); // reload the replacement Mixed cell pointer + emitter.instruction("ldr x11, [x10]"); // copy the replacement runtime tag + emitter.instruction("str x11, [x9]"); // overwrite the target cell tag + emitter.instruction("ldr x11, [x10, #8]"); // copy the replacement low payload word + emitter.instruction("str x11, [x9, #8]"); // overwrite the target cell low payload word + emitter.instruction("ldr x11, [x10, #16]"); // copy the replacement high payload word + emitter.instruction("str x11, [x9, #16]"); // overwrite the target cell high payload word + emitter.instruction("mov x0, x10"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label(&done); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Copies one replacement x86_64 Mixed cell payload into an existing target cell. +fn emit_x86_64_replace_mixed_cell( + emitter: &mut Emitter, + label_prefix: &str, + param_index: usize, + target_reg: &str, + replacement_reg: &str, +) { + let release_string = format!("{}_ref_{}_release_string_x", label_prefix, param_index); + let copy_new = format!("{}_ref_{}_copy_new_x", label_prefix, param_index); + let done = format!("{}_ref_{}_assign_done_x", label_prefix, param_index); + + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for old-payload inspection + emitter.instruction("mov r9, QWORD PTR [r10]"); // inspect the old payload tag before overwriting the cell + emitter.instruction("cmp r9, 1"); // strings own a persisted heap payload that needs safe free + emitter.instruction(&format!("je {}", release_string)); // release string payloads through the string-safe free path + emitter.instruction("cmp r9, 4"); // tags below array/hash/object/mixed are scalar payloads + emitter.instruction(&format!("jl {}", copy_new)); // scalar payloads can be overwritten directly + emitter.instruction("cmp r9, 7"); // tags above the refcounted payload range are not released here + emitter.instruction(&format!("jg {}", copy_new)); // unknown/null payload tags can be overwritten directly + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // pass the old refcounted child payload to the generic release helper + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction(&format!("jmp {}", copy_new)); // continue with replacement after releasing the old child + emitter.label(&release_string); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell before reading its string payload + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // pass the old string payload pointer to the safe free helper + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + emitter.label(©_new); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for replacement + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // reload the replacement Mixed cell pointer + emitter.instruction("mov r9, QWORD PTR [r11]"); // copy the replacement runtime tag + emitter.instruction("mov QWORD PTR [r10], r9"); // overwrite the target cell tag + emitter.instruction("mov r9, QWORD PTR [r11 + 8]"); // copy the replacement low payload word + emitter.instruction("mov QWORD PTR [r10 + 8], r9"); // overwrite the target cell low payload word + emitter.instruction("mov r9, QWORD PTR [r11 + 16]"); // copy the replacement high payload word + emitter.instruction("mov QWORD PTR [r10 + 16], r9"); // overwrite the target cell high payload word + emitter.instruction("mov rax, r11"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label(&done); + abi::emit_release_temporary_stack(emitter, 16); +} diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs new file mode 100644 index 0000000000..abd0b3360e --- /dev/null +++ b/src/codegen/eval_reflection_helpers.rs @@ -0,0 +1,358 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician materialize selected +//! synthetic reflection objects using the current module's private layouts. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - `ReflectionAttribute` stores private implementation slots that eval cannot +//! populate through public property writes. +//! - Eval-declared attributes do not have compile-time factories, so the helper +//! writes factory id 0; `ReflectionAttribute::newInstance()` then returns null. +//! - Target and repetition metadata are passed by the Rust eval bridge and +//! stored in the same private layout used by generated reflection objects. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{Function, LocalKind, Module}; +use crate::types::ClassInfo; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Fixed object slot layout for the synthetic `ReflectionAttribute` class. +struct ReflectionAttributeLayout { + class_id: u64, + property_count: usize, + name_lo: usize, + name_hi: usize, + args_lo: usize, + args_hi: usize, + factory_lo: usize, + factory_hi: usize, + target_lo: usize, + target_hi: usize, + repeated_lo: usize, + repeated_hi: usize, +} + +/// Emits eval reflection helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_reflection_helpers(module: &Module, emitter: &mut Emitter) { + if !module_uses_eval(module) { + return; + } + emitter.blank(); + emitter.comment("--- eval bridge: reflection helpers ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_attribute_new"); + let Some(layout) = reflection_attribute_layout(module) else { + emit_reflection_attribute_new_stub(emitter); + return; + }; + match module.target.arch { + Arch::AArch64 => emit_reflection_attribute_new_aarch64(emitter, &layout), + Arch::X86_64 => emit_reflection_attribute_new_x86_64(emitter, &layout), + } +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns the synthetic `ReflectionAttribute` object layout from class metadata. +fn reflection_attribute_layout(module: &Module) -> Option { + let info = module.class_infos.get("ReflectionAttribute")?; + let name_lo = reflection_property_offset(info, "__name")?; + let args_lo = reflection_property_offset(info, "__args")?; + let factory_lo = reflection_property_offset(info, "__factory")?; + let target_lo = reflection_property_offset(info, "__target")?; + let repeated_lo = reflection_property_offset(info, "__is_repeated")?; + Some(ReflectionAttributeLayout { + class_id: info.class_id, + property_count: info.properties.len(), + name_lo, + name_hi: name_lo + 8, + args_lo, + args_hi: args_lo + 8, + factory_lo, + factory_hi: factory_lo + 8, + target_lo, + target_hi: target_lo + 8, + repeated_lo, + repeated_hi: repeated_lo + 8, + }) +} + +/// Returns one declared property offset from the synthetic reflection class layout. +fn reflection_property_offset(info: &ClassInfo, property: &str) -> Option { + info.property_offsets.get(property).copied() +} + +/// Emits a fail-closed helper when reflection metadata is unavailable. +fn emit_reflection_attribute_new_stub(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // report helper failure when ReflectionAttribute metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // report helper failure when ReflectionAttribute metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + } +} + +/// Emits the ARM64 `ReflectionAttribute` materializer helper body. +fn emit_reflection_attribute_new_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let fail_label = "__elephc_eval_reflection_attribute_new_fail"; + let done_label = "__elephc_eval_reflection_attribute_new_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for inputs, object, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the attribute-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the attribute-name length + emitter.instruction("str x2, [sp, #16]"); // save the boxed eval argument array + emitter.instruction("str x3, [sp, #40]"); // save the PHP attribute target bitmask + emitter.instruction("str x4, [sp, #48]"); // save whether this attribute is repeated + emit_alloc_reflection_attribute_object_aarch64(emitter, layout); + emitter.instruction("str x0, [sp, #24]"); // save the unboxed ReflectionAttribute object pointer + emit_set_name_property_aarch64(emitter, layout); + emit_set_args_property_aarch64(emitter, layout, fail_label); + emit_set_factory_property_aarch64(emitter, layout); + emit_set_target_property_aarch64(emitter, layout); + emit_set_repeated_property_aarch64(emitter, layout); + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #24]"); // move the ReflectionAttribute object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the ReflectionAttribute object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection attribute to Rust +} + +/// Emits the x86_64 `ReflectionAttribute` materializer helper body. +fn emit_reflection_attribute_new_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + let fail_label = "__elephc_eval_reflection_attribute_new_fail_x"; + let done_label = "__elephc_eval_reflection_attribute_new_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve slots for inputs, object, and unboxed args + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the attribute-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the attribute-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the PHP attribute target bitmask + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save whether this attribute is repeated + emit_alloc_reflection_attribute_object_x86_64(emitter, layout); + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the unboxed ReflectionAttribute object pointer + emit_set_name_property_x86_64(emitter, layout); + emit_set_args_property_x86_64(emitter, layout, fail_label); + emit_set_factory_property_x86_64(emitter, layout); + emit_set_target_property_x86_64(emitter, layout); + emit_set_repeated_property_x86_64(emitter, layout); + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // move the ReflectionAttribute object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the ReflectionAttribute object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection attribute to Rust +} + +/// Allocates a zero-initialized ARM64 `ReflectionAttribute` object payload. +fn emit_alloc_reflection_attribute_object_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("mov x9, #4"); // heap kind 4 marks ReflectionAttribute as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "x0", offset); + abi::emit_store_zero_to_address(emitter, "x0", offset + 8); + } +} + +/// Allocates a zero-initialized x86_64 `ReflectionAttribute` object payload. +fn emit_alloc_reflection_attribute_object_x86_64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "rax", offset); + abi::emit_store_zero_to_address(emitter, "rax", offset + 8); + } +} + +/// Stores the incoming ARM64 attribute name into the object private slot. +fn emit_set_name_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x1, [sp, #0]"); // reload the attribute-name pointer for persistence + emitter.instruction("ldr x2, [sp, #8]"); // reload the attribute-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", layout.name_hi); +} + +/// Stores the incoming x86_64 attribute name into the object private slot. +fn emit_set_name_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the attribute-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the attribute-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "rax", "r10", layout.name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", layout.name_hi); +} + +/// Stores a retained ARM64 argument-array payload into the object private slot. +fn emit_set_args_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, + fail_label: &str, +) { + let args_ok_label = "__elephc_eval_reflection_attribute_args_ok"; + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed eval attribute-argument array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null argument arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the argument array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", args_ok_label)); // accept indexed argument arrays from older eval paths + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array argument metadata + emitter.label(args_ok_label); + emitter.instruction("str x1, [sp, #32]"); // save the unboxed argument array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the argument array for ReflectionAttribute ownership + emitter.instruction("ldr x1, [sp, #32]"); // reload the retained argument array payload + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.args_lo); + emitter.instruction("mov x10, #4"); // store the native property array tag expected by getArguments() + abi::emit_store_to_address(emitter, "x10", "x9", layout.args_hi); +} + +/// Stores a retained x86_64 argument-array payload into the object private slot. +fn emit_set_args_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, + fail_label: &str, +) { + let args_ok_label = "__elephc_eval_reflection_attribute_args_ok_x"; + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the boxed eval attribute-argument array + emitter.instruction("test rax, rax"); // check whether the boxed argument array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null argument arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the argument array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", args_ok_label)); // accept indexed argument arrays from older eval paths + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array argument metadata + emitter.label(args_ok_label); + emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the unboxed argument array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the argument array for ReflectionAttribute ownership + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the retained argument array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", layout.args_lo); + emitter.instruction("mov r11, 4"); // store the native property array tag expected by getArguments() + abi::emit_store_to_address(emitter, "r11", "r10", layout.args_hi); +} + +/// Stores factory id 0 on the ARM64 reflection object. +fn emit_set_factory_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_zero_to_address(emitter, "x9", layout.factory_lo); + abi::emit_store_zero_to_address(emitter, "x9", layout.factory_hi); +} + +/// Stores factory id 0 on the x86_64 reflection object. +fn emit_set_factory_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer + abi::emit_store_zero_to_address(emitter, "r10", layout.factory_lo); + abi::emit_store_zero_to_address(emitter, "r10", layout.factory_hi); +} + +/// Stores the eval-provided ARM64 target bitmask on the reflection object. +fn emit_set_target_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + emitter.instruction("ldr x10, [sp, #40]"); // reload the PHP attribute target bitmask + abi::emit_store_to_address(emitter, "x10", "x9", layout.target_lo); + abi::emit_store_zero_to_address(emitter, "x9", layout.target_hi); +} + +/// Stores the eval-provided x86_64 target bitmask on the reflection object. +fn emit_set_target_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the PHP attribute target bitmask + abi::emit_store_to_address(emitter, "r11", "r10", layout.target_lo); + abi::emit_store_zero_to_address(emitter, "r10", layout.target_hi); +} + +/// Stores the eval-provided ARM64 repeated flag on the reflection object. +fn emit_set_repeated_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + emitter.instruction("ldr x10, [sp, #48]"); // reload whether this attribute is repeated + abi::emit_store_to_address(emitter, "x10", "x9", layout.repeated_lo); + abi::emit_store_zero_to_address(emitter, "x9", layout.repeated_hi); +} + +/// Stores the eval-provided x86_64 repeated flag on the reflection object. +fn emit_set_repeated_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload whether this attribute is repeated + abi::emit_store_to_address(emitter, "r11", "r10", layout.repeated_lo); + abi::emit_store_zero_to_address(emitter, "r10", layout.repeated_hi); +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs new file mode 100644 index 0000000000..cc3eeef895 --- /dev/null +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -0,0 +1,2565 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician materialize +//! ReflectionClass, ReflectionObject, ReflectionFunction, ReflectionMethod, ReflectionParameter, +//! ReflectionProperty, ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects +//! with private metadata slots populated from runtime eval declarations. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - Reflection owner objects store private metadata slots such as `__attrs`, +//! `__name`, `__parameters`, and the ReflectionClass metadata-name arrays. +//! - The helper retains supplied array payloads for object ownership. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{Function, LocalKind, Module}; +use crate::types::ClassInfo; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Fixed object layout for one synthetic Reflection owner class. +struct ReflectionOwnerLayout { + class_id: u64, + property_count: usize, + name_lo: Option, + name_hi: Option, + short_name_lo: Option, + short_name_hi: Option, + namespace_name_lo: Option, + namespace_name_hi: Option, + interface_names_lo: Option, + interface_names_hi: Option, + trait_names_lo: Option, + trait_names_hi: Option, + method_names_lo: Option, + method_names_hi: Option, + property_names_lo: Option, + property_names_hi: Option, + method_objects_lo: Option, + method_objects_hi: Option, + property_objects_lo: Option, + property_objects_hi: Option, + constructor_lo: Option, + constructor_hi: Option, + parent_class_lo: Option, + parent_class_hi: Option, + value_lo: Option, + value_hi: Option, + backing_value_lo: Option, + backing_value_hi: Option, + attrs_lo: usize, + attrs_hi: usize, + is_final_lo: Option, + is_final_hi: Option, + is_abstract_lo: Option, + is_abstract_hi: Option, + is_interface_lo: Option, + is_interface_hi: Option, + is_trait_lo: Option, + is_trait_hi: Option, + is_enum_lo: Option, + is_enum_hi: Option, + is_readonly_lo: Option, + is_readonly_hi: Option, + is_anonymous_lo: Option, + is_anonymous_hi: Option, + is_instantiable_lo: Option, + is_instantiable_hi: Option, + is_cloneable_lo: Option, + is_cloneable_hi: Option, + is_iterable_lo: Option, + is_iterable_hi: Option, + is_internal_lo: Option, + is_internal_hi: Option, + is_user_defined_lo: Option, + is_user_defined_hi: Option, + is_deprecated_lo: Option, + is_deprecated_hi: Option, + modifiers_lo: Option, + modifiers_hi: Option, + in_namespace_lo: Option, + in_namespace_hi: Option, + is_static_lo: Option, + is_static_hi: Option, + is_public_lo: Option, + is_public_hi: Option, + is_protected_lo: Option, + is_protected_hi: Option, + is_private_lo: Option, + is_private_hi: Option, + is_enum_case_lo: Option, + is_enum_case_hi: Option, + required_parameter_count_lo: Option, + required_parameter_count_hi: Option, + position_lo: Option, + position_hi: Option, + is_optional_lo: Option, + is_optional_hi: Option, + is_variadic_lo: Option, + is_variadic_hi: Option, + is_passed_by_reference_lo: Option, + is_passed_by_reference_hi: Option, + has_type_lo: Option, + has_type_hi: Option, + parameter_type_lo: Option, + parameter_type_hi: Option, + settable_type_lo: Option, + settable_type_hi: Option, + parameter_class_lo: Option, + parameter_class_hi: Option, + has_default_value_lo: Option, + has_default_value_hi: Option, + is_default_value_constant_lo: Option, + is_default_value_constant_hi: Option, + is_promoted_lo: Option, + is_promoted_hi: Option, + is_virtual_lo: Option, + is_virtual_hi: Option, + is_dynamic_lo: Option, + is_dynamic_hi: Option, + default_value_lo: Option, + default_value_hi: Option, + default_value_constant_name_lo: Option, + default_value_constant_name_hi: Option, + declaring_function_lo: Option, + declaring_function_hi: Option, + allows_null_lo: Option, + allows_null_hi: Option, + is_array_type_lo: Option, + is_array_type_hi: Option, + is_callable_type_lo: Option, + is_callable_type_hi: Option, + is_builtin_lo: Option, + is_builtin_hi: Option, +} + +/// Layouts for the Reflection owner classes eval can materialize. +struct ReflectionOwnerLayouts { + class: ReflectionOwnerLayout, + object_class: ReflectionOwnerLayout, + enum_class: ReflectionOwnerLayout, + function: ReflectionOwnerLayout, + method: ReflectionOwnerLayout, + property: ReflectionOwnerLayout, + class_constant: ReflectionOwnerLayout, + enum_unit_case: ReflectionOwnerLayout, + enum_backed_case: ReflectionOwnerLayout, + parameter: ReflectionOwnerLayout, + named_type: ReflectionOwnerLayout, + union_type: ReflectionOwnerLayout, + intersection_type: ReflectionOwnerLayout, +} + +/// Emits eval Reflection owner helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_reflection_owner_helpers(module: &Module, emitter: &mut Emitter) { + if !module_uses_eval(module) { + return; + } + emitter.blank(); + emitter.comment("--- eval bridge: reflection owner helpers ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_owner_new"); + let Some(layouts) = reflection_owner_layouts(module) else { + emit_reflection_owner_new_stub(emitter); + return; + }; + match module.target.arch { + Arch::AArch64 => emit_reflection_owner_new_aarch64(emitter, &layouts), + Arch::X86_64 => emit_reflection_owner_new_x86_64(emitter, &layouts), + } +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns the Reflection owner object layouts from class metadata. +fn reflection_owner_layouts(module: &Module) -> Option { + Some(ReflectionOwnerLayouts { + class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, + object_class: reflection_owner_layout(module.class_infos.get("ReflectionObject")?, true)?, + enum_class: reflection_owner_layout(module.class_infos.get("ReflectionEnum")?, true)?, + function: reflection_owner_layout(module.class_infos.get("ReflectionFunction")?, true)?, + method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, true)?, + property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, true)?, + class_constant: reflection_owner_layout( + module.class_infos.get("ReflectionClassConstant")?, + true, + )?, + enum_unit_case: reflection_owner_layout( + module.class_infos.get("ReflectionEnumUnitCase")?, + true, + )?, + enum_backed_case: reflection_owner_layout( + module.class_infos.get("ReflectionEnumBackedCase")?, + true, + )?, + parameter: reflection_owner_layout(module.class_infos.get("ReflectionParameter")?, true)?, + named_type: reflection_owner_layout(module.class_infos.get("ReflectionNamedType")?, true)?, + union_type: reflection_owner_layout(module.class_infos.get("ReflectionUnionType")?, false)?, + intersection_type: reflection_owner_layout( + module.class_infos.get("ReflectionIntersectionType")?, + false, + )?, + }) +} + +/// Returns one Reflection owner layout from class metadata. +fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option { + let attrs_lo = reflection_property_offset(info, "__attrs")?; + let name_lo = has_name + .then(|| reflection_property_offset(info, "__name")) + .flatten(); + let short_name_lo = reflection_property_offset(info, "__short_name"); + let namespace_name_lo = reflection_property_offset(info, "__namespace_name"); + let interface_names_lo = reflection_property_offset(info, "__interface_names"); + let trait_names_lo = reflection_property_offset(info, "__trait_names"); + let method_names_lo = reflection_property_offset(info, "__method_names"); + let property_names_lo = reflection_property_offset(info, "__property_names"); + let method_objects_lo = reflection_property_offset(info, "__methods") + .or_else(|| reflection_property_offset(info, "__parameters")) + .or_else(|| reflection_property_offset(info, "__types")); + let property_objects_lo = reflection_property_offset(info, "__properties"); + let constructor_lo = reflection_property_offset(info, "__constructor"); + let parent_class_lo = reflection_property_offset(info, "__parent_class") + .or_else(|| reflection_property_offset(info, "__declaring_class")); + let value_lo = reflection_property_offset(info, "__value"); + let backing_value_lo = reflection_property_offset(info, "__backing_value"); + let is_final_lo = reflection_property_offset(info, "__is_final"); + let is_abstract_lo = reflection_property_offset(info, "__is_abstract"); + let is_interface_lo = reflection_property_offset(info, "__is_interface"); + let is_trait_lo = reflection_property_offset(info, "__is_trait"); + let is_enum_lo = reflection_property_offset(info, "__is_enum"); + let is_readonly_lo = reflection_property_offset(info, "__is_readonly"); + let is_anonymous_lo = reflection_property_offset(info, "__is_anonymous"); + let is_instantiable_lo = reflection_property_offset(info, "__is_instantiable"); + let is_cloneable_lo = reflection_property_offset(info, "__is_cloneable"); + let is_iterable_lo = reflection_property_offset(info, "__is_iterable"); + let is_internal_lo = reflection_property_offset(info, "__is_internal"); + let is_user_defined_lo = reflection_property_offset(info, "__is_user_defined"); + let is_deprecated_lo = reflection_property_offset(info, "__is_deprecated"); + let modifiers_lo = reflection_property_offset(info, "__modifiers"); + let in_namespace_lo = reflection_property_offset(info, "__in_namespace"); + let is_static_lo = reflection_property_offset(info, "__is_static"); + let is_public_lo = reflection_property_offset(info, "__is_public"); + let is_protected_lo = reflection_property_offset(info, "__is_protected"); + let is_private_lo = reflection_property_offset(info, "__is_private"); + let is_enum_case_lo = reflection_property_offset(info, "__is_enum_case"); + let required_parameter_count_lo = + reflection_property_offset(info, "__required_parameter_count"); + let position_lo = reflection_property_offset(info, "__position"); + let is_optional_lo = reflection_property_offset(info, "__is_optional") + .or_else(|| reflection_property_offset(info, "__optional")); + let is_variadic_lo = reflection_property_offset(info, "__is_variadic") + .or_else(|| reflection_property_offset(info, "__variadic")); + let is_passed_by_reference_lo = reflection_property_offset(info, "__is_passed_by_reference"); + let has_type_lo = reflection_property_offset(info, "__has_type"); + let parameter_type_lo = reflection_property_offset(info, "__type"); + let settable_type_lo = reflection_property_offset(info, "__settable_type"); + let parameter_class_lo = reflection_property_offset(info, "__class"); + let has_default_value_lo = reflection_property_offset(info, "__has_default_value"); + let is_default_value_constant_lo = + reflection_property_offset(info, "__is_default_value_constant"); + let is_promoted_lo = reflection_property_offset(info, "__is_promoted"); + let is_virtual_lo = reflection_property_offset(info, "__is_virtual"); + let is_dynamic_lo = reflection_property_offset(info, "__is_dynamic"); + let default_value_lo = reflection_property_offset(info, "__default_value"); + let default_value_constant_name_lo = + reflection_property_offset(info, "__default_value_constant_name"); + let declaring_function_lo = reflection_property_offset(info, "__declaring_function"); + let allows_null_lo = reflection_property_offset(info, "__allows_null"); + let is_array_type_lo = reflection_property_offset(info, "__is_array_type"); + let is_callable_type_lo = reflection_property_offset(info, "__is_callable_type"); + let is_builtin_lo = reflection_property_offset(info, "__is_builtin") + .or_else(|| reflection_property_offset(info, "__builtin")); + Some(ReflectionOwnerLayout { + class_id: info.class_id, + property_count: info.properties.len(), + name_lo, + name_hi: name_lo.map(|offset| offset + 8), + short_name_lo, + short_name_hi: short_name_lo.map(|offset| offset + 8), + namespace_name_lo, + namespace_name_hi: namespace_name_lo.map(|offset| offset + 8), + interface_names_lo, + interface_names_hi: interface_names_lo.map(|offset| offset + 8), + trait_names_lo, + trait_names_hi: trait_names_lo.map(|offset| offset + 8), + method_names_lo, + method_names_hi: method_names_lo.map(|offset| offset + 8), + property_names_lo, + property_names_hi: property_names_lo.map(|offset| offset + 8), + method_objects_lo, + method_objects_hi: method_objects_lo.map(|offset| offset + 8), + property_objects_lo, + property_objects_hi: property_objects_lo.map(|offset| offset + 8), + constructor_lo, + constructor_hi: constructor_lo.map(|offset| offset + 8), + parent_class_lo, + parent_class_hi: parent_class_lo.map(|offset| offset + 8), + value_lo, + value_hi: value_lo.map(|offset| offset + 8), + backing_value_lo, + backing_value_hi: backing_value_lo.map(|offset| offset + 8), + attrs_lo, + attrs_hi: attrs_lo + 8, + is_final_lo, + is_final_hi: is_final_lo.map(|offset| offset + 8), + is_abstract_lo, + is_abstract_hi: is_abstract_lo.map(|offset| offset + 8), + is_interface_lo, + is_interface_hi: is_interface_lo.map(|offset| offset + 8), + is_trait_lo, + is_trait_hi: is_trait_lo.map(|offset| offset + 8), + is_enum_lo, + is_enum_hi: is_enum_lo.map(|offset| offset + 8), + is_readonly_lo, + is_readonly_hi: is_readonly_lo.map(|offset| offset + 8), + is_anonymous_lo, + is_anonymous_hi: is_anonymous_lo.map(|offset| offset + 8), + is_instantiable_lo, + is_instantiable_hi: is_instantiable_lo.map(|offset| offset + 8), + is_cloneable_lo, + is_cloneable_hi: is_cloneable_lo.map(|offset| offset + 8), + is_iterable_lo, + is_iterable_hi: is_iterable_lo.map(|offset| offset + 8), + is_internal_lo, + is_internal_hi: is_internal_lo.map(|offset| offset + 8), + is_user_defined_lo, + is_user_defined_hi: is_user_defined_lo.map(|offset| offset + 8), + is_deprecated_lo, + is_deprecated_hi: is_deprecated_lo.map(|offset| offset + 8), + modifiers_lo, + modifiers_hi: modifiers_lo.map(|offset| offset + 8), + in_namespace_lo, + in_namespace_hi: in_namespace_lo.map(|offset| offset + 8), + is_static_lo, + is_static_hi: is_static_lo.map(|offset| offset + 8), + is_public_lo, + is_public_hi: is_public_lo.map(|offset| offset + 8), + is_protected_lo, + is_protected_hi: is_protected_lo.map(|offset| offset + 8), + is_private_lo, + is_private_hi: is_private_lo.map(|offset| offset + 8), + is_enum_case_lo, + is_enum_case_hi: is_enum_case_lo.map(|offset| offset + 8), + required_parameter_count_lo, + required_parameter_count_hi: required_parameter_count_lo.map(|offset| offset + 8), + position_lo, + position_hi: position_lo.map(|offset| offset + 8), + is_optional_lo, + is_optional_hi: is_optional_lo.map(|offset| offset + 8), + is_variadic_lo, + is_variadic_hi: is_variadic_lo.map(|offset| offset + 8), + is_passed_by_reference_lo, + is_passed_by_reference_hi: is_passed_by_reference_lo.map(|offset| offset + 8), + has_type_lo, + has_type_hi: has_type_lo.map(|offset| offset + 8), + parameter_type_lo, + parameter_type_hi: parameter_type_lo.map(|offset| offset + 8), + settable_type_lo, + settable_type_hi: settable_type_lo.map(|offset| offset + 8), + parameter_class_lo, + parameter_class_hi: parameter_class_lo.map(|offset| offset + 8), + has_default_value_lo, + has_default_value_hi: has_default_value_lo.map(|offset| offset + 8), + is_default_value_constant_lo, + is_default_value_constant_hi: is_default_value_constant_lo.map(|offset| offset + 8), + is_promoted_lo, + is_promoted_hi: is_promoted_lo.map(|offset| offset + 8), + is_virtual_lo, + is_virtual_hi: is_virtual_lo.map(|offset| offset + 8), + is_dynamic_lo, + is_dynamic_hi: is_dynamic_lo.map(|offset| offset + 8), + default_value_lo, + default_value_hi: default_value_lo.map(|offset| offset + 8), + default_value_constant_name_lo, + default_value_constant_name_hi: default_value_constant_name_lo.map(|offset| offset + 8), + declaring_function_lo, + declaring_function_hi: declaring_function_lo.map(|offset| offset + 8), + allows_null_lo, + allows_null_hi: allows_null_lo.map(|offset| offset + 8), + is_array_type_lo, + is_array_type_hi: is_array_type_lo.map(|offset| offset + 8), + is_callable_type_lo, + is_callable_type_hi: is_callable_type_lo.map(|offset| offset + 8), + is_builtin_lo, + is_builtin_hi: is_builtin_lo.map(|offset| offset + 8), + }) +} + +/// Returns one declared property offset from the synthetic reflection class layout. +fn reflection_property_offset(info: &ClassInfo, property: &str) -> Option { + info.property_offsets.get(property).copied() +} + +/// Emits a fail-closed helper when Reflection owner metadata is unavailable. +fn emit_reflection_owner_new_stub(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + } +} + +/// Emits the ARM64 Reflection owner materializer helper body. +fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &ReflectionOwnerLayouts) { + let fail_label = "__elephc_eval_reflection_owner_new_fail"; + let done_label = "__elephc_eval_reflection_owner_new_done"; + let box_label = "__elephc_eval_reflection_owner_new_box"; + let class_label = "__elephc_eval_reflection_owner_new_class"; + let object_label = "__elephc_eval_reflection_owner_new_object"; + let enum_label = "__elephc_eval_reflection_owner_new_enum"; + let function_label = "__elephc_eval_reflection_owner_new_function"; + let method_label = "__elephc_eval_reflection_owner_new_method"; + let property_label = "__elephc_eval_reflection_owner_new_property"; + let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant"; + let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case"; + let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case"; + let parameter_label = "__elephc_eval_reflection_owner_new_parameter"; + let named_type_label = "__elephc_eval_reflection_owner_new_named_type"; + let union_type_label = "__elephc_eval_reflection_owner_new_union_type"; + let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type"; + emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr + emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind + emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer + emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array + emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array + emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array + emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array + emitter.instruction("ldr x8, [sp, #160]"); // load the boxed ReflectionClass method objects array from the first stack argument + emitter.instruction("str x8, [sp, #120]"); // save the boxed ReflectionClass method objects array + emitter.instruction("ldr x8, [sp, #168]"); // load the boxed ReflectionClass property objects array from the second stack argument + emitter.instruction("str x8, [sp, #128]"); // save the boxed ReflectionClass property objects array + emitter.instruction("ldr x8, [sp, #176]"); // load the boxed ReflectionClass parent value from the third stack argument + emitter.instruction("str x8, [sp, #136]"); // save the boxed ReflectionClass parent value + emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass modifier flags from the fourth stack argument + emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags + emitter.instruction("ldr x8, [sp, #192]"); // load owner modifier/count metadata from the fifth stack argument + emitter.instruction("str x8, [sp, #96]"); // save owner modifier/count metadata + emitter.instruction("ldr x8, [sp, #200]"); // load ReflectionMethod getModifiers bitmask from the sixth stack argument + emitter.instruction("str x8, [sp, #72]"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("ldr x8, [sp, #208]"); // load boxed ReflectionClassConstant value from the seventh stack argument + emitter.instruction("str x8, [sp, #56]"); // save boxed ReflectionClassConstant value + emitter.instruction("ldr x8, [sp, #216]"); // load boxed ReflectionEnumBackedCase backing value from the eighth stack argument + emitter.instruction("str x8, [sp, #64]"); // save boxed ReflectionEnumBackedCase backing value + emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp x0, #6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("b.eq {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction("cmp x0, #7"); // owner kind 7 means ReflectionNamedType + emitter.instruction(&format!("b.eq {}", named_type_label)); // allocate a ReflectionNamedType owner + emitter.instruction("cmp x0, #8"); // owner kind 8 means ReflectionUnionType + emitter.instruction(&format!("b.eq {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp x0, #9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp x0, #10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction("cmp x0, #11"); // owner kind 11 means ReflectionEnum + emitter.instruction(&format!("b.eq {}", enum_label)); // allocate a ReflectionEnum owner + emitter.instruction("cmp x0, #12"); // owner kind 12 means ReflectionObject + emitter.instruction(&format!("b.eq {}", object_label)); // allocate a ReflectionObject owner + emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds + emit_aarch64_owner_kind_body( + emitter, + class_label, + &layouts.class, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + object_label, + &layouts.object_class, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + enum_label, + &layouts.enum_class, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + function_label, + &layouts.function, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + method_label, + &layouts.method, + true, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + property_label, + &layouts.property, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + class_constant_label, + &layouts.class_constant, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + enum_unit_case_label, + &layouts.enum_unit_case, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + enum_backed_case_label, + &layouts.enum_backed_case, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + parameter_label, + &layouts.parameter, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + named_type_label, + &layouts.named_type, + true, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + union_type_label, + &layouts.union_type, + false, + false, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + intersection_type_label, + &layouts.intersection_type, + false, + false, + fail_label, + box_label, + ); + emitter.label(box_label); + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #160"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection owner to Rust +} + +/// Emits the x86_64 Reflection owner materializer helper body. +fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionOwnerLayouts) { + let fail_label = "__elephc_eval_reflection_owner_new_fail_x"; + let done_label = "__elephc_eval_reflection_owner_new_done_x"; + let box_label = "__elephc_eval_reflection_owner_new_box_x"; + let class_label = "__elephc_eval_reflection_owner_new_class_x"; + let object_label = "__elephc_eval_reflection_owner_new_object_x"; + let enum_label = "__elephc_eval_reflection_owner_new_enum_x"; + let function_label = "__elephc_eval_reflection_owner_new_function_x"; + let method_label = "__elephc_eval_reflection_owner_new_method_x"; + let property_label = "__elephc_eval_reflection_owner_new_property_x"; + let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant_x"; + let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case_x"; + let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case_x"; + let parameter_label = "__elephc_eval_reflection_owner_new_parameter_x"; + let named_type_label = "__elephc_eval_reflection_owner_new_named_type_x"; + let union_type_label = "__elephc_eval_reflection_owner_new_union_type_x"; + let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument + emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument + emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load the boxed ReflectionClass parent value from the fifth stack argument + emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // save the boxed ReflectionClass parent value + emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass modifier flags from the sixth stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags + emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load owner modifier/count metadata from the seventh stack argument + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save owner modifier/count metadata + emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load ReflectionMethod getModifiers bitmask from the eighth stack argument + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionClassConstant value from the ninth stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save boxed ReflectionClassConstant value + emitter.instruction("mov rax, QWORD PTR [rbp + 88]"); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save boxed ReflectionEnumBackedCase backing value + emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp rdi, 6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("je {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction("cmp rdi, 7"); // owner kind 7 means ReflectionNamedType + emitter.instruction(&format!("je {}", named_type_label)); // allocate a ReflectionNamedType owner + emitter.instruction("cmp rdi, 8"); // owner kind 8 means ReflectionUnionType + emitter.instruction(&format!("je {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp rdi, 9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp rdi, 10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction("cmp rdi, 11"); // owner kind 11 means ReflectionEnum + emitter.instruction(&format!("je {}", enum_label)); // allocate a ReflectionEnum owner + emitter.instruction("cmp rdi, 12"); // owner kind 12 means ReflectionObject + emitter.instruction(&format!("je {}", object_label)); // allocate a ReflectionObject owner + emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds + emit_x86_64_owner_kind_body( + emitter, + class_label, + &layouts.class, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + object_label, + &layouts.object_class, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + enum_label, + &layouts.enum_class, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + function_label, + &layouts.function, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + method_label, + &layouts.method, + true, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + property_label, + &layouts.property, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + class_constant_label, + &layouts.class_constant, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + enum_unit_case_label, + &layouts.enum_unit_case, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + enum_backed_case_label, + &layouts.enum_backed_case, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + parameter_label, + &layouts.parameter, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + named_type_label, + &layouts.named_type, + true, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + union_type_label, + &layouts.union_type, + false, + false, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + intersection_type_label, + &layouts.intersection_type, + false, + false, + fail_label, + box_label, + ); + emitter.label(box_label); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection owner to Rust +} + +/// Emits one ARM64 owner-kind allocation and slot-population body. +fn emit_aarch64_owner_kind_body( + emitter: &mut Emitter, + label: &str, + layout: &ReflectionOwnerLayout, + set_name: bool, + is_method: bool, + fail_label: &str, + box_label: &str, +) { + emitter.label(label); + emit_alloc_reflection_owner_object_aarch64(emitter, layout); + emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer + if set_name { + emit_set_owner_name_property_aarch64(emitter, layout); + } + emit_set_owner_class_flags_property_aarch64(emitter, layout); + emit_set_owner_member_flags_property_aarch64(emitter, layout, is_method); + emit_set_owner_constant_value_property_aarch64(emitter, layout, fail_label); + emit_set_owner_settable_type_property_aarch64(emitter, layout); + emit_set_owner_backing_value_property_aarch64(emitter, layout, fail_label); + emit_set_owner_required_parameter_count_property_aarch64(emitter, layout); + emit_set_owner_named_type_flags_property_aarch64(emitter, layout); + emit_set_owner_parameter_property_aarch64(emitter, layout); + emit_set_owner_parameter_type_property_aarch64(emitter, layout, fail_label); + emit_set_owner_parameter_class_property_aarch64(emitter, layout, fail_label); + emit_set_owner_parameter_default_property_aarch64(emitter, layout, fail_label); + emit_set_owner_parameter_default_constant_name_property_aarch64(emitter, layout, fail_label); + emit_set_owner_metadata_arrays_property_aarch64(emitter, layout, fail_label); + emit_set_owner_constructor_property_aarch64(emitter, layout, fail_label); + emit_set_owner_parent_class_property_aarch64(emitter, layout, fail_label); + emit_set_owner_declaring_function_property_aarch64(emitter, layout, fail_label); + emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); + emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object +} + +/// Emits one x86_64 owner-kind allocation and slot-population body. +fn emit_x86_64_owner_kind_body( + emitter: &mut Emitter, + label: &str, + layout: &ReflectionOwnerLayout, + set_name: bool, + is_method: bool, + fail_label: &str, + box_label: &str, +) { + emitter.label(label); + emit_alloc_reflection_owner_object_x86_64(emitter, layout); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer + if set_name { + emit_set_owner_name_property_x86_64(emitter, layout); + } + emit_set_owner_class_flags_property_x86_64(emitter, layout); + emit_set_owner_member_flags_property_x86_64(emitter, layout, is_method); + emit_set_owner_constant_value_property_x86_64(emitter, layout, fail_label); + emit_set_owner_settable_type_property_x86_64(emitter, layout); + emit_set_owner_backing_value_property_x86_64(emitter, layout, fail_label); + emit_set_owner_required_parameter_count_property_x86_64(emitter, layout); + emit_set_owner_named_type_flags_property_x86_64(emitter, layout); + emit_set_owner_parameter_property_x86_64(emitter, layout); + emit_set_owner_parameter_type_property_x86_64(emitter, layout, fail_label); + emit_set_owner_parameter_class_property_x86_64(emitter, layout, fail_label); + emit_set_owner_parameter_default_property_x86_64(emitter, layout, fail_label); + emit_set_owner_parameter_default_constant_name_property_x86_64(emitter, layout, fail_label); + emit_set_owner_metadata_arrays_property_x86_64(emitter, layout, fail_label); + emit_set_owner_constructor_property_x86_64(emitter, layout, fail_label); + emit_set_owner_parent_class_property_x86_64(emitter, layout, fail_label); + emit_set_owner_declaring_function_property_x86_64(emitter, layout, fail_label); + emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); + emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object +} + +/// Allocates a zero-initialized ARM64 Reflection owner object payload. +fn emit_alloc_reflection_owner_object_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "x0", offset); + abi::emit_store_zero_to_address(emitter, "x0", offset + 8); + } +} + +/// Allocates a zero-initialized x86_64 Reflection owner object payload. +fn emit_alloc_reflection_owner_object_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "rax", offset); + abi::emit_store_zero_to_address(emitter, "rax", offset + 8); + } +} + +/// Stores the incoming ARM64 reflected class name into ReflectionClass. +fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &ReflectionOwnerLayout) { + let Some(name_lo) = layout.name_lo else { + return; + }; + let Some(name_hi) = layout.name_hi else { + return; + }; + emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence + emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", name_hi); + let ( + Some(short_name_lo), + Some(short_name_hi), + Some(namespace_name_lo), + Some(namespace_name_hi), + Some(in_namespace_lo), + Some(in_namespace_hi), + ) = ( + layout.short_name_lo, + layout.short_name_hi, + layout.namespace_name_lo, + layout.namespace_name_hi, + layout.in_namespace_lo, + layout.in_namespace_hi, + ) + else { + emit_set_owner_low_bit_final_property_aarch64(emitter, layout); + return; + }; + let scan_loop_label = format!( + "__elephc_eval_reflection_owner_name_scan_loop_{}", + layout.class_id + ); + let found_label = format!( + "__elephc_eval_reflection_owner_name_scan_found_{}", + layout.class_id + ); + let no_namespace_label = format!( + "__elephc_eval_reflection_owner_name_scan_none_{}", + layout.class_id + ); + let store_parts_label = format!( + "__elephc_eval_reflection_owner_name_store_parts_{}", + layout.class_id + ); + emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting + emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting + emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte + emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component + emitter.label(&scan_loop_label); + emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte + emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator + emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.label(&no_namespace_label); + emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer + emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length + emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names + emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path + emitter.label(&found_label); + emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator + emitter.instruction("add x7, x3, x6"); // compute the short-name pointer + emitter.instruction("sub x8, x4, x6"); // compute the short-name length + emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls + emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls + emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset + emitter.label(&store_parts_label); + emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence + emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length + emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", namespace_name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", namespace_name_hi); + emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present + emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() + abi::emit_store_to_address(emitter, "x10", "x9", in_namespace_lo); + abi::emit_store_zero_to_address(emitter, "x9", in_namespace_hi); + emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer + emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length + emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", short_name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", short_name_hi); +} + +/// Stores the incoming x86_64 reflected class name into ReflectionClass. +fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &ReflectionOwnerLayout) { + let Some(name_lo) = layout.name_lo else { + return; + }; + let Some(name_hi) = layout.name_hi else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rax", "r10", name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); + let ( + Some(short_name_lo), + Some(short_name_hi), + Some(namespace_name_lo), + Some(namespace_name_hi), + Some(in_namespace_lo), + Some(in_namespace_hi), + ) = ( + layout.short_name_lo, + layout.short_name_hi, + layout.namespace_name_lo, + layout.namespace_name_hi, + layout.in_namespace_lo, + layout.in_namespace_hi, + ) + else { + return; + }; + let scan_loop_label = format!( + "__elephc_eval_reflection_owner_name_scan_loop_{}_x", + layout.class_id + ); + let found_label = format!( + "__elephc_eval_reflection_owner_name_scan_found_{}_x", + layout.class_id + ); + let no_namespace_label = format!( + "__elephc_eval_reflection_owner_name_scan_none_{}_x", + layout.class_id + ); + let store_parts_label = format!( + "__elephc_eval_reflection_owner_name_store_parts_{}_x", + layout.class_id + ); + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting + emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte + emitter.instruction("test r11, r11"); // check whether the reflected name is empty + emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component + emitter.label(&scan_loop_label); + emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte + emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator + emitter.instruction("test r11, r11"); // check whether the first byte has been examined + emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.label(&no_namespace_label); + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names + emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path + emitter.label(&found_label); + emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator + emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer + emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix + emitter.instruction("sub rcx, rax"); // compute the short-name length + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset + emitter.label(&store_parts_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length + emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rax", "r10", namespace_name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", namespace_name_hi); + emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present + emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() + emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word + abi::emit_store_to_address(emitter, "rax", "r10", in_namespace_lo); + abi::emit_store_zero_to_address(emitter, "r10", in_namespace_hi); + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length + emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rax", "r10", short_name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", short_name_hi); +} + +/// Stores incoming ARM64 ReflectionClass boolean modifier flags. +fn emit_set_owner_class_flags_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(is_final_lo), + Some(is_final_hi), + Some(is_abstract_lo), + Some(is_abstract_hi), + Some(is_interface_lo), + Some(is_interface_hi), + Some(is_trait_lo), + Some(is_trait_hi), + Some(is_enum_lo), + Some(is_enum_hi), + Some(is_readonly_lo), + Some(is_readonly_hi), + Some(is_anonymous_lo), + Some(is_anonymous_hi), + Some(is_instantiable_lo), + Some(is_instantiable_hi), + Some(is_cloneable_lo), + Some(is_cloneable_hi), + Some(is_iterable_lo), + Some(is_iterable_hi), + Some(is_internal_lo), + Some(is_internal_hi), + Some(is_user_defined_lo), + Some(is_user_defined_hi), + Some(modifiers_lo), + Some(modifiers_hi), + ) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + layout.is_interface_lo, + layout.is_interface_hi, + layout.is_trait_lo, + layout.is_trait_hi, + layout.is_enum_lo, + layout.is_enum_hi, + layout.is_readonly_lo, + layout.is_readonly_hi, + layout.is_anonymous_lo, + layout.is_anonymous_hi, + layout.is_instantiable_lo, + layout.is_instantiable_hi, + layout.is_cloneable_lo, + layout.is_cloneable_hi, + layout.is_iterable_lo, + layout.is_iterable_hi, + layout.is_internal_lo, + layout.is_internal_hi, + layout.is_user_defined_lo, + layout.is_user_defined_hi, + layout.modifiers_lo, + layout.modifiers_hi, + ) + else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); + emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); + emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position + emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_interface_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_interface_hi); + emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position + emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_trait_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_trait_hi); + emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position + emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_enum_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_enum_hi); + emitter.instruction("lsr x10, x11, #5"); // move the readonly-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the readonly-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); + emitter.instruction("lsr x10, x11, #6"); // move the instantiable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the instantiable-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_instantiable_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_instantiable_hi); + emitter.instruction("lsr x10, x11, #7"); // move the cloneable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the cloneable-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_cloneable_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_cloneable_hi); + emitter.instruction("lsr x10, x11, #8"); // move the internal-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the internal-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_internal_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_internal_hi); + emitter.instruction("lsr x10, x11, #9"); // move the user-defined-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the user-defined-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_user_defined_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_user_defined_hi); + emitter.instruction("lsr x10, x11, #10"); // move the iterable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the iterable-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_iterable_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_iterable_hi); + emitter.instruction("lsr x10, x11, #11"); // move the anonymous-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the anonymous-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_anonymous_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_anonymous_hi); + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask + abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); +} + +/// Stores incoming x86_64 ReflectionClass boolean modifier flags. +fn emit_set_owner_class_flags_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(is_final_lo), + Some(is_final_hi), + Some(is_abstract_lo), + Some(is_abstract_hi), + Some(is_interface_lo), + Some(is_interface_hi), + Some(is_trait_lo), + Some(is_trait_hi), + Some(is_enum_lo), + Some(is_enum_hi), + Some(is_readonly_lo), + Some(is_readonly_hi), + Some(is_anonymous_lo), + Some(is_anonymous_hi), + Some(is_instantiable_lo), + Some(is_instantiable_hi), + Some(is_cloneable_lo), + Some(is_cloneable_hi), + Some(is_iterable_lo), + Some(is_iterable_hi), + Some(is_internal_lo), + Some(is_internal_hi), + Some(is_user_defined_lo), + Some(is_user_defined_hi), + Some(modifiers_lo), + Some(modifiers_hi), + ) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + layout.is_interface_lo, + layout.is_interface_hi, + layout.is_trait_lo, + layout.is_trait_hi, + layout.is_enum_lo, + layout.is_enum_hi, + layout.is_readonly_lo, + layout.is_readonly_hi, + layout.is_anonymous_lo, + layout.is_anonymous_hi, + layout.is_instantiable_lo, + layout.is_instantiable_hi, + layout.is_cloneable_lo, + layout.is_cloneable_hi, + layout.is_iterable_lo, + layout.is_iterable_hi, + layout.is_internal_lo, + layout.is_internal_hi, + layout.is_user_defined_lo, + layout.is_user_defined_hi, + layout.modifiers_lo, + layout.modifiers_hi, + ) + else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 1"); // move the abstract-class bit into position + emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit + emitter.instruction("shr rax, 2"); // move the interface bit into position + emitter.instruction("and rax, 1"); // extract the interface flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit + emitter.instruction("shr rax, 3"); // move the trait bit into position + emitter.instruction("and rax, 1"); // extract the trait flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit + emitter.instruction("shr rax, 4"); // move the enum bit into position + emitter.instruction("and rax, 1"); // extract the enum flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly-class bit + emitter.instruction("shr rax, 5"); // move the readonly-class bit into position + emitter.instruction("and rax, 1"); // extract the readonly-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the instantiable bit + emitter.instruction("shr rax, 6"); // move the instantiable-class bit into position + emitter.instruction("and rax, 1"); // extract the instantiable-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_instantiable_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_instantiable_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the cloneable bit + emitter.instruction("shr rax, 7"); // move the cloneable-class bit into position + emitter.instruction("and rax, 1"); // extract the cloneable-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_cloneable_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_cloneable_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the internal bit + emitter.instruction("shr rax, 8"); // move the internal-class bit into position + emitter.instruction("and rax, 1"); // extract the internal-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_internal_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_internal_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the user-defined bit + emitter.instruction("shr rax, 9"); // move the user-defined-class bit into position + emitter.instruction("and rax, 1"); // extract the user-defined-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_user_defined_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_user_defined_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the iterable bit + emitter.instruction("shr rax, 10"); // move the iterable-class bit into position + emitter.instruction("and rax, 1"); // extract the iterable-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_iterable_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_iterable_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the anonymous bit + emitter.instruction("shr rax, 11"); // move the anonymous-class bit into position + emitter.instruction("and rax, 1"); // extract the anonymous-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_anonymous_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_anonymous_hi); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask + abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); +} + +/// Stores incoming ARM64 ReflectionMethod/ReflectionProperty boolean flags. +#[rustfmt::skip] +fn emit_set_owner_member_flags_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + is_method: bool, +) { + let ( + Some(is_public_lo), + Some(is_public_hi), + Some(is_protected_lo), + Some(is_protected_hi), + Some(is_private_lo), + Some(is_private_hi), + ) = ( + layout.is_public_lo, + layout.is_public_hi, + layout.is_protected_lo, + layout.is_protected_hi, + layout.is_private_lo, + layout.is_private_hi, + ) + else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection member predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { + emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_static_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_static_hi); + } + emitter.instruction("lsr x10, x11, #1"); // move the public-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the public-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_public_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_public_hi); + emitter.instruction("lsr x10, x11, #2"); // move the protected-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the protected-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_protected_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_protected_hi); + emitter.instruction("lsr x10, x11, #3"); // move the private-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the private-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_private_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_private_hi); + if let (Some(is_enum_case_lo), Some(is_enum_case_hi)) = + (layout.is_enum_case_lo, layout.is_enum_case_hi) + { + emitter.instruction("lsr x10, x11, #7"); // move the enum-case flag into position + emitter.instruction("and x10, x10, #1"); // extract the enum-case flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_enum_case_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_enum_case_hi); + } + if let (Some(is_readonly_lo), Some(is_readonly_hi)) = + (layout.is_readonly_lo, layout.is_readonly_hi) + { + emitter.instruction("lsr x10, x11, #6"); // move the readonly-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the readonly-property flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); + } + if let (Some(has_default_value_lo), Some(has_default_value_hi)) = + (layout.has_default_value_lo, layout.has_default_value_hi) + { + emitter.instruction("lsr x10, x11, #8"); // move the default-value bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); + } + if let (Some(is_promoted_lo), Some(is_promoted_hi)) = + (layout.is_promoted_lo, layout.is_promoted_hi) + { + emitter.instruction("lsr x10, x11, #9"); // move the promoted-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the promoted-property flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); + } + if let (Some(is_virtual_lo), Some(is_virtual_hi)) = (layout.is_virtual_lo, layout.is_virtual_hi) + { + emitter.instruction("lsr x10, x11, #10"); // move the virtual-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the virtual-property flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_virtual_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_virtual_hi); + } + if let (Some(is_dynamic_lo), Some(is_dynamic_hi)) = (layout.is_dynamic_lo, layout.is_dynamic_hi) + { + emitter.instruction("lsr x10, x11, #13"); // move the dynamic-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the dynamic-property flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_dynamic_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_dynamic_hi); + } + if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { + if is_method { + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionMethod predicate flags + emitter.instruction("and x10, x11, #14"); // keep public/protected/private visibility flags + emitter.instruction("lsr x10, x10, #1"); // shift visibility flags into PHP modifier bit positions + emitter.instruction("and x12, x11, #1"); // isolate the static-method flag + emitter.instruction("lsl x12, x12, #4"); // move static into PHP modifier bit 16 + emitter.instruction("orr x10, x10, x12"); // merge static into the method modifier bitmask + emitter.instruction("and x12, x11, #48"); // keep final/abstract method flags + emitter.instruction("lsl x12, x12, #1"); // move final/abstract into PHP modifier bit positions + emitter.instruction("orr x10, x10, x12"); // merge final/abstract into the method modifier bitmask + } else { + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask + } + abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); + } + if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { + emitter.instruction("lsr x10, x11, #4"); // move the final-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the final-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); + } + if let (Some(is_abstract_lo), Some(is_abstract_hi)) = + (layout.is_abstract_lo, layout.is_abstract_hi) + { + emitter.instruction("lsr x10, x11, #5"); // move the abstract-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); + } +} + +/// Stores incoming bit-zero finality for ARM64 owners without full member flags. +fn emit_set_owner_low_bit_final_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection owner predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract bit-zero finality as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); +} + +/// Stores incoming x86_64 ReflectionMethod/ReflectionProperty boolean flags. +#[rustfmt::skip] +fn emit_set_owner_member_flags_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + is_method: bool, +) { + let ( + Some(is_public_lo), + Some(is_public_hi), + Some(is_protected_lo), + Some(is_protected_hi), + Some(is_private_lo), + Some(is_private_hi), + ) = ( + layout.is_public_lo, + layout.is_public_hi, + layout.is_protected_lo, + layout.is_protected_hi, + layout.is_private_lo, + layout.is_private_hi, + ) + else { + emit_set_owner_low_bit_final_property_x86_64(emitter, layout); + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { + emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit + emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_static_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_static_hi); + } + emitter.instruction("mov rax, r11"); // copy flags before extracting the public bit + emitter.instruction("shr rax, 1"); // move the public-member bit into position + emitter.instruction("and rax, 1"); // extract the public-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_public_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_public_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the protected bit + emitter.instruction("shr rax, 2"); // move the protected-member bit into position + emitter.instruction("and rax, 1"); // extract the protected-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_protected_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_protected_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the private bit + emitter.instruction("shr rax, 3"); // move the private-member bit into position + emitter.instruction("and rax, 1"); // extract the private-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_private_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_private_hi); + if let (Some(is_enum_case_lo), Some(is_enum_case_hi)) = + (layout.is_enum_case_lo, layout.is_enum_case_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum-case bit + emitter.instruction("shr rax, 7"); // move the enum-case bit into position + emitter.instruction("and rax, 1"); // extract the enum-case flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_enum_case_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_enum_case_hi); + } + if let (Some(is_readonly_lo), Some(is_readonly_hi)) = + (layout.is_readonly_lo, layout.is_readonly_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly bit + emitter.instruction("shr rax, 6"); // move the readonly-property bit into position + emitter.instruction("and rax, 1"); // extract the readonly-property flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); + } + if let (Some(has_default_value_lo), Some(has_default_value_hi)) = + (layout.has_default_value_lo, layout.has_default_value_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit + emitter.instruction("shr rax, 8"); // move the default-value bit into position + emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); + } + if let (Some(is_promoted_lo), Some(is_promoted_hi)) = + (layout.is_promoted_lo, layout.is_promoted_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted-property bit + emitter.instruction("shr rax, 9"); // move the promoted-property bit into position + emitter.instruction("and rax, 1"); // extract the promoted-property flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); + } + if let (Some(is_virtual_lo), Some(is_virtual_hi)) = (layout.is_virtual_lo, layout.is_virtual_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the virtual-property bit + emitter.instruction("shr rax, 10"); // move the virtual-property bit into position + emitter.instruction("and rax, 1"); // extract the virtual-property flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_virtual_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_virtual_hi); + } + if let (Some(is_dynamic_lo), Some(is_dynamic_hi)) = (layout.is_dynamic_lo, layout.is_dynamic_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the dynamic-property bit + emitter.instruction("shr rax, 13"); // move the dynamic-property bit into position + emitter.instruction("and rax, 1"); // extract the dynamic-property flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_dynamic_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_dynamic_hi); + } + if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { + if is_method { + emitter.instruction("mov rax, r11"); // copy ReflectionMethod predicate flags for visibility + emitter.instruction("and rax, 14"); // keep public/protected/private visibility flags + emitter.instruction("shr rax, 1"); // shift visibility flags into PHP modifier bit positions + emitter.instruction("mov rdx, r11"); // copy ReflectionMethod predicate flags for static + emitter.instruction("and rdx, 1"); // isolate the static-method flag + emitter.instruction("shl rdx, 4"); // move static into PHP modifier bit 16 + emitter.instruction("or rax, rdx"); // merge static into the method modifier bitmask + emitter.instruction("mov rdx, r11"); // copy ReflectionMethod predicate flags for final/abstract + emitter.instruction("and rdx, 48"); // keep final/abstract method flags + emitter.instruction("shl rdx, 1"); // move final/abstract into PHP modifier bit positions + emitter.instruction("or rax, rdx"); // merge final/abstract into the method modifier bitmask + } else { + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask + } + abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); + } + if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("shr rax, 4"); // move the final-member bit into position + emitter.instruction("and rax, 1"); // extract the final-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); + } + if let (Some(is_abstract_lo), Some(is_abstract_hi)) = + (layout.is_abstract_lo, layout.is_abstract_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 5"); // move the abstract-member bit into position + emitter.instruction("and rax, 1"); // extract the abstract-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + } +} + +/// Stores incoming bit-zero finality for x86_64 owners without full member flags. +fn emit_set_owner_low_bit_final_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection owner predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting bit-zero finality + emitter.instruction("and rax, 1"); // extract bit-zero finality as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); +} + +/// Stores incoming ARM64 ReflectionMethod required-parameter count. +fn emit_set_owner_required_parameter_count_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(required_parameter_count_lo), Some(required_parameter_count_hi)) = ( + layout.required_parameter_count_lo, + layout.required_parameter_count_hi, + ) else { + return; + }; + emitter.instruction("ldr x10, [sp, #96]"); // reload ReflectionMethod required-parameter count + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x10", "x9", required_parameter_count_lo); + abi::emit_store_zero_to_address(emitter, "x9", required_parameter_count_hi); + if let (Some(is_deprecated_lo), Some(is_deprecated_hi)) = + (layout.is_deprecated_lo, layout.is_deprecated_hi) + { + emitter.instruction("ldr x10, [sp, #48]"); // reload ReflectionFunctionAbstract predicate flags + emitter.instruction("lsr x10, x10, #14"); // move the deprecated-callable bit into position + emitter.instruction("and x10, x10, #1"); // extract the deprecated-callable flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_deprecated_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_deprecated_hi); + } +} + +/// Stores incoming x86_64 ReflectionMethod required-parameter count. +fn emit_set_owner_required_parameter_count_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(required_parameter_count_lo), Some(required_parameter_count_hi)) = ( + layout.required_parameter_count_lo, + layout.required_parameter_count_hi, + ) else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload ReflectionMethod required-parameter count + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rax", "r10", required_parameter_count_lo); + abi::emit_store_zero_to_address(emitter, "r10", required_parameter_count_hi); + if let (Some(is_deprecated_lo), Some(is_deprecated_hi)) = + (layout.is_deprecated_lo, layout.is_deprecated_hi) + { + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload ReflectionFunctionAbstract predicate flags + emitter.instruction("shr rax, 14"); // move the deprecated-callable bit into position + emitter.instruction("and rax, 1"); // extract the deprecated-callable flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_deprecated_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_deprecated_hi); + } +} + +/// Stores incoming ARM64 ReflectionParameter position and predicate flags. +fn emit_set_owner_parameter_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(position_lo), + Some(position_hi), + Some(is_optional_lo), + Some(is_optional_hi), + Some(is_variadic_lo), + Some(is_variadic_hi), + Some(is_passed_by_reference_lo), + Some(is_passed_by_reference_hi), + Some(has_type_lo), + Some(has_type_hi), + Some(has_default_value_lo), + Some(has_default_value_hi), + Some(is_default_value_constant_lo), + Some(is_default_value_constant_hi), + Some(is_promoted_lo), + Some(is_promoted_hi), + Some(allows_null_lo), + Some(allows_null_hi), + Some(is_array_type_lo), + Some(is_array_type_hi), + Some(is_callable_type_lo), + Some(is_callable_type_hi), + ) = ( + layout.position_lo, + layout.position_hi, + layout.is_optional_lo, + layout.is_optional_hi, + layout.is_variadic_lo, + layout.is_variadic_hi, + layout.is_passed_by_reference_lo, + layout.is_passed_by_reference_hi, + layout.has_type_lo, + layout.has_type_hi, + layout.has_default_value_lo, + layout.has_default_value_hi, + layout.is_default_value_constant_lo, + layout.is_default_value_constant_hi, + layout.is_promoted_lo, + layout.is_promoted_hi, + layout.allows_null_lo, + layout.allows_null_hi, + layout.is_array_type_lo, + layout.is_array_type_hi, + layout.is_callable_type_lo, + layout.is_callable_type_hi, + ) + else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionParameter predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + emitter.instruction("ldr x10, [sp, #96]"); // reload the zero-based parameter position + abi::emit_store_to_address(emitter, "x10", "x9", position_lo); + abi::emit_store_zero_to_address(emitter, "x9", position_hi); + emitter.instruction("and x10, x11, #1"); // extract the optional-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_optional_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_optional_hi); + emitter.instruction("lsr x10, x11, #1"); // move the variadic-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the variadic-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_variadic_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_variadic_hi); + emitter.instruction("lsr x10, x11, #2"); // move the by-reference-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the by-reference-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_passed_by_reference_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_passed_by_reference_hi); + emitter.instruction("lsr x10, x11, #3"); // move the typed-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the typed-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", has_type_lo); + abi::emit_store_zero_to_address(emitter, "x9", has_type_hi); + emitter.instruction("lsr x10, x11, #4"); // move the default-value bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); + emitter.instruction("lsr x10, x11, #5"); // move the promoted-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the promoted-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); + emitter.instruction("lsr x10, x11, #6"); // move the nullable-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the nullable-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "x9", allows_null_hi); + emitter.instruction("lsr x10, x11, #7"); // move the default-constant bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-constant flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_default_value_constant_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_default_value_constant_hi); + emitter.instruction("lsr x10, x11, #8"); // move the array-type bit into position + emitter.instruction("and x10, x10, #1"); // extract the array-type flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_array_type_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_array_type_hi); + emitter.instruction("lsr x10, x11, #9"); // move the callable-type bit into position + emitter.instruction("and x10, x10, #1"); // extract the callable-type flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_callable_type_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_callable_type_hi); +} + +/// Stores incoming ARM64 ReflectionParameter type metadata. +fn emit_set_owner_parameter_type_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(type_lo), Some(type_hi)) = (layout.parameter_type_lo, layout.parameter_type_hi) + else { + return; + }; + emitter.instruction("ldr x0, [sp, #120]"); // reload the boxed ReflectionParameter type value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null type metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed type value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed type value for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed type value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "x1", "x9", type_lo); + abi::emit_store_zero_to_address(emitter, "x9", type_hi); +} + +/// Stores incoming ARM64 ReflectionParameter class-type metadata. +fn emit_set_owner_parameter_class_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(class_lo), Some(class_hi)) = + (layout.parameter_class_lo, layout.parameter_class_hi) + else { + return; + }; + emitter.instruction("ldr x0, [sp, #64]"); // reload the boxed ReflectionParameter class value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null class metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed class value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed class value for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed class value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "x1", "x9", class_lo); + abi::emit_store_zero_to_address(emitter, "x9", class_hi); +} + +/// Stores incoming ARM64 ReflectionParameter default-value metadata. +fn emit_set_owner_parameter_default_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(default_lo), Some(default_hi)) = (layout.default_value_lo, layout.default_value_hi) + else { + return; + }; + emitter.instruction("ldr x0, [sp, #128]"); // reload the boxed ReflectionParameter default value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null default metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed default value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed default value for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed default value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "x1", "x9", default_lo); + abi::emit_store_zero_to_address(emitter, "x9", default_hi); +} + +/// Stores incoming ARM64 ReflectionParameter default-constant-name metadata. +fn emit_set_owner_parameter_default_constant_name_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let done_label = "__elephc_eval_reflection_owner_parameter_default_constant_name_done"; + let (Some(name_lo), Some(name_hi)) = ( + layout.default_value_constant_name_lo, + layout.default_value_constant_name_hi, + ) else { + return; + }; + emitter.instruction("ldr x10, [sp, #48]"); // reload ReflectionParameter predicate flags + emitter.instruction("lsr x10, x10, #7"); // move the default-constant bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-constant flag as a boolean + emitter.instruction(&format!("cbz x10, {}", done_label)); // keep the empty name when no constant default exists + emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionParameter default-constant name + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null default-constant metadata + emitter.instruction("bl __rt_mixed_unbox"); // expose the default-constant name string payload + emitter.instruction("cmp x0, #1"); // runtime tag 1 means string + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-string default-constant metadata + emitter.instruction("bl __rt_str_persist"); // copy the default-constant name bytes for parameter storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "x1", "x9", name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", name_hi); + emitter.label(done_label); +} + +/// Stores incoming ARM64 ReflectionNamedType predicate flags. +fn emit_set_owner_named_type_flags_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(allows_null_lo), Some(allows_null_hi)) = + (layout.allows_null_lo, layout.allows_null_hi) + else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionNamedType predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionNamedType object pointer + emitter.instruction("and x10, x11, #1"); // extract the nullable-type flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "x9", allows_null_hi); + let (Some(is_builtin_lo), Some(is_builtin_hi)) = (layout.is_builtin_lo, layout.is_builtin_hi) + else { + return; + }; + emitter.instruction("lsr x10, x11, #1"); // move the builtin-type bit into position + emitter.instruction("and x10, x10, #1"); // extract the builtin-type flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_builtin_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_builtin_hi); +} + +/// Stores incoming x86_64 ReflectionParameter position and predicate flags. +fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &ReflectionOwnerLayout) { + let ( + Some(position_lo), + Some(position_hi), + Some(is_optional_lo), + Some(is_optional_hi), + Some(is_variadic_lo), + Some(is_variadic_hi), + Some(is_passed_by_reference_lo), + Some(is_passed_by_reference_hi), + Some(has_type_lo), + Some(has_type_hi), + Some(has_default_value_lo), + Some(has_default_value_hi), + Some(is_default_value_constant_lo), + Some(is_default_value_constant_hi), + Some(is_promoted_lo), + Some(is_promoted_hi), + Some(allows_null_lo), + Some(allows_null_hi), + Some(is_array_type_lo), + Some(is_array_type_hi), + Some(is_callable_type_lo), + Some(is_callable_type_hi), + ) = ( + layout.position_lo, + layout.position_hi, + layout.is_optional_lo, + layout.is_optional_hi, + layout.is_variadic_lo, + layout.is_variadic_hi, + layout.is_passed_by_reference_lo, + layout.is_passed_by_reference_hi, + layout.has_type_lo, + layout.has_type_hi, + layout.has_default_value_lo, + layout.has_default_value_hi, + layout.is_default_value_constant_lo, + layout.is_default_value_constant_hi, + layout.is_promoted_lo, + layout.is_promoted_hi, + layout.allows_null_lo, + layout.allows_null_hi, + layout.is_array_type_lo, + layout.is_array_type_hi, + layout.is_callable_type_lo, + layout.is_callable_type_hi, + ) + else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload the zero-based parameter position + abi::emit_store_to_address(emitter, "rax", "r10", position_lo); + abi::emit_store_zero_to_address(emitter, "r10", position_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the optional bit + emitter.instruction("and rax, 1"); // extract the optional-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_optional_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_optional_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the variadic bit + emitter.instruction("shr rax, 1"); // move the variadic-parameter bit into position + emitter.instruction("and rax, 1"); // extract the variadic-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_variadic_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_variadic_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the by-reference bit + emitter.instruction("shr rax, 2"); // move the by-reference-parameter bit into position + emitter.instruction("and rax, 1"); // extract the by-reference-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_passed_by_reference_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_passed_by_reference_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the typed bit + emitter.instruction("shr rax, 3"); // move the typed-parameter bit into position + emitter.instruction("and rax, 1"); // extract the typed-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", has_type_lo); + abi::emit_store_zero_to_address(emitter, "r10", has_type_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit + emitter.instruction("shr rax, 4"); // move the default-value bit into position + emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted bit + emitter.instruction("shr rax, 5"); // move the promoted-parameter bit into position + emitter.instruction("and rax, 1"); // extract the promoted-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit + emitter.instruction("shr rax, 6"); // move the nullable-parameter bit into position + emitter.instruction("and rax, 1"); // extract the nullable-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "r10", allows_null_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-constant bit + emitter.instruction("shr rax, 7"); // move the default-constant bit into position + emitter.instruction("and rax, 1"); // extract the default-constant flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_default_value_constant_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_default_value_constant_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the array-type bit + emitter.instruction("shr rax, 8"); // move the array-type bit into position + emitter.instruction("and rax, 1"); // extract the array-type flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_array_type_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_array_type_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the callable-type bit + emitter.instruction("shr rax, 9"); // move the callable-type bit into position + emitter.instruction("and rax, 1"); // extract the callable-type flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_callable_type_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_callable_type_hi); +} + +/// Stores incoming x86_64 ReflectionParameter type metadata. +fn emit_set_owner_parameter_type_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(type_lo), Some(type_hi)) = (layout.parameter_type_lo, layout.parameter_type_hi) + else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 128]"); // reload the boxed ReflectionParameter type value + emitter.instruction("test rax, rax"); // check whether the boxed type value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null type metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed type value across incref + emitter.instruction("call __rt_incref"); // retain the boxed type value for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed type value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", type_lo); + abi::emit_store_zero_to_address(emitter, "r10", type_hi); +} + +/// Stores incoming x86_64 ReflectionParameter class-type metadata. +fn emit_set_owner_parameter_class_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(class_lo), Some(class_hi)) = + (layout.parameter_class_lo, layout.parameter_class_hi) + else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the boxed ReflectionParameter class value + emitter.instruction("test rax, rax"); // check whether the boxed class value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null class metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed class value across incref + emitter.instruction("call __rt_incref"); // retain the boxed class value for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed class value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", class_lo); + abi::emit_store_zero_to_address(emitter, "r10", class_hi); +} + +/// Stores incoming x86_64 ReflectionParameter default-value metadata. +fn emit_set_owner_parameter_default_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(default_lo), Some(default_hi)) = (layout.default_value_lo, layout.default_value_hi) + else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 136]"); // reload the boxed ReflectionParameter default value + emitter.instruction("test rax, rax"); // check whether the boxed default value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null default metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed default value across incref + emitter.instruction("call __rt_incref"); // retain the boxed default value for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed default value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", default_lo); + abi::emit_store_zero_to_address(emitter, "r10", default_hi); +} + +/// Stores incoming x86_64 ReflectionParameter default-constant-name metadata. +fn emit_set_owner_parameter_default_constant_name_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let done_label = "__elephc_eval_reflection_owner_parameter_default_constant_name_done_x"; + let (Some(name_lo), Some(name_hi)) = ( + layout.default_value_constant_name_lo, + layout.default_value_constant_name_hi, + ) else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags + emitter.instruction("shr r11, 7"); // move the default-constant bit into position + emitter.instruction("and r11, 1"); // extract the default-constant flag as a boolean + emitter.instruction(&format!("jz {}", done_label)); // keep the empty name when no constant default exists + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionParameter default-constant name + emitter.instruction("test rax, rax"); // check whether the boxed default-constant name is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null default-constant metadata + emitter.instruction("call __rt_mixed_unbox"); // expose the default-constant name string payload + emitter.instruction("cmp rax, 1"); // runtime tag 1 means string + emitter.instruction(&format!("jne {}", fail_label)); // reject non-string default-constant metadata + emitter.instruction("mov rax, rdi"); // move the string pointer into the x86_64 persist argument + emitter.instruction("call __rt_str_persist"); // copy the default-constant name bytes for parameter storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "rax", "r10", name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); + emitter.label(done_label); +} + +/// Stores incoming x86_64 ReflectionNamedType predicate flags. +fn emit_set_owner_named_type_flags_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(allows_null_lo), Some(allows_null_hi)) = + (layout.allows_null_lo, layout.allows_null_hi) + else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionNamedType predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionNamedType object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit + emitter.instruction("and rax, 1"); // extract the nullable-type flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "r10", allows_null_hi); + let (Some(is_builtin_lo), Some(is_builtin_hi)) = (layout.is_builtin_lo, layout.is_builtin_hi) + else { + return; + }; + emitter.instruction("mov rax, r11"); // copy flags before extracting the builtin bit + emitter.instruction("shr rax, 1"); // move the builtin-type bit into position + emitter.instruction("and rax, 1"); // extract the builtin-type flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_builtin_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_builtin_hi); +} + +/// Stores incoming ARM64 ReflectionClass metadata name arrays. +fn emit_set_owner_metadata_arrays_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + if let (Some(low), Some(high)) = (layout.interface_names_lo, layout.interface_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 80, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.trait_names_lo, layout.trait_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 88, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_names_lo, layout.method_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 104, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_names_lo, layout.property_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 112, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_objects_lo, layout.method_objects_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 120, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_objects_lo, layout.property_objects_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 128, low, high, fail_label); + } +} + +/// Stores one retained ARM64 boxed metadata-name array into a ReflectionClass slot. +fn emit_set_owner_metadata_array_slot_aarch64( + emitter: &mut Emitter, + boxed_slot: usize, + low_offset: usize, + high_offset: usize, + fail_label: &str, +) { + emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low_offset); + abi::emit_load_int_immediate(emitter, "x10", 4); + abi::emit_store_to_address(emitter, "x10", "x9", high_offset); +} + +/// Stores incoming x86_64 ReflectionClass metadata name arrays. +fn emit_set_owner_metadata_arrays_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + if let (Some(low), Some(high)) = (layout.interface_names_lo, layout.interface_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -88, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.trait_names_lo, layout.trait_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -96, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_names_lo, layout.method_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -112, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_names_lo, layout.property_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -120, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_objects_lo, layout.method_objects_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -128, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_objects_lo, layout.property_objects_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -136, low, high, fail_label); + } +} + +/// Stores one retained x86_64 boxed metadata-name array into a ReflectionClass slot. +fn emit_set_owner_metadata_array_slot_x86_64( + emitter: &mut Emitter, + boxed_slot: isize, + low_offset: usize, + high_offset: usize, + fail_label: &str, +) { + let boxed_slot = if boxed_slot < 0 { + format!("- {}", -boxed_slot) + } else { + format!("+ {}", boxed_slot) + }; + emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low_offset); + abi::emit_load_int_immediate(emitter, "r11", 4); + abi::emit_store_to_address(emitter, "r11", "r10", high_offset); +} + +/// Stores a retained ARM64 boxed ReflectionMethod-or-null constructor cell. +fn emit_set_owner_constructor_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.constructor_lo, layout.constructor_hi) else { + return; + }; + emitter.instruction("ldr x0, [sp, #224]"); // reload the boxed ReflectionClass constructor value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constructor metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed constructor value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed constructor value for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constructor value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); +} + +/// Stores a retained x86_64 boxed ReflectionMethod-or-null constructor cell. +fn emit_set_owner_constructor_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.constructor_lo, layout.constructor_hi) else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp + 96]"); // reload the boxed ReflectionClass constructor value + emitter.instruction("test rax, rax"); // check whether the boxed constructor value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constructor metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constructor value across incref + emitter.instruction("call __rt_incref"); // retain the boxed constructor value for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constructor value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); +} + +/// Stores a retained ARM64 boxed parent ReflectionClass-or-false cell. +fn emit_set_owner_parent_class_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.parent_class_lo, layout.parent_class_hi) else { + return; + }; + emitter.instruction("ldr x0, [sp, #136]"); // reload the boxed ReflectionClass parent value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null parent metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed parent value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed parent value for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed parent value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); +} + +/// Stores a retained x86_64 boxed parent ReflectionClass-or-false cell. +fn emit_set_owner_parent_class_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.parent_class_lo, layout.parent_class_hi) else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 144]"); // reload the boxed ReflectionClass parent value + emitter.instruction("test rax, rax"); // check whether the boxed parent value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null parent metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed parent value across incref + emitter.instruction("call __rt_incref"); // retain the boxed parent value for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed parent value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); +} + +/// Stores a retained ARM64 boxed declaring ReflectionFunction/Method cell. +fn emit_set_owner_declaring_function_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.declaring_function_lo, layout.declaring_function_hi) + else { + return; + }; + emitter.instruction("ldr x0, [sp, #80]"); // reload the boxed ReflectionParameter declaring function + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null declaring-function metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed declaring function across incref + emitter.instruction("bl __rt_incref"); // retain the declaring function for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained declaring function value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); +} + +/// Stores a retained x86_64 boxed declaring ReflectionFunction/Method cell. +fn emit_set_owner_declaring_function_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.declaring_function_lo, layout.declaring_function_hi) + else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the boxed ReflectionParameter declaring function + emitter.instruction("test rax, rax"); // check whether the declaring-function value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null declaring-function metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed declaring function across incref + emitter.instruction("call __rt_incref"); // retain the declaring function for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained declaring function value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); +} + +/// Stores a retained ARM64 boxed ReflectionClassConstant value cell. +fn emit_set_owner_constant_value_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.value_lo, layout.value_hi) else { + return; + }; + emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionClassConstant value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constant-value metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed constant value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constant value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionClassConstant object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); +} + +/// Stores a retained ARM64 boxed ReflectionProperty settable-type cell. +fn emit_set_owner_settable_type_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(low), Some(high)) = (layout.settable_type_lo, layout.settable_type_hi) else { + return; + }; + let skip_label = "__elephc_eval_reflection_owner_new_skip_settable_type"; + emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionProperty settable type + emitter.instruction(&format!("cbz x0, {}", skip_label)); // leave null when no settable type is retained + emitter.instruction("str x0, [sp, #40]"); // save the boxed settable type across incref + emitter.instruction("bl __rt_incref"); // retain the boxed settable type for ReflectionProperty storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed settable type + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionProperty object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); + emitter.label(skip_label); +} + +/// Stores a retained x86_64 boxed ReflectionClassConstant value cell. +fn emit_set_owner_constant_value_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.value_lo, layout.value_hi) else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionClassConstant value + emitter.instruction("test rax, rax"); // check whether the boxed constant value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constant-value metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constant value across incref + emitter.instruction("call __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constant value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionClassConstant object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); +} + +/// Stores a retained x86_64 boxed ReflectionProperty settable-type cell. +fn emit_set_owner_settable_type_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(low), Some(high)) = (layout.settable_type_lo, layout.settable_type_hi) else { + return; + }; + let skip_label = "__elephc_eval_reflection_owner_new_skip_settable_type_x"; + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionProperty settable type + emitter.instruction("test rax, rax"); // check whether a settable type was provided + emitter.instruction(&format!("jz {}", skip_label)); // leave null when no settable type is retained + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed settable type across incref + emitter.instruction("call __rt_incref"); // retain the boxed settable type for ReflectionProperty storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed settable type + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionProperty object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); + emitter.label(skip_label); +} + +/// Stores a retained ARM64 boxed ReflectionEnumBackedCase backing-value cell. +fn emit_set_owner_backing_value_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.backing_value_lo, layout.backing_value_hi) else { + return; + }; + emitter.instruction("ldr x0, [sp, #64]"); // reload the boxed ReflectionEnumBackedCase backing value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null backing-value metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed backing value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed backing value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionEnumBackedCase object pointer + abi::emit_store_to_address(emitter, "x1", "x9", low); + abi::emit_store_zero_to_address(emitter, "x9", high); +} + +/// Stores a retained x86_64 boxed ReflectionEnumBackedCase backing-value cell. +fn emit_set_owner_backing_value_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let (Some(low), Some(high)) = (layout.backing_value_lo, layout.backing_value_hi) else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the boxed ReflectionEnumBackedCase backing value + emitter.instruction("test rax, rax"); // check whether the boxed backing value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null backing-value metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed backing value across incref + emitter.instruction("call __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed backing value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionEnumBackedCase object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", low); + abi::emit_store_zero_to_address(emitter, "r10", high); +} + +/// Stores a retained ARM64 attribute-array payload into the owner private slot. +fn emit_set_owner_attrs_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.attrs_lo); + abi::emit_load_int_immediate(emitter, "x10", 4); + abi::emit_store_to_address(emitter, "x10", "x9", layout.attrs_hi); +} + +/// Stores a retained x86_64 attribute-array payload into the owner private slot. +fn emit_set_owner_attrs_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array + emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", layout.attrs_lo); + abi::emit_load_int_immediate(emitter, "r11", 4); + abi::emit_store_to_address(emitter, "r11", "r10", layout.attrs_hi); +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs new file mode 100644 index 0000000000..8073517d82 --- /dev/null +++ b/src/codegen/eval_static_property_helpers.rs @@ -0,0 +1,1226 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician access native static +//! properties through symbol-backed storage. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user static-property symbols, so +//! these C-ABI bridge symbols are emitted into the user assembly. +//! - Supported slots include public static properties plus protected/private +//! static properties when the active eval class scope satisfies PHP visibility. +//! - Null helper returns mean "no bridge match"; boxed PHP null is returned as +//! a real Mixed cell pointer. + +use std::collections::BTreeMap; + +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::static_property_symbol; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +/// Static property slot metadata needed by eval bridge dispatch. +#[derive(Clone)] +struct EvalStaticPropertySlot { + class_name: String, + declaring_class: String, + allowed_scopes: Vec, + property: String, + visibility: Visibility, + symbol: String, + ty: PhpType, + is_declared: bool, +} + +/// Emits eval static-property helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_static_property_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_static_property_slots(module); + emit_static_property_get_helper(module, emitter, data, &slots); + emit_static_property_is_initialized_helper(module, emitter, data, &slots); + emit_static_property_set_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects static properties with storage layouts and visibility rules the bridge can access. +fn collect_eval_static_property_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_static_property_slots(module, class_name, class_info, &mut slots); + } + slots +} + +/// Adds bridge-supported static properties visible from one class. +fn collect_class_static_property_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + slots: &mut Vec, +) { + let mut properties = class_info.static_properties.iter().collect::>(); + properties.sort_by(|(left, _), (right, _)| left.cmp(right)); + for (property, ty) in properties { + let visibility = static_property_visibility(class_info, property); + if !static_property_visibility_supported(visibility) + || !static_property_type_supported(ty) + { + continue; + } + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + let Some(declaring_info) = module.class_infos.get(declaring_class) else { + continue; + }; + slots.push(EvalStaticPropertySlot { + class_name: class_name.to_string(), + declaring_class: declaring_class.to_string(), + allowed_scopes: visibility_scope_names(module, declaring_class, visibility), + property: property.clone(), + visibility: visibility.clone(), + symbol: static_property_symbol(declaring_class, property), + ty: ty.codegen_repr(), + is_declared: declaring_info.declared_static_properties.contains(property), + }); + } +} + +/// Returns the declared static-property visibility, defaulting to public metadata. +fn static_property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Visibility { + class_info + .static_property_visibilities + .get(property) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval static-property bridge can enforce this visibility. +fn static_property_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) +} + +/// Returns true for static-property storage shapes the bridge can box and update. +fn static_property_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Emits `__elephc_eval_value_static_property_get(class, name, scope, scope_len) -> Mixed*`. +fn emit_static_property_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static property get ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_property_get"); + match module.target.arch { + Arch::AArch64 => emit_static_property_get_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_get_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_static_property_is_initialized(class, name, scope, scope_len) -> bool`. +fn emit_static_property_is_initialized_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static-property initialization probe ---"); + label_c_global( + module, + emitter, + "__elephc_eval_value_static_property_is_initialized", + ); + match module.target.arch { + Arch::AArch64 => emit_static_property_is_initialized_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_is_initialized_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_static_property_set(class, name, Mixed*, scope, scope_len) -> bool`. +fn emit_static_property_set_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static property set ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_property_set"); + match module.target.arch { + Arch::AArch64 => emit_static_property_set_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_set_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 static-property get helper body. +fn emit_static_property_get_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_get_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property/scope slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed static property value to Rust +} + +/// Emits the x86_64 static-property get helper body. +fn emit_static_property_get_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_get_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed static property value to Rust +} + +/// Emits the ARM64 static-property-initialization helper body. +fn emit_static_property_is_initialized_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_is_initialized_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property/scope slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "is_initialized"); + emitter.instruction("mov x0, #0"); // report an initialization miss to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_static_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the x86_64 static-property-initialization helper body. +fn emit_static_property_is_initialized_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_is_initialized_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "is_initialized"); + emitter.instruction("xor eax, eax"); // report an initialization miss to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_static_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the ARM64 static-property set helper body. +fn emit_static_property_set_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let fail_label = "__elephc_eval_value_static_property_set_fail"; + let done_label = "__elephc_eval_value_static_property_set_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class/property, value, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the boxed value being assigned + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope pointer + emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("b {}", fail_label)); // no supported static property matched the request + emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report a failed eval static-property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits the x86_64 static-property set helper body. +fn emit_static_property_set_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let fail_label = "__elephc_eval_value_static_property_set_fail_x"; + let done_label = "__elephc_eval_value_static_property_set_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve aligned slots for class, property, value, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the boxed value being assigned + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the active eval class-scope length + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported static property matched the request + emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report a failed eval static-property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits ARM64 class-name and property-name dispatch for static property helpers. +fn emit_aarch64_static_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + mode: &str, +) { + for (class_name, class_slots) in grouped_slots(slots) { + let next_label = format!( + "__elephc_eval_static_property_{}_next_{}", + mode, + label_fragment(class_name) + ); + emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_aarch64_static_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and property-name dispatch for static property helpers. +fn emit_x86_64_static_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + mode: &str, +) { + for (class_name, class_slots) in grouped_slots(slots) { + let next_label = format!( + "__elephc_eval_static_property_{}_next_{}_x", + mode, + label_fragment(class_name) + ); + emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_x86_64_static_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison for a static property group. +fn emit_aarch64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // try the next class when names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison for a static property group. +fn emit_x86_64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // try the next class when names differ +} + +/// Emits one ARM64 property-name comparison and branch to the matching body. +fn emit_aarch64_static_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare property names with PHP case-sensitive rules + let target_label = slot_body_label(module, slot, mode); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the static property body when names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue static-property dispatch when names differ + emit_aarch64_static_property_scope_check(emitter, data, slot, mode, &target_label); + emitter.label(&miss_label); +} + +/// Emits one x86_64 property-name comparison and branch to the matching body. +fn emit_x86_64_static_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the property names matched + let target_label = slot_body_label(module, slot, mode); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the static property body when names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("je {}", miss_label)); // continue static-property dispatch when names differ + emit_x86_64_static_property_scope_check(emitter, data, slot, mode, &target_label); + emitter.label(&miss_label); +} + +/// Emits ARM64 visibility checks for a protected/private static-property bridge hit. +fn emit_aarch64_static_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, + target_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("cbz x1, 1f"); // skip scoped dispatch outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.label("1"); +} + +/// Emits x86_64 visibility checks for a protected/private static-property bridge hit. +fn emit_x86_64_static_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, + target_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction("jz 1f"); // skip scoped dispatch outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.label("1"); +} + +/// Returns ARM64 stack offsets for the class-scope pointer and length. +fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" | "is_initialized" => (32, 40), + "set" => (40, 48), + _ => unreachable!("eval static property helpers only use get/set/is_initialized modes"), + } +} + +/// Returns x86_64 frame offsets for the class-scope pointer and length. +fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" | "is_initialized" => (40, 48), + "set" => (48, 56), + _ => unreachable!("eval static property helpers only use get/set/is_initialized modes"), + } +} + +/// Emits ARM64 get bodies for every bridge-supported static property slot. +fn emit_aarch64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_aarch64_uninitialized_guard(emitter, slot, done_label); + emit_aarch64_box_static_property_slot(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the static property value + } +} + +/// Emits x86_64 get bodies for every bridge-supported static property slot. +fn emit_x86_64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_x86_64_uninitialized_guard(emitter, slot, done_label); + emit_x86_64_box_static_property_slot(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the static property value + } +} + +/// Emits ARM64 static-property-initialization bodies for every supported slot. +fn emit_aarch64_static_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_aarch64_static_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after materializing the static initialization flag + } +} + +/// Emits x86_64 static-property-initialization bodies for every supported slot. +fn emit_x86_64_static_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_x86_64_static_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the static initialization flag + } +} + +/// Emits ARM64 set bodies for every bridge-supported static property slot. +fn emit_aarch64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_aarch64_store_static_property_slot(module, emitter, data, slot, fail_label); + emitter.instruction("mov x0, #1"); // report a successful eval static-property write to Rust + emitter.instruction(&format!("b {}", done_label)); // return after storing the static property value + } +} + +/// Emits x86_64 set bodies for every bridge-supported static property slot. +fn emit_x86_64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_x86_64_store_static_property_slot(module, emitter, data, slot, fail_label); + emitter.instruction("mov rax, 1"); // report a successful eval static-property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // return after storing the static property value + } +} + +/// Emits an ARM64 boolean for one static property's initialized state. +fn emit_aarch64_static_property_initialized_flag( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !slot.is_declared { + emitter.instruction("mov x0, #1"); // non-typed declared static properties are always initialized + return; + } + abi::emit_load_symbol_to_reg(emitter, "x10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "x11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x10, x11"); // compare the static property marker against the uninitialized sentinel + emitter.instruction("cset x0, ne"); // materialize true when the static property is initialized +} + +/// Emits an x86_64 boolean for one static property's initialized state. +fn emit_x86_64_static_property_initialized_flag( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !slot.is_declared { + emitter.instruction("mov rax, 1"); // non-typed declared static properties are always initialized + return; + } + abi::emit_load_symbol_to_reg(emitter, "r10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp r10, r11"); // compare the static property marker against the uninitialized sentinel + emitter.instruction("setne al"); // materialize true when the static property is initialized + emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register +} + +/// Emits an ARM64 uninitialized typed-static-property guard before boxing. +fn emit_aarch64_uninitialized_guard( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "x10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "x11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x10, x11"); // check whether the typed static property is initialized + emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the static slot is initialized + emitter.instruction("mov x0, xzr"); // report uninitialized static property as a bridge failure + emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Emits an x86_64 uninitialized typed-static-property guard before boxing. +fn emit_x86_64_uninitialized_guard( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "r10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp r10, r11"); // check whether the typed static property is initialized + emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the static slot is initialized + emitter.instruction("xor eax, eax"); // report uninitialized static property as a bridge failure + emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Boxes an ARM64 static property symbol payload into a Mixed cell. +fn emit_aarch64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &slot.ty); + } + PhpType::Float => { + abi::emit_load_symbol_to_reg(emitter, "d0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + PhpType::Str => { + abi::emit_load_symbol_to_reg(emitter, "x1", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "x2", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + PhpType::TaggedScalar => { + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "x1", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!( + "{}_mixed_null", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + emitter.instruction(&format!("cbz x0, {}", null_label)); // null static storage reads as PHP null + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Boxes an x86_64 static property symbol payload into a Mixed cell. +fn emit_x86_64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &slot.ty); + } + PhpType::Float => { + abi::emit_load_symbol_to_reg(emitter, "xmm0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + PhpType::Str => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "rdx", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + PhpType::TaggedScalar => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "rdx", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!( + "{}_mixed_null_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + emitter.instruction("test rax, rax"); // check whether static storage holds a Mixed cell + emitter.instruction(&format!("jz {}", null_label)); // null static storage reads as PHP null + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("jmp {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Stores a boxed Mixed eval value into an ARM64 static property symbol. +fn emit_aarch64_store_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + fail_label: &str, +) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), + PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), + PhpType::Float => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float + abi::emit_store_reg_to_symbol(emitter, "d0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + PhpType::Str => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + abi::emit_store_reg_to_symbol(emitter, "x1", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "x2", &slot.symbol, 8); + } + PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Array(_) => { + emit_aarch64_store_heap_static_property_slot(emitter, slot, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_store_heap_static_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_aarch64_store_object_static_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value being assigned + emitter.instruction("bl __rt_incref"); // retain the Mixed cell for static-property ownership + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + _ => {} + } +} + +/// Stores a boxed Mixed eval value into an x86_64 static property symbol. +fn emit_x86_64_store_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + fail_label: &str, +) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), + PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float + abi::emit_store_reg_to_symbol(emitter, "xmm0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "rdx", &slot.symbol, 8); + } + PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Array(_) => { + emit_x86_64_store_heap_static_property_slot(emitter, slot, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_store_heap_static_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_x86_64_store_object_static_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value being assigned + emitter.instruction("call __rt_incref"); // retain the Mixed cell for static-property ownership + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + _ => {} + } +} + +/// Emits an ARM64 scalar static-property store after Mixed coercion. +fn emit_aarch64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("bl {}", helper)); // coerce the eval value to the declared static-property type + abi::emit_store_reg_to_symbol(emitter, result_reg, &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Emits an x86_64 scalar static-property store after Mixed coercion. +fn emit_x86_64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("call {}", helper)); // coerce the eval value to the declared static-property type + abi::emit_store_reg_to_symbol(emitter, result_reg, &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Stores a boxed ARM64 eval heap value into an array-like static property symbol. +fn emit_aarch64_store_heap_static_property_slot( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "x10", expected_tag); + emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the static-property ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Validates and stores a boxed ARM64 eval object into an object static property symbol. +fn emit_aarch64_store_object_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object static-property hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object static-property type hint + } + emit_aarch64_store_heap_static_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed x86_64 eval heap value into an array-like static property symbol. +fn emit_x86_64_store_heap_static_property_slot( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the static-property ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Validates and stores a boxed x86_64 eval object into an object static property symbol. +fn emit_x86_64_store_object_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object static-property hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the static-property hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object static-property type hint + } + emit_x86_64_store_heap_static_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed eval value into an ARM64 nullable-int static property symbol. +fn emit_aarch64_store_tagged_scalar_static_property( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + let null_label = format!( + "{}_tagged_scalar_null", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null static-property writes + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "x1", &slot.symbol, 8); +} + +/// Stores a boxed eval value into an x86_64 nullable-int static property symbol. +fn emit_x86_64_store_tagged_scalar_static_property( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + let null_label = format!( + "{}_tagged_scalar_null_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null static-property writes + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "rdx", &slot.symbol, 8); +} + +/// Clears the typed-property high word after a successful non-pair static store. +fn clear_uninitialized_marker_after_static_store( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !matches!(slot.ty.codegen_repr(), PhpType::Str | PhpType::TaggedScalar) { + abi::emit_store_zero_to_symbol(emitter, &slot.symbol, 8); + } +} + +/// Groups static property slots by PHP-visible class name in deterministic order. +fn grouped_slots(slots: &[EvalStaticPropertySlot]) -> BTreeMap<&str, Vec<&EvalStaticPropertySlot>> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_name.as_str()) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a get/set static property slot. +fn slot_body_label(module: &Module, slot: &EvalStaticPropertySlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("{}{}", slot_body_label_raw(slot, mode), suffix) +} + +/// Returns a platform-safe label for continuing after a scoped static-property name miss. +fn slot_access_miss_label( + module: &Module, + slot: &EvalStaticPropertySlot, + mode: &str, +) -> String { + format!("{}_access_miss", slot_body_label(module, slot, mode)) +} + +/// Returns the architecture-independent body label stem for a static property slot. +fn slot_body_label_raw(slot: &EvalStaticPropertySlot, mode: &str) -> String { + format!( + "__elephc_eval_static_property_{}_{}_{}_{}", + mode, + label_fragment(&slot.class_name), + label_fragment(&slot.declaring_class), + label_fragment(&slot.property) + ) +} + +/// Returns class scopes that satisfy one member visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/fibers.rs b/src/codegen/fibers.rs index 6e5741c398..f6cb7c76d2 100644 --- a/src/codegen/fibers.rs +++ b/src/codegen/fibers.rs @@ -156,6 +156,8 @@ fn fiber_wrapper_label(closure_name: &str) -> String { fn descriptor_invoker_placeholder_sig() -> FunctionSig { FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Mixed, declared_return: false, @@ -185,6 +187,8 @@ fn signature_from_closure(closure: &Function, visible_abi_param_count: usize) -> .take(visible_abi_param_count) .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; visible_abi_param_count], + param_attributes: Vec::new(), defaults: closure .params .iter() @@ -230,6 +234,7 @@ fn ensure_variadic_param_slot(signature: &mut FunctionSig) { signature.defaults.push(None); signature.ref_params.push(false); signature.declared_params.push(false); + signature.param_type_exprs.push(None); } /// Returns hidden argument types for closure captures stored in descriptor capture slots. diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index d7928e7a1d..cb9b2cc2cd 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -162,10 +162,14 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { ctx.emitter.comment("enable heap debug flag"); abi::emit_enable_heap_debug_flag(ctx.emitter); } - store_argc_local_if_present(ctx); - store_argv_local_if_present(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); + zero_initialize_eval_scope_locals(ctx); + store_argc_global_if_needed(ctx); + store_argv_global_if_needed(ctx); + store_argc_local_if_present(ctx); + store_argv_local_if_present(ctx); } /// Emits a callable function prologue using an already-resolved entry label. @@ -205,6 +209,8 @@ pub(super) fn emit_function_prologue_with_label( } zero_initialize_function_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); + zero_initialize_eval_scope_locals(ctx); Ok(()) } @@ -285,6 +291,9 @@ fn emit_main_global_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { continue; } let symbol = ir_global_symbol(&name); + if !ctx.data.has_comm(&symbol) { + continue; + } ctx.emitter.comment(&format!("epilogue cleanup global ${}", name)); emit_static_symbol_value_cleanup(ctx, &symbol, &ty); abi::emit_store_zero_to_symbol(ctx.emitter, &symbol, 0); @@ -339,6 +348,8 @@ pub(super) fn emit_web_handler_prologue(ctx: &mut FunctionContext<'_>) { emit_callee_saved_saves(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); + zero_initialize_eval_scope_locals(ctx); } /// Emits the `--web` top-level handler epilogue and returns to the bridge. @@ -419,6 +430,8 @@ fn emit_main_local_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { _ => {} } } + emit_eval_scope_epilogue_cleanup(ctx); + emit_eval_context_epilogue_cleanup(ctx); } /// Returns main local slots that receive owned refcounted values through `StoreLocal`. @@ -441,7 +454,9 @@ fn main_cleanup_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, P .as_deref() .is_none_or(|name| !param_names.contains(name)) }) - .filter(|local| local_slot_has_store(ctx.function, local.id)) + .filter(|local| { + local_slot_has_store(ctx.function, local.id) || function_has_eval_scope(ctx.function) + }) .filter_map(|local| { let ty = local.php_type.codegen_repr(); if !(matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted()) { @@ -525,6 +540,117 @@ fn ref_cell_owner_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, locals } +/// Zero-initializes persistent eval scope handles before the first eval call can allocate one. +fn zero_initialize_eval_scope_locals(ctx: &mut FunctionContext<'_>) { + for (_, offset) in eval_scope_locals(ctx) { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } +} + +/// Zero-initializes persistent eval context handles before the first eval call can allocate one. +fn zero_initialize_eval_context_locals(ctx: &mut FunctionContext<'_>) { + for (_, offset) in eval_context_locals(ctx) { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } +} + +/// Releases persistent eval scopes allocated for this frame. +fn emit_eval_scope_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + for (name, offset) in eval_scope_locals(ctx) { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_scope_cleanup(ctx, offset); + } +} + +/// Releases persistent eval contexts allocated for this frame. +fn emit_eval_context_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + for (name, offset) in eval_context_locals(ctx) { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_context_cleanup(ctx, offset); + } +} + +/// Frees one persistent eval scope handle when it was allocated. +fn emit_eval_scope_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { + let result_reg = abi::int_result_reg(ctx.emitter); + let done = ctx.next_label("eval_scope_cleanup_done"); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if arg_reg != result_reg { + ctx.emitter + .instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the persistent eval scope handle to the free helper + } + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_free"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + ctx.emitter.label(&done); +} + +/// Frees one persistent eval context handle when it was allocated. +fn emit_eval_context_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { + let result_reg = abi::int_result_reg(ctx.emitter); + let done = ctx.next_label("eval_context_cleanup_done"); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if arg_reg != result_reg { + ctx.emitter.instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the persistent eval context handle to the free helper + } + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_context_free"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + ctx.emitter.label(&done); +} + +/// Returns hidden eval scope slots and their frame offsets. +fn eval_scope_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| matches!(local.kind, LocalKind::EvalScope | LocalKind::EvalGlobalScope)) + .filter_map(|local| { + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, offset)| *offset); + locals +} + +/// Returns hidden eval context slots and their frame offsets. +fn eval_context_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local.kind == LocalKind::EvalContext) + .filter_map(|local| { + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, offset)| *offset); + locals +} + +/// Returns true when the function owns a persistent eval scope local. +fn function_has_eval_scope(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| matches!(local.kind, LocalKind::EvalScope | LocalKind::EvalGlobalScope)) +} + /// Returns true when a local slot is written by an explicit EIR `StoreLocal`. fn local_slot_has_store(function: &Function, slot: LocalSlotId) -> bool { function.instructions.iter().any(|inst| { @@ -645,7 +771,13 @@ fn emit_function_local_epilogue_cleanup( ) { let cleanup_locals = function_cleanup_locals(ctx, skip_return_slot); let ref_cell_owners = ref_cell_owner_locals(ctx); - if cleanup_locals.is_empty() && ref_cell_owners.is_empty() { + let eval_scopes = eval_scope_locals(ctx); + let eval_contexts = eval_context_locals(ctx); + if cleanup_locals.is_empty() + && ref_cell_owners.is_empty() + && eval_scopes.is_empty() + && eval_contexts.is_empty() + { return; } let return_ty = ctx.function.return_php_type.codegen_repr(); @@ -663,6 +795,14 @@ fn emit_function_local_epilogue_cleanup( _ => {} } } + for (name, offset) in eval_scopes { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_scope_cleanup(ctx, offset); + } + for (name, offset) in eval_contexts { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_context_cleanup(ctx, offset); + } if preserves_return { pop_return_value(ctx, &return_ty); } @@ -696,7 +836,9 @@ fn function_cleanup_locals( .is_none_or(|name| !param_names.contains(name)) }) .filter(|local| Some(local.id) != skip_return_slot) - .filter(|local| local_slot_has_store(ctx.function, local.id)) + .filter(|local| { + local_slot_has_store(ctx.function, local.id) || function_has_eval_scope(ctx.function) + }) .filter_map(|local| { let ty = local.php_type.codegen_repr(); if !cleanup_tracked_codegen_type(&ty) { @@ -958,40 +1100,86 @@ fn align_to_16(bytes: usize) -> usize { /// Stores the OS argument count into `$argc` when the EIR main function has that local. fn store_argc_local_if_present(ctx: &mut FunctionContext<'_>) { - let Some(argc_slot) = ctx + let Some((argc_slot, argc_ty)) = ctx .function .locals .iter() .find(|local| local.name.as_deref() == Some("argc")) - .map(|local| local.id) + .map(|local| (local.id, local.php_type.codegen_repr())) else { return; }; let Ok(offset) = ctx.local_offset(argc_slot) else { return; }; - abi::store_at_offset( - ctx.emitter, - abi::process_argc_reg(ctx.emitter.target), - offset, - ); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, "_global_argc", 0); + if matches!(argc_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + } + abi::store_at_offset(ctx.emitter, result_reg, offset); } /// Builds and stores the PHP `$argv` array when the EIR main function has that local. fn store_argv_local_if_present(ctx: &mut FunctionContext<'_>) { - let Some(argv_slot) = ctx + let Some((argv_slot, argv_ty)) = ctx .function .locals .iter() .find(|local| local.name.as_deref() == Some("argv")) - .map(|local| local.id) + .map(|local| (local.id, local.php_type.codegen_repr())) else { return; }; let Ok(offset) = ctx.local_offset(argv_slot) else { return; }; + let array_ty = argv_array_type(); ctx.emitter.comment("build $argv array from OS argv"); abi::emit_call_label(ctx.emitter, "__rt_build_argv"); - abi::emit_store(ctx.emitter, &PhpType::Array(Box::new(PhpType::Str)), offset); + if matches!(argv_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &array_ty); + } + abi::emit_store(ctx.emitter, &argv_ty, offset); +} + +/// Initializes program-global `$argc` storage for eval or static `global $argc`. +fn store_argc_global_if_needed(ctx: &mut FunctionContext<'_>) { + if !superglobal_storage_needed(ctx, "argc") { + return; + } + let symbol = ir_global_symbol("argc"); + ctx.data.add_comm(symbol.clone(), PhpType::Int.stack_size().max(8)); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, "_global_argc", 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); +} + +/// Initializes program-global `$argv` storage for eval or static `global $argv`. +fn store_argv_global_if_needed(ctx: &mut FunctionContext<'_>) { + if !superglobal_storage_needed(ctx, "argv") { + return; + } + let symbol = ir_global_symbol("argv"); + let array_ty = argv_array_type(); + ctx.data.add_comm(symbol.clone(), array_ty.stack_size().max(8)); + ctx.emitter.comment("build global $argv array from OS argv"); + abi::emit_call_label(ctx.emitter, "__rt_build_argv"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &array_ty, false); +} + +/// Returns true when a process superglobal needs program-global storage. +fn superglobal_storage_needed(ctx: &FunctionContext<'_>, name: &str) -> bool { + ctx.module.required_runtime_features.eval_bridge + || ctx + .module + .data + .global_names + .iter() + .any(|candidate| candidate == name) +} + +/// Returns the PHP storage type for `$argv`. +fn argv_array_type() -> PhpType { + PhpType::Array(Box::new(PhpType::Str)) } diff --git a/src/codegen/literal_defaults.rs b/src/codegen/literal_defaults.rs index 1b5d7333cf..cbcfe18de4 100644 --- a/src/codegen/literal_defaults.rs +++ b/src/codegen/literal_defaults.rs @@ -9,8 +9,9 @@ //! Key details: //! - This is intentionally narrower than full PHP expression lowering: only //! scalar, string, null, indexed-array literals with scalar/string/null -//! elements, and associative-array literals (empty, positional, or with -//! constant integer/string keys and scalar/string/null values) land here. +//! elements, empty object-typed indexed arrays, and associative-array literals +//! (empty, positional, or with constant integer/string keys and scalar/string/null +//! values) land here. use crate::codegen::platform::Arch; use crate::codegen::{ @@ -800,6 +801,7 @@ fn array_element_size(elem_type: &PhpType) -> Result { | PhpType::Bool | PhpType::Float | PhpType::Mixed + | PhpType::Object(_) | PhpType::Union(_) | PhpType::Iterable | PhpType::Void => Ok(8), diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index ba362e3f93..9775c3a8f4 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -167,9 +167,14 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::BufferGet => buffers::lower_buffer_get(ctx, &inst), Op::BufferSet => buffers::lower_buffer_set(ctx, &inst), Op::ObjectNew => objects::lower_object_new(ctx, &inst), + Op::ObjectCloneShallow => objects::lower_object_clone_shallow(ctx, &inst), Op::DynamicObjectNew => objects::lower_dynamic_object_new(ctx, &inst), Op::DynamicObjectNewMixed => objects::lower_dynamic_object_new_mixed(ctx, &inst), + Op::DynamicObjectNewWithoutConstructorMixed => { + objects::lower_dynamic_object_new_without_constructor_mixed(ctx, &inst) + } Op::PropGet => objects::lower_prop_get(ctx, &inst), + Op::PropInitialized => objects::lower_prop_initialized(ctx, &inst), Op::LoadPropRefCell => objects::lower_load_prop_ref_cell(ctx, &inst), Op::LoadArrayElemRefCell => arrays::lower_load_array_elem_ref_cell(ctx, &inst), Op::BindRefCellPtr => lower_bind_ref_cell_ptr(ctx, &inst), @@ -185,6 +190,15 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::InitStaticLocal => static_locals::lower_init_static_local(ctx, &inst), Op::LoadStaticProperty => static_properties::lower_load_static_property(ctx, &inst), Op::StoreStaticProperty => static_properties::lower_store_static_property(ctx, &inst), + Op::LoadReflectionStaticProperty => { + static_properties::lower_load_reflection_static_property(ctx, &inst) + } + Op::ReflectionStaticPropertyInitialized => { + static_properties::lower_reflection_static_property_initialized(ctx, &inst) + } + Op::StoreReflectionStaticProperty => { + static_properties::lower_store_reflection_static_property(ctx, &inst) + } Op::Call => lower_direct_call(ctx, &inst), Op::ClosureCall => callables::lower_closure_call(ctx, &inst), Op::ExprCall => callables::lower_expr_call(ctx, &inst), @@ -193,10 +207,21 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::MethodCall => lower_method_call(ctx, &inst), Op::NullsafeMethodCall => lower_nullsafe_method_call(ctx, &inst), Op::StaticMethodCall => lower_static_method_call(ctx, &inst), + Op::EvalStaticMethodCall => lower_eval_static_method_call(ctx, &inst), Op::EnumBackingStringToInt => enums::lower_enum_backing_string_to_int(ctx, &inst), Op::EnumBackingMixedToInt => enums::lower_enum_backing_mixed_to_int(ctx, &inst), Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), + Op::EvalLiteralCall => builtins::lower_eval_literal_call(ctx, &inst), + Op::EvalScopeGet => builtins::lower_eval_scope_get(ctx, &inst), + Op::EvalScopeSet => builtins::lower_eval_scope_set(ctx, &inst), + Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), + Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), + Op::EvalObjectNew => builtins::lower_eval_object_new(ctx, &inst), + Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), + Op::EvalClassExists => builtins::lower_eval_class_exists(ctx, &inst), + Op::EvalConstantExists => builtins::lower_eval_constant_exists(ctx, &inst), + Op::EvalConstantFetch => builtins::lower_eval_constant_fetch(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), Op::FirstClassCallableNew => lower_first_class_callable_new(ctx, &inst), @@ -606,6 +631,8 @@ fn function_signature_from_eir_with_param_count( .take(param_count) .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; param_count], + param_attributes: Vec::new(), defaults: function .params .iter() @@ -645,12 +672,29 @@ fn ensure_variadic_param_slot(signature: &mut FunctionSig) { if signature.params.iter().any(|(name, _)| name == &variadic) { return; } + let variadic_index = signature.params.len(); + let variadic_type_expr = if signature.param_type_exprs.len() > variadic_index { + signature.param_type_exprs.remove(variadic_index) + } else { + None + }; + let variadic_ref = if signature.ref_params.len() > variadic_index { + signature.ref_params.remove(variadic_index) + } else { + false + }; + let variadic_declared = if signature.declared_params.len() > variadic_index { + signature.declared_params.remove(variadic_index) + } else { + false + }; signature .params .push((variadic, PhpType::Array(Box::new(PhpType::Mixed)))); signature.defaults.push(None); - signature.ref_params.push(false); - signature.declared_params.push(false); + signature.ref_params.push(variadic_ref); + signature.declared_params.push(variadic_declared); + signature.param_type_exprs.push(variadic_type_expr); } /// Lowers a concrete include-loaded function variant activation marker. @@ -1765,17 +1809,6 @@ fn first_class_callable_descriptor( method_name, )); } - if let Some(callee) = ctx.callable_function_by_name(target) { - return Ok(Some(FirstClassCallableDescriptor { - entry_label: function_symbol(&callee.name), - kind: callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, - sig: Some(function_signature_from_eir(callee)), - invocation: callable_descriptor::CallableDescriptorInvocation::named( - callable_descriptor::CallableDescriptorShape::Function, - callee.name.clone(), - ), - })); - } if ctx.has_extern_function(target) { return Ok(Some(FirstClassCallableDescriptor { entry_label: ctx.emitter.target.extern_symbol(target), @@ -1790,6 +1823,17 @@ fn first_class_callable_descriptor( if let Some(descriptor) = first_class_builtin_descriptor(ctx, target)? { return Ok(Some(descriptor)); } + if let Some(callee) = ctx.callable_function_by_name(target) { + return Ok(Some(FirstClassCallableDescriptor { + entry_label: function_symbol(&callee.name), + kind: callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + sig: Some(function_signature_from_eir(callee)), + invocation: callable_descriptor::CallableDescriptorInvocation::named( + callable_descriptor::CallableDescriptorShape::Function, + callee.name.clone(), + ), + })); + } Ok(None) } @@ -1905,6 +1949,15 @@ fn lower_runtime_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resu emit_object_tostring_call(ctx, value, normalized)?; return store_if_result(ctx, inst); } + if inst.result_php_type.codegen_repr() == PhpType::Iterable + && matches!( + source_ty, + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) | PhpType::Iterable + ) + { + ctx.load_value_to_result(value)?; + return store_if_result(ctx, inst); + } if inst.result_php_type.codegen_repr() == PhpType::TaggedScalar { match source_ty { PhpType::Int | PhpType::Bool | PhpType::Callable => { @@ -2870,11 +2923,15 @@ fn lower_mixed_method_call( ) -> Result<()> { let candidates = mixed_method_candidates(ctx, method_name, inst.operands.len())?; if candidates.is_empty() { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_method_call(ctx, inst, object, method_name); + } emit_method_call_on_null_fatal(ctx, method_name); return Ok(()); } let receiver_reg = abi::nested_call_reg(ctx.emitter); + let non_object_label = ctx.next_label("mixed_method_non_object"); let no_match_label = ctx.next_label("mixed_method_no_match"); let done_label = ctx.next_label("mixed_method_done"); let match_labels = candidates @@ -2889,7 +2946,7 @@ fn lower_mixed_method_call( ctx.load_value_to_result(object)?; abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); - emit_mixed_method_object_payload_or_fatal(ctx, receiver_reg, &no_match_label); + emit_mixed_method_object_payload_or_fatal(ctx, receiver_reg, &non_object_label); emit_mixed_method_class_dispatch( ctx, receiver_reg, @@ -2905,6 +2962,14 @@ fn lower_mixed_method_call( } ctx.emitter.label(&no_match_label); + if builtins::has_eval_context(ctx) { + builtins::lower_eval_method_call(ctx, inst, object, method_name)?; + abi::emit_jump(ctx.emitter, &done_label); + } else { + emit_method_call_on_null_fatal(ctx, method_name); + } + + ctx.emitter.label(&non_object_label); emit_method_call_on_null_fatal(ctx, method_name); ctx.emitter.label(&done_label); @@ -3712,11 +3777,12 @@ fn lower_throwable_standard_method( store_if_result(ctx, inst) } -/// Loads `Throwable::getMessage()` from payload offsets 8/16 into string result registers. +/// Loads `Throwable::getMessage()` from payload offsets 8/16 and returns a caller-owned string copy. fn lower_throwable_get_message(ctx: &mut FunctionContext<'_>, object_reg: &str) -> Result { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_load_from_address(ctx.emitter, ptr_reg, object_reg, 8); abi::emit_load_from_address(ctx.emitter, len_reg, object_reg, 16); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); Ok(PhpType::Str) } @@ -4517,13 +4583,20 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - ); } let late_bound_static = is_late_bound_static_receiver(receiver_label); - let receiver_info = ctx - .module - .class_infos - .get(receiver.as_str()) - .ok_or_else(|| { - CodegenIrError::unsupported(format!("static method call on unknown class {}", receiver)) - })?; + let Some(receiver_info) = ctx.module.class_infos.get(receiver.as_str()) else { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_static_method_call( + ctx, + inst, + receiver.as_str(), + method_name, + ); + } + return Err(CodegenIrError::unsupported(format!( + "static method call on unknown class {}", + receiver + ))); + }; let method_key = php_symbol_key(method_name); let impl_class = receiver_info .static_method_impl_classes @@ -4570,6 +4643,22 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - } else { None }; + let eval_done_label = if late_bound_static && ctx.module.required_runtime_features.eval_bridge { + let no_override_label = ctx.next_label("eval_late_static_no_override"); + let done_label = ctx.next_label("eval_late_static_done"); + builtins::lower_eval_native_frame_static_method_call( + ctx, + inst, + receiver.as_str(), + method_name, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Some(done_label) + } else { + None + }; let call_args = materialize_static_method_call_args_with_refs( ctx, &called_class_id, @@ -4596,7 +4685,19 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - } ctx.store_result_value(result)?; } - emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks) + emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + Ok(()) +} + +/// Lowers a direct static-method call against a class declared by a previous eval fragment. +fn lower_eval_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let target = method_name_data(ctx, inst)?.to_string(); + let (receiver_label, method_name) = parse_static_method_target(&target)?; + let receiver = resolve_static_method_receiver(ctx, receiver_label)?; + builtins::lower_eval_static_method_call(ctx, inst, receiver.as_str(), method_name) } /// Lowers `self::method()` or `parent::method()` when it targets an instance method. @@ -5460,6 +5561,17 @@ fn emit_loaded_indexed_array_to_mixed(ctx: &mut FunctionContext<'_>, source_elem abi::emit_call_label(ctx.emitter, "__rt_array_to_mixed"); } +/// Converts the currently loaded associative-array argument into boxed Mixed values. +fn emit_loaded_assoc_array_to_mixed(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => {} + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the loaded associative-array argument to the Mixed conversion helper + } + } + abi::emit_call_label(ctx.emitter, "__rt_hash_to_mixed"); +} + /// Loads method call arguments for lexical `self::`/`parent::` instance calls using local `this`. fn materialize_method_call_args_with_receiver_local_and_refs( ctx: &mut FunctionContext<'_>, @@ -6016,10 +6128,7 @@ pub(super) fn direct_call_stack_pad_bytes( ctx: &FunctionContext<'_>, overflow_bytes: usize, ) -> usize { - match ctx.emitter.target.arch { - Arch::AArch64 if overflow_bytes > 0 => 16, - _ => 0, - } + abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, overflow_bytes) } /// Lowers a signed integer comparison into a boolean result value. @@ -6579,9 +6688,9 @@ fn lower_load_global(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resul let data = expect_global_name(inst)?; let name = ctx.global_name_data(data)?; let symbol = ir_global_symbol(name); - let result = inst.result.ok_or_else(|| { - CodegenIrError::invalid_module("load_global missing result value") - })?; + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("load_global missing result value"))?; let ty = ctx.value_php_type(result)?; ctx.data .add_comm(symbol.clone(), ty.codegen_repr().stack_size().max(8)); @@ -6789,8 +6898,12 @@ fn lower_invoker_ref_arg(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R /// Lowers PHP echo output for a previously computed SSA value. fn lower_echo_value(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let value = expect_operand(inst, 0)?; - if let PhpType::Object(class_name) = ctx.value_php_type(value)?.codegen_repr() { - return lower_object_echo_value(ctx, value, &class_name); + match ctx.value_php_type(value)?.codegen_repr() { + PhpType::Object(class_name) => return lower_object_echo_value(ctx, value, &class_name), + PhpType::Mixed | PhpType::Union(_) => { + return conversions::emit_mixed_string_context_stdout(ctx, value); + } + _ => {} } let ty = ctx.load_value_to_result(value)?; let raw_ty = ctx.raw_value_php_type(value)?; diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs index 4afaee788d..d649f0c1e1 100644 --- a/src/codegen/lower_inst/arrays.rs +++ b/src/codegen/lower_inst/arrays.rs @@ -11,8 +11,10 @@ use crate::codegen::{ abi, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed, - emit_release_pushed_refcounted_temp_after_array_push, runtime_value_tag, + emit_box_runtime_payload_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, + runtime_value_tag, }; +use crate::codegen::callable_invoker_args::INVOKER_ARG_REF_CELL_TAG; use crate::codegen::platform::Arch; use crate::codegen::sentinels::TAGGED_SCALAR_ARRAY_VALUE_TYPE; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; @@ -801,7 +803,7 @@ fn emit_array_get_in_bounds_aarch64( PhpType::Mixed => { ctx.emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip the indexed-array header to reach Mixed cell payloads ctx.emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", index_reg, array_reg, index_reg)); // load the selected boxed Mixed cell - abi::emit_incref_if_refcounted(ctx.emitter, elem_ty); + emit_mixed_array_get_deref_invoker_ref_cell(ctx, index_reg); } other if other.is_refcounted() => { ctx.emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip the indexed-array header to reach pointer payloads @@ -863,7 +865,7 @@ fn emit_array_get_in_bounds_x86_64( PhpType::Mixed => { ctx.emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip the indexed-array header to reach Mixed cell payloads ctx.emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", index_reg, array_reg, index_reg)); // load the selected boxed Mixed cell - abi::emit_incref_if_refcounted(ctx.emitter, elem_ty); + emit_mixed_array_get_deref_invoker_ref_cell(ctx, index_reg); } other if other.is_refcounted() => { ctx.emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip the indexed-array header to reach pointer payloads @@ -880,6 +882,105 @@ fn emit_array_get_in_bounds_x86_64( Ok(()) } +/// Dereferences descriptor-style ref-cell markers loaded from Mixed array slots. +fn emit_mixed_array_get_deref_invoker_ref_cell( + ctx: &mut FunctionContext<'_>, + mixed_reg: &str, +) { + let ref_label = ctx.next_label("array_get_mixed_ref_cell"); + let done_label = ctx.next_label("array_get_mixed_done"); + let tag_reg = abi::secondary_scratch_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cbz {}, {}", mixed_reg, done_label)); // null gap cells read as PHP null and carry no tag word to inspect + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", mixed_reg, mixed_reg)); // null gap cells read as PHP null and carry no tag word to inspect + ctx.emitter.instruction(&format!("jz {}", done_label)); // skip marker detection for null gap cells + } + } + abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 0); + emit_branch_if_invoker_ref_cell_tag(ctx, tag_reg, &ref_label); + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&ref_label); + emit_box_loaded_invoker_ref_cell_value_as_mixed(ctx, mixed_reg); + ctx.emitter.label(&done_label); +} + +/// Boxes the current value referenced by a loaded invoker ref-cell marker. +fn emit_box_loaded_invoker_ref_cell_value_as_mixed( + ctx: &mut FunctionContext<'_>, + mixed_reg: &str, +) { + let ref_cell_reg = abi::symbol_scratch_reg(ctx.emitter); + let tag_reg = abi::secondary_scratch_reg(ctx.emitter); + let lo_reg = abi::tertiary_scratch_reg(ctx.emitter); + let hi_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x12", + Arch::X86_64 => "rdx", + }; + let string_hi_label = ctx.next_label("array_get_mixed_ref_string_hi"); + let mixed_cell_label = ctx.next_label("array_get_mixed_ref_cell"); + let box_label = ctx.next_label("array_get_mixed_ref_box"); + let done_label = ctx.next_label("array_get_mixed_ref_done"); + + abi::emit_load_from_address(ctx.emitter, ref_cell_reg, mixed_reg, 8); + abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 16); + abi::emit_load_from_address(ctx.emitter, lo_reg, ref_cell_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, hi_reg, 0); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #{}", tag_reg, runtime_value_tag(&PhpType::Mixed))); // check whether the ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("b.eq {}", mixed_cell_label)); // retain and forward boxed Mixed values without reboxing their pointer + ctx.emitter.instruction(&format!("cmp {}, #1", tag_reg)); // check whether the referenced value is a string slot + ctx.emitter.instruction(&format!("b.eq {}", string_hi_label)); // load string length only for string ref-cells + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", tag_reg, runtime_value_tag(&PhpType::Mixed))); // check whether the ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("je {}", mixed_cell_label)); // retain and forward boxed Mixed values without reboxing their pointer + ctx.emitter.instruction(&format!("cmp {}, 1", tag_reg)); // check whether the referenced value is a string slot + ctx.emitter.instruction(&format!("je {}", string_hi_label)); // load string length only for string ref-cells + } + } + abi::emit_jump(ctx.emitter, &box_label); + + ctx.emitter.label(&string_hi_label); + abi::emit_load_from_address(ctx.emitter, hi_reg, ref_cell_reg, 8); + + ctx.emitter.label(&box_label); + emit_box_runtime_payload_as_mixed(ctx.emitter, tag_reg, lo_reg, hi_reg); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&mixed_cell_label); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, result_reg, lo_reg); + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); + + ctx.emitter.label(&done_label); +} + +/// Branches when a loaded Mixed tag is an invoker ref-cell marker. +fn emit_branch_if_invoker_ref_cell_tag( + ctx: &mut FunctionContext<'_>, + tag_reg: &str, + label: &str, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #{}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for a by-reference variadic marker + ctx.emitter.instruction(&format!("b.eq {}", label)); // dereference marker slots instead of returning the marker + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for a by-reference variadic marker + ctx.emitter.instruction(&format!("je {}", label)); // dereference marker slots instead of returning the marker + } + } +} + /// Emits PHP's undefined integer array-key warning for the key in the result register. fn emit_undefined_array_key_warning(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_warn_undefined_array_key_int"); @@ -1387,16 +1488,22 @@ fn lower_mixed_array_set_aarch64( value: ValueId, value_ty: &PhpType, ) -> Result<()> { - if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - ctx.load_value_to_result(value)?; - abi::emit_incref_if_refcounted(ctx.emitter, value_ty); + let value_ty = value_ty.codegen_repr(); + let fresh_boxed_value = !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)); + if fresh_boxed_value { + box_value_for_mixed_container(ctx, value, &value_ty)?; } else { - box_value_for_mixed_container(ctx, value, value_ty)?; + ctx.load_value_to_result(value)?; + abi::emit_incref_if_refcounted(ctx.emitter, &value_ty); } abi::emit_push_reg(ctx.emitter, "x0"); ctx.load_value_to_reg(array, "x0")?; ctx.load_value_to_reg(index, "x1")?; abi::emit_pop_reg(ctx.emitter, "x2"); + if fresh_boxed_value { + emit_mixed_array_set_ref_marker_writeback_aarch64(ctx); + return Ok(()); + } abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); Ok(()) } @@ -1409,20 +1516,107 @@ fn lower_mixed_array_set_x86_64( value: ValueId, value_ty: &PhpType, ) -> Result<()> { - if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - ctx.load_value_to_result(value)?; - abi::emit_incref_if_refcounted(ctx.emitter, value_ty); + let value_ty = value_ty.codegen_repr(); + let fresh_boxed_value = !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)); + if fresh_boxed_value { + box_value_for_mixed_container(ctx, value, &value_ty)?; } else { - box_value_for_mixed_container(ctx, value, value_ty)?; + ctx.load_value_to_result(value)?; + abi::emit_incref_if_refcounted(ctx.emitter, &value_ty); } abi::emit_push_reg(ctx.emitter, "rax"); ctx.load_value_to_reg(array, "rdi")?; ctx.load_value_to_reg(index, "rsi")?; abi::emit_pop_reg(ctx.emitter, "rdx"); + if fresh_boxed_value { + emit_mixed_array_set_ref_marker_writeback_x86_64(ctx); + return Ok(()); + } abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); Ok(()) } +/// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on AArch64. +fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<'_>) { + let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let mixed_cell_label = ctx.next_label("mixed_array_set_ref_mixed_cell"); + let done_label = ctx.next_label("mixed_array_set_done"); + + ctx.emitter.instruction("cmp x1, #0"); // reject negative indexes before checking for by-reference markers + ctx.emitter.instruction(&format!("b.lt {}", runtime_label)); // let the runtime setter drop ignored negative-index writes + ctx.emitter.instruction("ldr x9, [x0]"); // load the current logical length of the indexed array + ctx.emitter.instruction("cmp x1, x9"); // only existing slots can hold by-reference marker cells + ctx.emitter.instruction(&format!("b.hs {}", runtime_label)); // delegate appends and gap writes to the runtime setter + ctx.emitter.instruction("add x10, x0, #24"); // compute the boxed-Mixed payload base for indexed slots + ctx.emitter.instruction("ldr x11, [x10, x1, lsl #3]"); // load the existing boxed Mixed slot + ctx.emitter.instruction(&format!("cbz x11, {}", runtime_label)); // null gap slots are ordinary array writes + ctx.emitter.instruction("ldr x12, [x11]"); // load the existing Mixed tag for marker detection + ctx.emitter.instruction(&format!("cmp x12, #{}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage + ctx.emitter.instruction(&format!("b.ne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("ldr x12, [x11, #16]"); // load the source runtime tag carried by the by-reference marker + ctx.emitter.instruction("ldr x10, [x11, #8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction(&format!("cmp x12, #{}", runtime_value_tag(&PhpType::Mixed))); // check whether the caller ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("b.eq {}", mixed_cell_label)); // transfer boxed Mixed replacements as handles rather than payload words + ctx.emitter.instruction("ldr x12, [x2, #8]"); // load the replacement Mixed low payload word + ctx.emitter.instruction("str x12, [x10]"); // write the replacement low word through the caller ref-cell + ctx.emitter.instruction("ldr x12, [x2, #16]"); // load the replacement Mixed high payload word + ctx.emitter.instruction("str x12, [x10, #8]"); // write the replacement high word through the caller ref-cell + ctx.emitter.instruction("str x0, [sp, #-16]!"); // preserve the array result while freeing only the Mixed wrapper + ctx.emitter.instruction("mov x0, x2"); // pass the consumed fresh Mixed wrapper to heap_free + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the array pointer as the ArraySet result + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the runtime setter after marker write-through + + ctx.emitter.label(&mixed_cell_label); + ctx.emitter.instruction("str x2, [x10]"); // transfer the fresh boxed Mixed handle into the caller ref-cell + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the runtime setter after handle transfer + + ctx.emitter.label(&runtime_label); + abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); + ctx.emitter.label(&done_label); +} + +/// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on x86_64. +fn emit_mixed_array_set_ref_marker_writeback_x86_64(ctx: &mut FunctionContext<'_>) { + let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let mixed_cell_label = ctx.next_label("mixed_array_set_ref_mixed_cell"); + let done_label = ctx.next_label("mixed_array_set_done"); + + ctx.emitter.instruction("cmp rsi, 0"); // reject negative indexes before checking for by-reference markers + ctx.emitter.instruction(&format!("jl {}", runtime_label)); // let the runtime setter drop ignored negative-index writes + ctx.emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the current logical length of the indexed array + ctx.emitter.instruction("cmp rsi, r9"); // only existing slots can hold by-reference marker cells + ctx.emitter.instruction(&format!("jae {}", runtime_label)); // delegate appends and gap writes to the runtime setter + ctx.emitter.instruction("mov r10, QWORD PTR [rdi + 24 + rsi * 8]"); // load the existing boxed Mixed slot + ctx.emitter.instruction("test r10, r10"); // check whether the existing slot is a null gap + ctx.emitter.instruction(&format!("jz {}", runtime_label)); // null gap slots are ordinary array writes + ctx.emitter.instruction("mov r11, QWORD PTR [r10]"); // load the existing Mixed tag for marker detection + ctx.emitter.instruction(&format!("cmp r11, {}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage + ctx.emitter.instruction(&format!("jne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("mov r11, QWORD PTR [r10 + 16]"); // load the source runtime tag carried by the by-reference marker + ctx.emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction(&format!("cmp r11, {}", runtime_value_tag(&PhpType::Mixed))); // check whether the caller ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("je {}", mixed_cell_label)); // transfer boxed Mixed replacements as handles rather than payload words + ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 8]"); // load the replacement Mixed low payload word + ctx.emitter.instruction("mov QWORD PTR [r10], r11"); // write the replacement low word through the caller ref-cell + ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 16]"); // load the replacement Mixed high payload word + ctx.emitter.instruction("mov QWORD PTR [r10 + 8], r11"); // write the replacement high word through the caller ref-cell + abi::emit_push_reg(ctx.emitter, "rdi"); + ctx.emitter.instruction("mov rax, rdx"); // pass the consumed fresh Mixed wrapper to heap_free + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + abi::emit_pop_reg(ctx.emitter, "rax"); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the runtime setter after marker write-through + + ctx.emitter.label(&mixed_cell_label); + ctx.emitter.instruction("mov QWORD PTR [r10], rdx"); // transfer the fresh boxed Mixed handle into the caller ref-cell + ctx.emitter.instruction("mov rax, rdi"); // return the unchanged indexed array after marker handle transfer + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the runtime setter after handle transfer + + ctx.emitter.label(&runtime_label); + abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); + ctx.emitter.label(&done_label); +} + /// Boxes a value for a Mixed array, consuming owned producers when possible. fn box_value_for_mixed_container( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 08c35e0f99..1a04552d64 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -9,16 +9,21 @@ //! - Runtime conversions reuse existing target-aware helpers instead of duplicating parsing logic. //! - Selected Mixed predicates inspect the boxed runtime tag through shared predicate lowering. +use std::collections::BTreeSet; + use crate::codegen::abi; use crate::codegen::platform::Arch; use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; use crate::names::{define_seen_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::checker::builtins::is_php_visible_builtin_function; -use crate::types::PhpType; +use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; -use super::{expect_data, expect_operand, load_value_to_first_int_arg, predicates, store_if_result}; +use super::{ + conversions, expect_data, expect_operand, load_value_to_first_int_arg, predicates, + store_if_result, +}; use crate::codegen::{CodegenIrError, Result}; pub(crate) mod attributes; @@ -27,6 +32,7 @@ mod buffers; pub(crate) mod class_relations; pub(crate) mod ctype; pub(crate) mod debug; +mod eval; pub(crate) mod io; mod isset; pub(crate) mod is_numeric; @@ -57,9 +63,17 @@ pub(super) fn lower_builtin_call(ctx: &mut FunctionContext<'_>, inst: &Instructi } match key.as_str() { "closure_bind" => lower_closure_bind(ctx, inst), + "eval" => eval::lower_eval(ctx, inst), "buffer_len" => buffers::lower_buffer_len(ctx, inst), "buffer_free" => buffers::lower_buffer_free(ctx, inst), - + "strval" => lower_strval(ctx, inst), + "method_exists" | "property_exists" => lower_member_exists(ctx, inst, key.as_str()), + "is_integer" | "is_long" => { + lower_static_type_predicate(ctx, inst, key.as_str(), PhpType::Int) + } + "is_double" | "is_real" => { + lower_static_type_predicate(ctx, inst, key.as_str(), PhpType::Float) + } "empty" => lower_empty(ctx, inst), "unset" => types::lower_unset_builtin(ctx, inst), "isset" => isset::lower_isset(ctx, inst), @@ -78,6 +92,282 @@ pub(super) fn lower_hash_isset(ctx: &mut FunctionContext<'_>, inst: &Instruction isset::lower_hash_isset(ctx, inst) } +/// Lowers a statically-known eval fragment through the current bridge fallback path. +pub(super) fn lower_eval_literal_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval(ctx, inst) +} + +/// Lowers a direct EIR eval-scope lookup by static variable name. +pub(super) fn lower_eval_scope_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_scope_get(ctx, inst) +} + +/// Lowers a direct EIR eval-scope write by static variable name. +pub(super) fn lower_eval_scope_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_scope_set(ctx, inst) +} + +/// Lowers a native call to a zero-argument function declared through `eval()`. +pub(super) fn lower_eval_function_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_call(ctx, inst) +} + +/// Lowers a post-eval function call whose arguments are packed in an array/hash container. +pub(super) fn lower_eval_function_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_call_array(ctx, inst) +} + +/// Lowers post-eval object construction for classes declared by eval fragments. +pub(super) fn lower_eval_object_new( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_object_new(ctx, inst) +} + +/// Lowers fallback construction of a runtime class name through eval dynamic metadata. +pub(super) fn lower_eval_object_new_dynamic_fallback( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + miss_label: &str, +) -> Result<()> { + eval::lower_eval_object_new_dynamic_fallback(ctx, inst, miss_label) +} + +/// Lowers a post-eval method call that may target an eval-created object. +pub(super) fn lower_eval_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + method_name: &str, +) -> Result<()> { + eval::lower_eval_method_call(ctx, inst, object, method_name) +} + +/// Lowers a post-eval static method call to an eval-declared class. +pub(super) fn lower_eval_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + method_name: &str, +) -> Result<()> { + eval::lower_eval_static_method_call(ctx, inst, class_name, method_name) +} + +/// Lowers a late-static AOT-frame static method call through an active eval override. +pub(super) fn lower_eval_native_frame_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + method_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_method_call( + ctx, + inst, + frame_class, + method_name, + no_override_label, + done_label, + ) +} + +/// Lowers a late-static AOT-frame static-property read through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_property_get( + ctx, + inst, + frame_class, + property_name, + no_override_label, + done_label, + ) +} + +/// Lowers a late-static AOT-frame static-property write through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_property_set( + ctx, + inst, + value, + frame_class, + property_name, + no_override_label, + done_label, + ) +} + +/// Lowers post-eval callable-array dispatch against eval dynamic callables. +pub(super) fn lower_eval_callable_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, + arg_array: ValueId, +) -> Result<()> { + eval::lower_eval_callable_call_array(ctx, inst, callback, arg_array) +} + +/// Lowers post-eval callable probes against eval dynamic callables. +pub(super) fn lower_eval_is_callable( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, +) -> Result<()> { + eval::lower_eval_is_callable(ctx, inst, callback) +} + +/// Lowers post-eval member-existence probes against eval dynamic metadata. +pub(super) fn lower_eval_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + member: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_member_exists(ctx, inst, target, member, name) +} + +/// Lowers post-eval class-relation probes against eval dynamic metadata. +pub(super) fn lower_eval_class_relation( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_class_relation(ctx, inst, target, name) +} + +/// Lowers post-eval object class-name introspection against eval dynamic metadata. +pub(super) fn lower_eval_object_class_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_object_class_name(ctx, inst, object, name) +} + +/// Lowers post-eval object/class relation predicates against eval dynamic metadata. +pub(super) fn lower_eval_object_is_a( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target_class: &str, + exclude_self: bool, +) -> Result<()> { + eval::lower_eval_object_is_a(ctx, inst, object, target_class, exclude_self) +} + +/// Lowers post-eval object/class relation predicates with runtime target cells. +pub(super) fn lower_eval_object_is_a_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target: ValueId, + exclude_self: bool, +) -> Result<()> { + eval::lower_eval_object_is_a_dynamic(ctx, inst, object, target, exclude_self) +} + +/// Returns true when this lowered function has a persistent eval context local. +pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { + eval::has_eval_context(ctx) +} + +/// Lowers post-eval dynamic function existence probes to the optional eval bridge. +pub(super) fn lower_eval_function_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_exists(ctx, inst) +} + +/// Lowers post-eval dynamic class existence probes to the optional eval bridge. +pub(super) fn lower_eval_class_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_class_exists(ctx, inst) +} + +/// Lowers post-eval dynamic constant existence probes to the optional eval bridge. +pub(super) fn lower_eval_constant_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_constant_exists(ctx, inst) +} + +/// Lowers post-eval dynamic constant fetches to the optional eval bridge. +pub(super) fn lower_eval_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_constant_fetch(ctx, inst) +} + +/// Lowers post-eval class-like constant fetches to the optional eval bridge. +pub(super) fn lower_eval_class_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + constant_name: &str, +) -> Result<()> { + eval::lower_eval_class_constant_fetch(ctx, inst, class_name, constant_name) +} + +/// Lowers post-eval static-property reads to the optional eval bridge. +pub(super) fn lower_eval_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + property_name: &str, +) -> Result<()> { + eval::lower_eval_static_property_get(ctx, inst, class_name, property_name) +} + +/// Lowers post-eval static-property writes to the optional eval bridge. +pub(super) fn lower_eval_static_property_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + class_name: &str, + property_name: &str, +) -> Result<()> { + eval::lower_eval_static_property_set(ctx, inst, value, class_name, property_name) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; @@ -201,12 +491,12 @@ fn emit_mixed_gettype(ctx: &mut FunctionContext<'_>, value: ValueId) -> Result<( fn emit_branch_on_gettype_mixed_tag(ctx: &mut FunctionContext<'_>, tag: u8, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp x0, #{}", tag)); // compare the unboxed Mixed tag against this gettype() case - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching gettype() type-name case + ctx.emitter.instruction(&format!("cmp x0, #{}", tag)); // compare the unboxed Mixed tag against this gettype() case + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching gettype() type-name case } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp rax, {}", tag)); // compare the unboxed Mixed tag against this gettype() case - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching gettype() type-name case + ctx.emitter.instruction(&format!("cmp rax, {}", tag)); // compare the unboxed Mixed tag against this gettype() case + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching gettype() type-name case } } } @@ -289,7 +579,7 @@ pub(crate) fn lower_function_exists(ctx: &mut FunctionContext<'_>, inst: &Instru store_if_result(ctx, inst) } -/// Lowers AOT class/interface/enum existence checks for literal names. +/// Lowers AOT class/interface/enum existence checks for literal or dynamic string names. pub(crate) fn lower_class_like_exists( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -297,29 +587,136 @@ pub(crate) fn lower_class_like_exists( ) -> Result<()> { ensure_arg_count_between(inst, name, 1, 2)?; let value = expect_operand(inst, 0)?; - let symbol_name = const_string_operand(ctx, value)?; - let exists = match name { - "class_exists" => contains_folded( - ctx.module - .class_infos - .keys() - .filter(|class_name| !is_internal_synthetic_class_name(class_name)), - &symbol_name, - ), - "interface_exists" => contains_folded(ctx.module.interface_infos.keys(), &symbol_name), - "trait_exists" => contains_folded(ctx.module.trait_table.names.iter(), &symbol_name), - "enum_exists" => contains_folded(ctx.module.enum_infos.keys(), &symbol_name), - _ => false, - }; - emit_static_bool(ctx, exists); + if let Some(symbol_name) = maybe_const_string_operand(ctx, value)? { + let exists = match name { + "class_exists" => contains_folded( + ctx.module + .class_infos + .keys() + .filter(|class_name| !is_internal_synthetic_class_name(class_name)), + &symbol_name, + ), + "interface_exists" => contains_folded(ctx.module.interface_infos.keys(), &symbol_name), + "trait_exists" => contains_folded(ctx.module.trait_table.names.iter(), &symbol_name), + "enum_exists" => contains_folded(ctx.module.enum_infos.keys(), &symbol_name), + _ => false, + }; + emit_static_bool(ctx, exists); + } else { + lower_dynamic_class_like_exists(ctx, name, value)?; + } store_if_result(ctx, inst) } +/// Lowers a dynamic string `class_exists()`-family lookup against known AOT metadata. +fn lower_dynamic_class_like_exists( + ctx: &mut FunctionContext<'_>, + name: &str, + value: ValueId, +) -> Result<()> { + if ctx.value_php_type(value)?.codegen_repr() != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "{} with non-string dynamic name", + name + ))); + } + let candidates = dynamic_class_like_exists_candidates(ctx, name); + if candidates.is_empty() { + emit_static_bool(ctx, false); + return Ok(()); + } + + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + ctx.load_string_value_to_regs(value, ptr_reg, len_reg)?; + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + + let matched_label = ctx.next_label(&format!("{}_dynamic_match", name)); + let done_label = ctx.next_label(&format!("{}_dynamic_done", name)); + for candidate in candidates { + emit_branch_if_dynamic_class_like_exists_candidate(ctx, &candidate, &matched_label); + } + emit_static_bool(ctx, false); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&matched_label); + emit_static_bool(ctx, true); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, 16); + Ok(()) +} + +/// Collects deterministic class-like name candidates for a dynamic existence lookup. +fn dynamic_class_like_exists_candidates(ctx: &FunctionContext<'_>, name: &str) -> Vec { + let mut candidates = BTreeSet::new(); + match name { + "class_exists" => { + candidates.extend( + ctx.module + .class_infos + .keys() + .filter(|class_name| !is_internal_synthetic_class_name(class_name)) + .cloned(), + ); + } + "interface_exists" => candidates.extend(ctx.module.interface_infos.keys().cloned()), + "trait_exists" => candidates.extend(ctx.module.trait_table.names.iter().cloned()), + "enum_exists" => candidates.extend(ctx.module.enum_infos.keys().cloned()), + _ => {} + } + candidates.into_iter().collect() +} + +/// Branches when the saved dynamic class-like string matches a metadata candidate. +fn emit_branch_if_dynamic_class_like_exists_candidate( + ctx: &mut FunctionContext<'_>, + candidate: &str, + matched_label: &str, +) { + let bare_candidate = candidate.trim_start_matches('\\'); + emit_dynamic_class_like_exists_compare(ctx, bare_candidate.as_bytes(), matched_label); + let qualified_candidate = format!("\\{}", bare_candidate); + emit_dynamic_class_like_exists_compare(ctx, qualified_candidate.as_bytes(), matched_label); +} + +/// Emits one case-insensitive comparison for the saved dynamic class-like lookup. +fn emit_dynamic_class_like_exists_compare( + ctx: &mut FunctionContext<'_>, + candidate: &[u8], + matched_label: &str, +) { + let (candidate_label, candidate_len) = ctx.data.add_string(candidate); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); + abi::emit_symbol_address(ctx.emitter, "x3", &candidate_label); + abi::emit_load_int_immediate(ctx.emitter, "x4", candidate_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // report existence when the runtime name matches case-insensitively + } + Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 8); + abi::emit_symbol_address(ctx.emitter, "rdx", &candidate_label); + abi::emit_load_int_immediate(ctx.emitter, "rcx", candidate_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); + ctx.emitter.instruction("test rax, rax"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("je {}", matched_label)); // report existence when the runtime name matches case-insensitively + } + } +} + /// Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers. pub(crate) fn lower_is_callable(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "is_callable", 1)?; let value = expect_operand(inst, 0)?; - match ctx.value_php_type(value)?.codegen_repr() { + let value_ty = ctx.value_php_type(value)?.codegen_repr(); + if has_eval_context(ctx) && value_ty != PhpType::Callable { + return lower_eval_is_callable(ctx, inst, value); + } + match value_ty { PhpType::Callable => emit_static_bool(ctx, true), PhpType::Str => { if let Ok(function_name) = const_string_operand(ctx, value) { @@ -373,7 +770,7 @@ pub(crate) fn lower_is_callable(ctx: &mut FunctionContext<'_>, inst: &Instructio /// Calls the runtime `is_callable` helper for pointer-shaped values already in result regs. fn emit_is_callable_pointer_lookup(ctx: &mut FunctionContext<'_>, label: &str) { if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // move pointer-shaped value into helper argument 0 + ctx.emitter.instruction("mov rdi, rax"); // move pointer-shaped value into helper argument 0 } abi::emit_call_label(ctx.emitter, label); } @@ -382,17 +779,205 @@ fn emit_is_callable_pointer_lookup(ctx: &mut FunctionContext<'_>, label: &str) { fn emit_is_callable_dynamic_string_lookup(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, x1"); // move string pointer into helper argument 0 - ctx.emitter.instruction("mov x1, x2"); // move string length into helper argument 1 + ctx.emitter.instruction("mov x0, x1"); // move string pointer into helper argument 0 + ctx.emitter.instruction("mov x1, x2"); // move string length into helper argument 1 } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, rax"); // move string pointer into helper argument 0 - ctx.emitter.instruction("mov rsi, rdx"); // move string length into helper argument 1 + ctx.emitter.instruction("mov rdi, rax"); // move string pointer into helper argument 0 + ctx.emitter.instruction("mov rsi, rdx"); // move string length into helper argument 1 } } abi::emit_call_label(ctx.emitter, "__rt_is_callable_string"); } +/// Lowers `method_exists()` and `property_exists()` through eval or static metadata. +fn lower_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, +) -> Result<()> { + ensure_arg_count(inst, name, 2)?; + let target = expect_operand(inst, 0)?; + let member = expect_operand(inst, 1)?; + if has_eval_context(ctx) { + return lower_eval_member_exists(ctx, inst, target, member, name); + } + let member_name = const_string_operand(ctx, member)?; + let exists = match ctx.value_php_type(target)?.codegen_repr() { + PhpType::Object(class_name) => { + static_member_exists_on_class(ctx, &class_name, &member_name, name, true) + } + PhpType::Str => { + let class_name = const_string_operand(ctx, target)?; + static_member_exists_on_class(ctx, &class_name, &member_name, name, false) + } + other => { + return Err(CodegenIrError::unsupported(format!( + "{} target PHP type {:?}", + name, other + ))) + } + }; + emit_static_bool(ctx, exists); + store_if_result(ctx, inst) +} + +/// Checks one static class-like target for `method_exists()` or `property_exists()`. +fn static_member_exists_on_class( + ctx: &FunctionContext<'_>, + class_name: &str, + member_name: &str, + name: &str, + target_is_object: bool, +) -> bool { + let Some((resolved_class, class_info)) = lookup_class_info(ctx, class_name) else { + return false; + }; + match name { + "method_exists" => static_method_exists_on_class_info( + ctx, + &resolved_class, + class_info, + member_name, + target_is_object, + ), + "property_exists" => static_property_exists_on_class_info( + &resolved_class, + class_info, + member_name, + target_is_object, + ), + _ => false, + } +} + +/// Looks up class metadata using PHP's case-insensitive class-name semantics. +fn lookup_class_info<'a>( + ctx: &'a FunctionContext<'_>, + class_name: &str, +) -> Option<(String, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(candidate, class_info)| (candidate.clone(), class_info)) +} + +/// Checks static method metadata while hiding inherited private methods on class-string targets. +fn static_method_exists_on_class_info( + ctx: &FunctionContext<'_>, + resolved_class: &str, + class_info: &ClassInfo, + method_name: &str, + target_is_object: bool, +) -> bool { + let method_key = php_symbol_key(method_name); + if class_info.methods.contains_key(&method_key) { + return target_is_object + || method_visible_from_class_string( + resolved_class, + &method_key, + &class_info.method_visibilities, + &class_info.method_declaring_classes, + ); + } + if class_info.static_methods.contains_key(&method_key) { + return target_is_object + || method_visible_from_class_string( + resolved_class, + &method_key, + &class_info.static_method_visibilities, + &class_info.static_method_declaring_classes, + ); + } + if target_is_object { + return static_parent_chain_method_exists(ctx, class_info, &method_key); + } + false +} + +/// Checks parent class metadata for private methods visible to object-target `method_exists()`. +fn static_parent_chain_method_exists( + ctx: &FunctionContext<'_>, + class_info: &ClassInfo, + method_key: &str, +) -> bool { + let mut visited = BTreeSet::new(); + let mut parent_name = class_info.parent.as_deref(); + while let Some(candidate) = parent_name { + let parent_key = php_symbol_key(candidate.trim_start_matches('\\')); + if !visited.insert(parent_key) { + return false; + } + let Some((_resolved_class, parent_info)) = lookup_class_info(ctx, candidate) else { + return false; + }; + if parent_info.methods.contains_key(method_key) + || parent_info.static_methods.contains_key(method_key) + { + return true; + } + parent_name = parent_info.parent.as_deref(); + } + false +} + +/// Returns whether a method should be visible for a class-string member probe. +fn method_visible_from_class_string( + resolved_class: &str, + method_key: &str, + visibilities: &std::collections::HashMap, + declaring_classes: &std::collections::HashMap, +) -> bool { + visibilities.get(method_key) != Some(&Visibility::Private) + || declaring_classes + .get(method_key) + .is_none_or(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(resolved_class.trim_start_matches('\\')) + }) +} + +/// Checks static property metadata while hiding inherited private properties. +fn static_property_exists_on_class_info( + resolved_class: &str, + class_info: &ClassInfo, + property_name: &str, + _target_is_object: bool, +) -> bool { + property_visible_from_class_string( + resolved_class, + property_name, + &class_info.property_visibilities, + &class_info.property_declaring_classes, + ) || property_visible_from_class_string( + resolved_class, + property_name, + &class_info.static_property_visibilities, + &class_info.static_property_declaring_classes, + ) +} + +/// Returns whether a property exists for a class-string or ordinary object probe. +fn property_visible_from_class_string( + resolved_class: &str, + property_name: &str, + visibilities: &std::collections::HashMap, + declaring_classes: &std::collections::HashMap, +) -> bool { + let Some(visibility) = visibilities.get(property_name) else { + return false; + }; + visibility != &Visibility::Private + || declaring_classes + .get(property_name) + .is_none_or(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(resolved_class.trim_start_matches('\\')) + }) +} + /// Returns true when a static `Class::method` string names a public static method. fn static_method_string_is_callable( ctx: &FunctionContext<'_>, @@ -420,8 +1005,8 @@ fn emit_variant_function_exists(ctx: &mut FunctionContext<'_>, function_name: &s abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, &active_symbol, 0); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test whether an include has activated this function variant - ctx.emitter.instruction(&format!("cset {}, ne", result_reg)); // return true only when a function variant is active + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test whether an include has activated this function variant + ctx.emitter.instruction(&format!("cset {}, ne", result_reg)); // return true only when a function variant is active } Arch::X86_64 => { ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // test whether an include has activated this function variant @@ -573,6 +1158,10 @@ pub(crate) fn lower_floatval(ctx: &mut FunctionContext<'_>, inst: &Instruction) ctx.load_value_to_result(value)?; abi::emit_call_label(ctx.emitter, "__rt_str_to_number"); } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + } other => { return Err(CodegenIrError::unsupported(format!( "floatval for PHP type {:?}", @@ -605,6 +1194,10 @@ pub(crate) fn lower_boolval(ctx: &mut FunctionContext<'_>, inst: &Instruction) - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable => { predicates::emit_array_truthiness(ctx, value)?; } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + } other => { return Err(CodegenIrError::unsupported(format!( "boolval for PHP type {:?}", @@ -615,6 +1208,12 @@ pub(crate) fn lower_boolval(ctx: &mut FunctionContext<'_>, inst: &Instruction) - store_if_result(ctx, inst) } +/// Lowers `strval()` through the same semantics as an explicit PHP string cast. +fn lower_strval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + ensure_arg_count(inst, "strval", 1)?; + conversions::lower_cast_to_string(ctx, inst) +} + /// Lowers `empty()` for concrete scalar and array-like operands. fn lower_empty(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "empty", 1)?; @@ -678,13 +1277,13 @@ fn emit_int_result_zero_bool(ctx: &mut FunctionContext<'_>) { let result_reg = abi::int_result_reg(ctx.emitter); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // compare the empty() integer operand against zero - ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // return true when the integer operand is zero + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // compare the empty() integer operand against zero + ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // return true when the integer operand is zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // compare the empty() integer operand against zero - ctx.emitter.instruction("sete al"); // materialize true when the integer operand is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // compare the empty() integer operand against zero + ctx.emitter.instruction("sete al"); // materialize true when the integer operand is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -693,14 +1292,14 @@ fn emit_int_result_zero_bool(ctx: &mut FunctionContext<'_>) { fn emit_float_result_zero_bool(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("fcmp d0, #0.0"); // compare the empty() float operand against zero - ctx.emitter.instruction("cset x0, eq"); // return true when the float operand is zero + ctx.emitter.instruction("fcmp d0, #0.0"); // compare the empty() float operand against zero + ctx.emitter.instruction("cset x0, eq"); // return true when the float operand is zero } Arch::X86_64 => { - ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for empty() comparison - ctx.emitter.instruction("ucomisd xmm0, xmm1"); // compare the empty() float operand against zero - ctx.emitter.instruction("sete al"); // materialize true when the float operand is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for empty() comparison + ctx.emitter.instruction("ucomisd xmm0, xmm1"); // compare the empty() float operand against zero + ctx.emitter.instruction("sete al"); // materialize true when the float operand is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -710,13 +1309,13 @@ fn emit_string_length_zero_bool(ctx: &mut FunctionContext<'_>) { let len_reg = abi::string_result_regs(ctx.emitter).1; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // compare the empty() string length against zero - ctx.emitter.instruction("cset x0, eq"); // return true when the string length is zero + ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // compare the empty() string length against zero + ctx.emitter.instruction("cset x0, eq"); // return true when the string length is zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // compare the empty() string length against zero - ctx.emitter.instruction("sete al"); // materialize true when the string length is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // compare the empty() string length against zero + ctx.emitter.instruction("sete al"); // materialize true when the string length is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -725,10 +1324,10 @@ fn emit_string_length_zero_bool(ctx: &mut FunctionContext<'_>) { fn invert_bool_result(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("eor x0, x0, #1"); // invert the canonical boolean result for empty() + ctx.emitter.instruction("eor x0, x0, #1"); // invert the canonical boolean result for empty() } Arch::X86_64 => { - ctx.emitter.instruction("xor rax, 1"); // invert the canonical boolean result for empty() + ctx.emitter.instruction("xor rax, 1"); // invert the canonical boolean result for empty() } } } @@ -775,17 +1374,17 @@ fn emit_tagged_scalar_int_predicate( "cmp x1, #{}", crate::codegen::sentinels::TAGGED_SCALAR_TAG_NULL ); - ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? - ctx.emitter.instruction("cset x0, ne"); // materialize true when the tagged scalar holds an integer + ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? + ctx.emitter.instruction("cset x0, ne"); // materialize true when the tagged scalar holds an integer } Arch::X86_64 => { let cmp_inst = format!( "cmp rdx, {}", crate::codegen::sentinels::TAGGED_SCALAR_TAG_NULL ); - ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? - ctx.emitter.instruction("setne al"); // materialize true when the tagged scalar holds an integer - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? + ctx.emitter.instruction("setne al"); // materialize true when the tagged scalar holds an integer + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } Ok(()) @@ -836,24 +1435,24 @@ fn emit_mixed_is_iterable(ctx: &mut FunctionContext<'_>, value: ValueId) -> Resu abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #4"); // check for a boxed indexed-array payload - ctx.emitter.instruction(&format!("b.eq {}", true_case)); // indexed arrays satisfy is_iterable - ctx.emitter.instruction("cmp x0, #5"); // check for a boxed associative-array payload - ctx.emitter.instruction(&format!("b.eq {}", true_case)); // associative arrays satisfy is_iterable - ctx.emitter.instruction("cmp x0, #6"); // check for a boxed object payload - ctx.emitter.instruction(&format!("b.eq {}", object_case)); // objects need a Traversable interface check - ctx.emitter.instruction("mov x0, #0"); // all other Mixed payloads are not iterable - ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path + ctx.emitter.instruction("cmp x0, #4"); // check for a boxed indexed-array payload + ctx.emitter.instruction(&format!("b.eq {}", true_case)); // indexed arrays satisfy is_iterable + ctx.emitter.instruction("cmp x0, #5"); // check for a boxed associative-array payload + ctx.emitter.instruction(&format!("b.eq {}", true_case)); // associative arrays satisfy is_iterable + ctx.emitter.instruction("cmp x0, #6"); // check for a boxed object payload + ctx.emitter.instruction(&format!("b.eq {}", object_case)); // objects need a Traversable interface check + ctx.emitter.instruction("mov x0, #0"); // all other Mixed payloads are not iterable + ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 4"); // check for a boxed indexed-array payload - ctx.emitter.instruction(&format!("je {}", true_case)); // indexed arrays satisfy is_iterable - ctx.emitter.instruction("cmp rax, 5"); // check for a boxed associative-array payload - ctx.emitter.instruction(&format!("je {}", true_case)); // associative arrays satisfy is_iterable - ctx.emitter.instruction("cmp rax, 6"); // check for a boxed object payload - ctx.emitter.instruction(&format!("je {}", object_case)); // objects need a Traversable interface check - ctx.emitter.instruction("mov rax, 0"); // all other Mixed payloads are not iterable - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path + ctx.emitter.instruction("cmp rax, 4"); // check for a boxed indexed-array payload + ctx.emitter.instruction(&format!("je {}", true_case)); // indexed arrays satisfy is_iterable + ctx.emitter.instruction("cmp rax, 5"); // check for a boxed associative-array payload + ctx.emitter.instruction(&format!("je {}", true_case)); // associative arrays satisfy is_iterable + ctx.emitter.instruction("cmp rax, 6"); // check for a boxed object payload + ctx.emitter.instruction(&format!("je {}", object_case)); // objects need a Traversable interface check + ctx.emitter.instruction("mov rax, 0"); // all other Mixed payloads are not iterable + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path } } ctx.emitter.label(&object_case); @@ -879,16 +1478,16 @@ fn emit_runtime_object_iterable_check( } match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("str x1, [sp, #-16]!"); // preserve the unboxed object pointer across Traversable checks + ctx.emitter.instruction("str x1, [sp, #-16]!"); // preserve the unboxed object pointer across Traversable checks for interface_id in interface_ids { emit_saved_object_interface_check(ctx, interface_id, &object_true); } - ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer after failed checks - ctx.emitter.instruction("mov x0, #0"); // non-Traversable objects are not iterable - ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path + ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer after failed checks + ctx.emitter.instruction("mov x0, #0"); // non-Traversable objects are not iterable + ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path ctx.emitter.label(&object_true); - ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer before returning true - ctx.emitter.instruction(&format!("b {}", true_case)); // continue through the shared truthy result path + ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer before returning true + ctx.emitter.instruction(&format!("b {}", true_case)); // continue through the shared truthy result path } Arch::X86_64 => { abi::emit_push_reg(ctx.emitter, "rdi"); @@ -896,11 +1495,11 @@ fn emit_runtime_object_iterable_check( emit_saved_object_interface_check(ctx, interface_id, &object_true); } abi::emit_pop_reg(ctx.emitter, "r10"); - ctx.emitter.instruction("xor eax, eax"); // non-Traversable objects are not iterable - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path + ctx.emitter.instruction("xor eax, eax"); // non-Traversable objects are not iterable + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path ctx.emitter.label(&object_true); abi::emit_pop_reg(ctx.emitter, "r10"); - ctx.emitter.instruction(&format!("jmp {}", true_case)); // continue through the shared truthy result path + ctx.emitter.instruction(&format!("jmp {}", true_case)); // continue through the shared truthy result path } } } @@ -913,20 +1512,20 @@ fn emit_saved_object_interface_check( ) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + ctx.emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 abi::emit_load_int_immediate(ctx.emitter, "x1", interface_id as i64); abi::emit_load_int_immediate(ctx.emitter, "x2", 1); - abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface - ctx.emitter.instruction("cmp x0, #0"); // test whether the runtime matcher succeeded - ctx.emitter.instruction(&format!("b.ne {}", true_case)); // a matching interface makes the object iterable + abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface + ctx.emitter.instruction("cmp x0, #0"); // test whether the runtime matcher succeeded + ctx.emitter.instruction(&format!("b.ne {}", true_case)); // a matching interface makes the object iterable } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 abi::emit_load_int_immediate(ctx.emitter, "rsi", interface_id as i64); abi::emit_load_int_immediate(ctx.emitter, "rdx", 1); - abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface - ctx.emitter.instruction("test rax, rax"); // test whether the runtime matcher succeeded - ctx.emitter.instruction(&format!("jne {}", true_case)); // a matching interface makes the object iterable + abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface + ctx.emitter.instruction("test rax, rax"); // test whether the runtime matcher succeeded + ctx.emitter.instruction(&format!("jne {}", true_case)); // a matching interface makes the object iterable } } } @@ -1092,23 +1691,26 @@ fn is_internal_synthetic_class_name(name: &str) -> bool { /// Returns a string literal value defined by a `ConstStr` instruction. fn const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result { + maybe_const_string_operand(ctx, value)?.ok_or_else(|| { + CodegenIrError::unsupported("function_exists with non-literal function name") + }) +} + +/// Returns a string literal operand when a value is produced by `ConstStr`. +fn maybe_const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { let value_ref = ctx .function .value(value) .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; let ValueDef::Instruction { inst, .. } = value_ref.def else { - return Err(CodegenIrError::unsupported( - "function_exists with non-literal function name", - )); + return Ok(None); }; let inst_ref = ctx .function .instruction(inst) .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; if inst_ref.op != Op::ConstStr { - return Err(CodegenIrError::unsupported( - "function_exists with non-literal function name", - )); + return Ok(None); } let Some(Immediate::Data(data)) = inst_ref.immediate else { return Err(CodegenIrError::invalid_module( @@ -1120,6 +1722,7 @@ fn const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result, env_source: Option, + param_types: Vec, return_ty: PhpType, } @@ -3193,6 +3194,7 @@ struct StaticMethodCallbackTarget { struct InstanceMethodCallbackTarget { entry_label: String, receiver: ValueId, + param_types: Vec, return_ty: PhpType, } @@ -3287,6 +3289,7 @@ fn static_method_callback_target_inner( called_class: StaticCallbackCalledClass::Immediate(receiver_info.class_id), dynamic_slot: None, env_source: None, + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3346,6 +3349,7 @@ fn static_late_bound_method_callback_target( called_class: StaticCallbackCalledClass::Env, dynamic_slot: receiver_info.static_vtable_slots.get(&method_key).copied(), env_source: Some(static_callback_env_source(ctx)?), + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3449,6 +3453,7 @@ fn instance_method_sort_callback_target( Ok(Some(InstanceMethodCallbackTarget { entry_label: method_symbol(impl_class, &method_key), receiver, + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3511,7 +3516,7 @@ fn require_static_method_callback_arg_types( Ok(()) } -/// Verifies the target static method can consume the wrapper's unboxed integer ABI values. +/// Verifies the target method can consume the wrapper's runtime callback ABI values. fn require_static_method_callback_param_types( owner: &str, callback_name: &str, @@ -3524,10 +3529,10 @@ fn require_static_method_callback_param_types( if matches!(visible_ty, PhpType::Void | PhpType::Never) { continue; } - if matches!( - (¶m_ty, &visible_ty), - (PhpType::Int | PhpType::Bool, PhpType::Int | PhpType::Bool) - ) { + if param_ty == PhpType::Mixed { + continue; + } + if matches!((¶m_ty, &visible_ty), (PhpType::Int | PhpType::Bool, PhpType::Int | PhpType::Bool)) { continue; } if matches!((¶m_ty, &visible_ty), (PhpType::Str, PhpType::Str)) { @@ -3541,6 +3546,79 @@ fn require_static_method_callback_param_types( Ok(()) } +const NO_CALLBACK_BOX_OFFSET: usize = usize::MAX; + +/// Stack frame layout used by generated callback ABI adapters. +struct CallbackWrapperFrame { + hidden_offset: usize, + raw_offsets: Vec, + boxed_offsets: Vec, + return_offset: usize, + return_address_offset: usize, + total_bytes: usize, +} + +/// Builds the stack layout for one generated callback wrapper. +fn callback_wrapper_frame(param_types: &[PhpType], visible_arg_types: &[PhpType]) -> CallbackWrapperFrame { + let mut offset = 0usize; + let hidden_offset = offset; + offset += 8; + let mut raw_offsets = Vec::with_capacity(visible_arg_types.len()); + for ty in visible_arg_types { + raw_offsets.push(offset); + offset += callback_visible_arg_slot_size(ty); + } + let mut boxed_offsets = Vec::with_capacity(visible_arg_types.len()); + for (param_ty, visible_ty) in param_types.iter().zip(visible_arg_types.iter()) { + if callback_arg_needs_mixed_box(param_ty, visible_ty) { + boxed_offsets.push(offset); + offset += 8; + } else { + boxed_offsets.push(NO_CALLBACK_BOX_OFFSET); + } + } + let return_offset = offset; + offset += 16; + let return_address_offset = offset; + offset += 8; + CallbackWrapperFrame { + hidden_offset, + raw_offsets, + boxed_offsets, + return_offset, + return_address_offset, + total_bytes: align_callback_frame_bytes(offset), + } +} + +/// Rounds a wrapper frame size up to the stack alignment required before calls. +fn align_callback_frame_bytes(bytes: usize) -> usize { + (bytes + 15) & !15 +} + +/// Returns the stack bytes needed to preserve one incoming runtime callback argument. +fn callback_visible_arg_slot_size(ty: &PhpType) -> usize { + if matches!(ty.codegen_repr(), PhpType::Str) { + 16 + } else { + 8 + } +} + +/// Returns true when the target method parameter needs a boxed Mixed argument. +fn callback_arg_needs_mixed_box(param_ty: &PhpType, visible_ty: &PhpType) -> bool { + param_ty.codegen_repr() == PhpType::Mixed + && !matches!(visible_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) +} + +/// Returns true when any callback argument must be boxed for the target signature. +fn callback_wrapper_has_boxed_args(frame: &CallbackWrapperFrame) -> bool { + frame + .boxed_offsets + .iter() + .any(|offset| *offset != NO_CALLBACK_BOX_OFFSET) +} + /// Counts integer ABI registers consumed by the visible callback argument list. fn callback_arg_abi_slots(visible_arg_types: &[PhpType]) -> usize { visible_arg_types @@ -3555,48 +3633,189 @@ fn callback_arg_abi_slots(visible_arg_types: &[PhpType]) -> usize { .sum() } -/// Shifts AArch64 callback arguments right by one slot for a hidden receiver/class id. -fn shift_callback_args_after_hidden_aarch64( +/// Saves the incoming runtime callback arguments before nested boxing calls can clobber them. +fn save_callback_visible_args( ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, visible_arg_types: &[PhpType], ) { - match visible_arg_types { - [ty] if matches!(ty.codegen_repr(), PhpType::Str) => { - ctx.emitter.instruction("mov x2, x1"); // shift the callback string length after the hidden receiver/class id - ctx.emitter.instruction("mov x1, x0"); // shift the callback string pointer after the hidden receiver/class id - } - [_] => { - ctx.emitter.instruction("mov x1, x0"); // shift the scalar callback argument after the hidden receiver/class id + let mut reg_index = 0usize; + for (index, ty) in visible_arg_types.iter().enumerate() { + let offset = frame.raw_offsets[index]; + if matches!(ty.codegen_repr(), PhpType::Str) { + let ptr_reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index); + let len_reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index + 1); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, offset); + abi::emit_store_to_sp(ctx.emitter, len_reg, offset + 8); + reg_index += 2; + } else { + let reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index); + abi::emit_store_to_sp(ctx.emitter, reg, offset); + reg_index += 1; } - [_, _] => { - ctx.emitter.instruction("mov x2, x1"); // shift the second scalar callback argument after the hidden receiver/class id - ctx.emitter.instruction("mov x1, x0"); // shift the first scalar callback argument after the hidden receiver/class id + } +} + +/// Saves an already materialized hidden receiver or called-class id into the wrapper frame. +fn save_callback_hidden_arg(ctx: &mut FunctionContext<'_>, frame: &CallbackWrapperFrame, reg: &str) { + abi::emit_store_to_sp(ctx.emitter, reg, frame.hidden_offset); +} + +/// Boxes visible runtime arguments whose target method parameters are `Mixed`. +fn box_callback_mixed_args( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + param_types: &[PhpType], + visible_arg_types: &[PhpType], +) { + for (index, (param_ty, visible_ty)) in param_types.iter().zip(visible_arg_types.iter()).enumerate() { + let box_offset = frame.boxed_offsets[index]; + if box_offset == NO_CALLBACK_BOX_OFFSET || !callback_arg_needs_mixed_box(param_ty, visible_ty) { + continue; } - _ => {} + load_callback_raw_arg_to_result(ctx, frame.raw_offsets[index], visible_ty); + emit_box_current_value_as_mixed(ctx.emitter, &visible_ty.codegen_repr()); + abi::emit_store_to_sp(ctx.emitter, abi::int_result_reg(ctx.emitter), box_offset); } } -/// Shifts x86_64 callback arguments right by one slot for a hidden receiver/class id. -fn shift_callback_args_after_hidden_x86_64( +/// Loads a saved runtime callback argument into the normal result registers. +fn load_callback_raw_arg_to_result( ctx: &mut FunctionContext<'_>, + raw_offset: usize, + visible_ty: &PhpType, +) { + if matches!(visible_ty.codegen_repr(), PhpType::Str) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, raw_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, raw_offset + 8); + } else { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), raw_offset); + } +} + +/// Loads the hidden receiver/class id and visible method parameters into callee ABI registers. +fn load_callback_target_args( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, visible_arg_types: &[PhpType], ) { - match visible_arg_types { - [ty] if matches!(ty.codegen_repr(), PhpType::Str) => { - ctx.emitter.instruction("mov rdx, rsi"); // shift the callback string length after the hidden receiver/class id - ctx.emitter.instruction("mov rsi, rdi"); // shift the callback string pointer after the hidden receiver/class id + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + frame.hidden_offset, + ); + let mut reg_index = 1usize; + for (index, visible_ty) in visible_arg_types.iter().enumerate() { + let box_offset = frame.boxed_offsets[index]; + if box_offset != NO_CALLBACK_BOX_OFFSET { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + box_offset, + ); + reg_index += 1; + } else if matches!(visible_ty.codegen_repr(), PhpType::Str) { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + frame.raw_offsets[index], + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index + 1), + frame.raw_offsets[index] + 8, + ); + reg_index += 2; + } else { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + frame.raw_offsets[index], + ); + reg_index += 1; + } + } +} + +/// Saves a callback return value while boxed argument temporaries are released. +fn save_callback_return_value( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + match return_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, frame.return_offset); + abi::emit_store_to_sp(ctx.emitter, len_reg, frame.return_offset + 8); } - [_] => { - ctx.emitter.instruction("mov rsi, rdi"); // shift the scalar callback argument after the hidden receiver/class id + PhpType::Float => { + abi::emit_store_to_sp(ctx.emitter, abi::float_result_reg(ctx.emitter), frame.return_offset); } - [_, _] => { - ctx.emitter.instruction("mov rdx, rsi"); // shift the second scalar callback argument after the hidden receiver/class id - ctx.emitter.instruction("mov rsi, rdi"); // shift the first scalar callback argument after the hidden receiver/class id + _ => { + abi::emit_store_to_sp(ctx.emitter, abi::int_result_reg(ctx.emitter), frame.return_offset); } - _ => {} } } +/// Restores a callback return value after boxed argument temporaries are released. +fn restore_callback_return_value( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + match return_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, frame.return_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, frame.return_offset + 8); + } + PhpType::Float => { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::float_result_reg(ctx.emitter), frame.return_offset); + } + _ => { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), frame.return_offset); + } + } +} + +/// Releases boxed Mixed arguments allocated by the wrapper for target `Mixed` parameters. +fn release_callback_boxed_args(ctx: &mut FunctionContext<'_>, frame: &CallbackWrapperFrame) { + for offset in &frame.boxed_offsets { + if *offset == NO_CALLBACK_BOX_OFFSET { + continue; + } + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), *offset); + abi::emit_decref_if_refcounted(ctx.emitter, &PhpType::Mixed); + } +} + +/// Cleans up boxed callback arguments without losing the callback return value. +fn cleanup_callback_boxed_args( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + if !callback_wrapper_has_boxed_args(frame) { + return; + } + if callback_return_may_alias_boxed_args(return_ty) { + return; + } + save_callback_return_value(ctx, frame, return_ty); + release_callback_boxed_args(ctx, frame); + restore_callback_return_value(ctx, frame, return_ty); +} + +/// Returns true when the callback result may be one of the boxed wrapper arguments. +fn callback_return_may_alias_boxed_args(return_ty: &PhpType) -> bool { + matches!( + return_ty.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar + ) +} + /// Emits a local wrapper that prepends the hidden static called-class id. fn emit_static_method_callback_wrapper( ctx: &mut FunctionContext<'_>, @@ -3623,25 +3842,26 @@ fn emit_static_method_callback_wrapper_aarch64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); - ctx.emitter.instruction("sub sp, sp, #16"); // reserve wrapper spill space for the runtime callback return address - ctx.emitter.instruction("str x30, [sp, #8]"); // preserve the runtime helper return address across the static method call + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); + save_callback_visible_args(ctx, &frame, visible_arg_types); match target.called_class { StaticCallbackCalledClass::Immediate(class_id) => { abi::emit_load_int_immediate(ctx.emitter, "x3", class_id as i64); } StaticCallbackCalledClass::Env => { - ctx.emitter.instruction(&format!("ldr x3, [{}]", env_reg)); // load the late-static called-class id from the callback environment + abi::emit_load_from_address(ctx.emitter, "x3", env_reg, 0); } } - shift_callback_args_after_hidden_aarch64(ctx, visible_arg_types); - ctx.emitter.instruction("mov x0, x3"); // pass the called-class id as the hidden static method argument + save_callback_hidden_arg(ctx, &frame, "x3"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); emit_static_callback_dispatch(ctx, target); - ctx.emitter.instruction("ldr x30, [sp, #8]"); // restore the runtime helper return address after the static method call - ctx.emitter.instruction("add sp, sp, #16"); // release the wrapper spill space before returning to the runtime helper + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x30", frame.return_address_offset); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("ret"); // return the static method result to the runtime callback helper } @@ -3651,24 +3871,26 @@ fn emit_static_method_callback_wrapper_x86_64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested static method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + save_callback_visible_args(ctx, &frame, visible_arg_types); match target.called_class { StaticCallbackCalledClass::Immediate(class_id) => { abi::emit_load_int_immediate(ctx.emitter, "rcx", class_id as i64); } StaticCallbackCalledClass::Env => { - ctx.emitter - .instruction(&format!("mov rcx, QWORD PTR [{}]", env_reg)); // load the late-static called-class id from the callback environment + abi::emit_load_from_address(ctx.emitter, "rcx", env_reg, 0); } } - shift_callback_args_after_hidden_x86_64(ctx, visible_arg_types); - ctx.emitter.instruction("mov rdi, rcx"); // pass the called-class id as the hidden static method argument + save_callback_hidden_arg(ctx, &frame, "rcx"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); emit_static_callback_dispatch(ctx, target); + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning ctx.emitter.instruction("ret"); // return the static method result to the runtime callback helper } @@ -3701,18 +3923,19 @@ fn emit_instance_method_callback_wrapper_aarch64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); - ctx.emitter.instruction("sub sp, sp, #16"); // reserve wrapper spill space for the runtime callback return address - ctx.emitter.instruction("str x30, [sp, #8]"); // preserve the runtime helper return address across the instance method call - ctx.emitter.instruction(&format!("ldr x3, [{}]", env_reg)); // load the captured object receiver from the callback environment - shift_callback_args_after_hidden_aarch64(ctx, visible_arg_types); - ctx.emitter.instruction("mov x0, x3"); // pass the captured object receiver as the method receiver + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); + save_callback_visible_args(ctx, &frame, visible_arg_types); + abi::emit_load_from_address(ctx.emitter, "x3", env_reg, 0); + save_callback_hidden_arg(ctx, &frame, "x3"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); abi::emit_call_label(ctx.emitter, &target.entry_label); - ctx.emitter.instruction("ldr x30, [sp, #8]"); // restore the runtime helper return address after the instance method call - ctx.emitter.instruction("add sp, sp, #16"); // release the wrapper spill space before returning to the runtime helper + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x30", frame.return_address_offset); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("ret"); // return the instance method result to the runtime callback helper } @@ -3722,17 +3945,19 @@ fn emit_instance_method_callback_wrapper_x86_64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested instance method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments - ctx.emitter - .instruction(&format!("mov rcx, QWORD PTR [{}]", env_reg)); // load the captured object receiver from the callback environment - shift_callback_args_after_hidden_x86_64(ctx, visible_arg_types); - ctx.emitter.instruction("mov rdi, rcx"); // pass the captured object receiver as the method receiver + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + save_callback_visible_args(ctx, &frame, visible_arg_types); + abi::emit_load_from_address(ctx.emitter, "rcx", env_reg, 0); + save_callback_hidden_arg(ctx, &frame, "rcx"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); abi::emit_call_label(ctx.emitter, &target.entry_label); + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning ctx.emitter.instruction("ret"); // return the instance method result to the runtime callback helper } diff --git a/src/codegen/lower_inst/builtins/arrays/key_exists.rs b/src/codegen/lower_inst/builtins/arrays/key_exists.rs index 9978ff51f7..e589047a14 100644 --- a/src/codegen/lower_inst/builtins/arrays/key_exists.rs +++ b/src/codegen/lower_inst/builtins/arrays/key_exists.rs @@ -26,6 +26,9 @@ pub(super) fn lower_array_key_exists(ctx: &mut FunctionContext<'_>, inst: &Instr match ctx.value_php_type(array)?.codegen_repr() { PhpType::Array(_) => lower_indexed_array_key_exists(ctx, inst, key, array), PhpType::AssocArray { .. } => lower_assoc_array_key_exists(ctx, inst, key, array), + PhpType::Mixed | PhpType::Union(_) => { + lower_mixed_container_key_exists(ctx, inst, key, array) + } other => Err(CodegenIrError::unsupported(format!( "array_key_exists for PHP array type {:?}", other @@ -33,6 +36,101 @@ pub(super) fn lower_array_key_exists(ctx: &mut FunctionContext<'_>, inst: &Instr } } +/// Lowers `array_key_exists()` for a boxed Mixed container by dispatching on +/// its runtime tag: hashes probe `__rt_hash_get`, indexed arrays reuse the +/// int-key bounds helper (the key normalizer already folds numeric strings to +/// integer keys), and non-container payloads answer false. +fn lower_mixed_container_key_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + key: ValueId, + array: ValueId, +) -> Result<()> { + let hash_label = ctx.next_label("mixed_key_exists_hash"); + let indexed_label = ctx.next_label("mixed_key_exists_indexed"); + let missing_label = ctx.next_label("mixed_key_exists_missing"); + let done_label = ctx.next_label("mixed_key_exists_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + // Normalize the key first: the helpers leave (key_lo, key_hi) in + // x1/x2 with key_hi == -1 marking integer keys. + materialize_hash_key_aarch64(ctx, key)?; + ctx.load_value_to_reg(array, "x9")?; + ctx.emitter + .instruction(&format!("cbz x9, {}", missing_label)); // null Mixed containers hold no keys + ctx.emitter.instruction("ldr x10, [x9]"); // load the container runtime tag + ctx.emitter.instruction("ldr x0, [x9, #8]"); // load the boxed payload pointer + ctx.emitter + .instruction(&format!("cbz x0, {}", missing_label)); // defensive payload null guard + ctx.emitter.instruction("cmp x10, #5"); // tag 5 = associative hash + ctx.emitter + .instruction(&format!("b.eq {}", hash_label)); // hashes probe the hash table + ctx.emitter.instruction("cmp x10, #4"); // tag 4 = indexed array + ctx.emitter + .instruction(&format!("b.eq {}", indexed_label)); // indexed arrays use the int-key helper + ctx.emitter + .instruction(&format!("b {}", missing_label)); // non-container payloads have no keys + + ctx.emitter.label(&indexed_label); + ctx.emitter.instruction("cmn x2, #1"); // key_hi == -1 marks an integer key + ctx.emitter + .instruction(&format!("b.ne {}", missing_label)); // string keys never exist in indexed arrays + abi::emit_call_label(ctx.emitter, "__rt_array_key_exists"); + ctx.emitter + .instruction(&format!("b {}", done_label)); // result already in x0 + + ctx.emitter.label(&hash_label); + abi::emit_call_label(ctx.emitter, "__rt_hash_get"); + ctx.emitter + .instruction(&format!("b {}", done_label)); // found flag already in x0 + + ctx.emitter.label(&missing_label); + ctx.emitter.instruction("mov x0, #0"); // missing/unsupported containers answer false + ctx.emitter.label(&done_label); + } + Arch::X86_64 => { + // Normalize the key first: the helpers leave (key_lo, key_hi) in + // rsi/rdx with key_hi == -1 marking integer keys. + materialize_hash_key_x86_64(ctx, key)?; + ctx.load_value_to_reg(array, "r10")?; + ctx.emitter.instruction("test r10, r10"); // null Mixed containers hold no keys + ctx.emitter + .instruction(&format!("jz {}", missing_label)); + ctx.emitter.instruction("mov r11, QWORD PTR [r10]"); // load the container runtime tag + ctx.emitter.instruction("mov rdi, QWORD PTR [r10 + 8]"); // load the boxed payload pointer + ctx.emitter.instruction("test rdi, rdi"); // defensive payload null guard + ctx.emitter + .instruction(&format!("jz {}", missing_label)); + ctx.emitter.instruction("cmp r11, 5"); // tag 5 = associative hash + ctx.emitter + .instruction(&format!("je {}", hash_label)); // hashes probe the hash table + ctx.emitter.instruction("cmp r11, 4"); // tag 4 = indexed array + ctx.emitter + .instruction(&format!("je {}", indexed_label)); // indexed arrays use the int-key helper + ctx.emitter + .instruction(&format!("jmp {}", missing_label)); // non-container payloads have no keys + + ctx.emitter.label(&indexed_label); + ctx.emitter.instruction("cmp rdx, -1"); // key_hi == -1 marks an integer key + ctx.emitter + .instruction(&format!("jne {}", missing_label)); // string keys never exist in indexed arrays + abi::emit_call_label(ctx.emitter, "__rt_array_key_exists"); + ctx.emitter + .instruction(&format!("jmp {}", done_label)); // result already in rax + + ctx.emitter.label(&hash_label); + abi::emit_call_label(ctx.emitter, "__rt_hash_get"); + ctx.emitter + .instruction(&format!("jmp {}", done_label)); // found flag already in rax + + ctx.emitter.label(&missing_label); + ctx.emitter.instruction("xor eax, eax"); // missing/unsupported containers answer false + ctx.emitter.label(&done_label); + } + } + store_if_result(ctx, inst) +} + /// Lowers indexed-array key existence through the bounds-check runtime helper. fn lower_indexed_array_key_exists( ctx: &mut FunctionContext<'_>, @@ -90,6 +188,12 @@ fn materialize_hash_key_aarch64(ctx: &mut FunctionContext<'_>, key: ValueId) -> abi::emit_load_int_immediate(ctx.emitter, "x2", -1); Ok(()) } + PhpType::Float => { + ctx.load_value_to_reg(key, "d0")?; + ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + Ok(()) + } // PHP null normalizes to the empty string "" as an array key. PhpType::Void | PhpType::Never => { let (label, len) = ctx.data.add_string(b""); @@ -119,6 +223,12 @@ fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: ValueId) -> R abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); Ok(()) } + PhpType::Float => { + ctx.load_value_to_reg(key, "xmm0")?; + ctx.emitter.instruction("cvttsd2si rsi, xmm0"); // PHP casts float array keys to integer keys + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + Ok(()) + } // PHP null normalizes to the empty string "" as an array key. PhpType::Void | PhpType::Never => { let (label, len) = ctx.data.add_string(b""); diff --git a/src/codegen/lower_inst/builtins/attributes.rs b/src/codegen/lower_inst/builtins/attributes.rs index aee9f45461..a5f97aee03 100644 --- a/src/codegen/lower_inst/builtins/attributes.rs +++ b/src/codegen/lower_inst/builtins/attributes.rs @@ -12,13 +12,19 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; use crate::codegen::{CodegenIrError, Result}; -use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; +use crate::ir::{Immediate, Instruction, Module, Op, ValueDef, ValueId}; use crate::names::php_symbol_key; -use crate::types::{AttrArgEntry, AttrArgValue, ClassInfo, PhpType}; +use crate::types::{AttrArgEntry, AttrArgValue, AttrKey, ClassInfo, PhpType}; use super::super::super::context::FunctionContext; const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_CLASS: i64 = 1; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_FUNCTION: i64 = 2; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_METHOD: i64 = 4; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_PROPERTY: i64 = 8; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT: i64 = 16; +pub(in crate::codegen::lower_inst) const REFLECTION_ATTRIBUTE_TARGET_PARAMETER: i64 = 32; /// Fixed object slot layout for the synthetic `ReflectionAttribute` class. struct ReflectionAttributeLayout { @@ -30,6 +36,10 @@ struct ReflectionAttributeLayout { args_hi: usize, factory_lo: usize, factory_hi: usize, + target_lo: usize, + target_hi: usize, + repeated_lo: usize, + repeated_hi: usize, } /// Lowers `class_attribute_names(class)` into an indexed string array. @@ -48,7 +58,7 @@ pub(crate) fn lower_class_attribute_names( super::store_if_result(ctx, inst) } -/// Lowers `class_attribute_args(class, attr)` into an indexed Mixed array. +/// Lowers `class_attribute_args(class, attr)` into a Mixed PHP argument array. pub(crate) fn lower_class_attribute_args( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -76,7 +86,12 @@ pub(crate) fn lower_class_get_attributes( .map(|info| (info.attribute_names.clone(), info.attribute_args.clone())) .unwrap_or_else(|| (Vec::new(), Vec::new())); - emit_reflection_attribute_array(ctx, &attr_names, &attr_args)?; + emit_reflection_attribute_array( + ctx, + &attr_names, + &attr_args, + REFLECTION_ATTRIBUTE_TARGET_CLASS, + )?; super::store_if_result(ctx, inst) } @@ -89,15 +104,18 @@ fn attribute_args( let attr_key = php_symbol_key(attr_name.trim_start_matches('\\')); class_info(ctx, class_name) .and_then(|info| { - info.attribute_names.iter().enumerate().find_map(|(idx, name)| { - let candidate = php_symbol_key(name.trim_start_matches('\\')); - (candidate == attr_key).then(|| { - info.attribute_args - .get(idx) - .and_then(Clone::clone) - .unwrap_or_default() + info.attribute_names + .iter() + .enumerate() + .find_map(|(idx, name)| { + let candidate = php_symbol_key(name.trim_start_matches('\\')); + (candidate == attr_key).then(|| { + info.attribute_args + .get(idx) + .and_then(Clone::clone) + .unwrap_or_default() + }) }) - }) }) .unwrap_or_default() } @@ -117,6 +135,7 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( ctx: &mut FunctionContext<'_>, attr_names: &[String], attr_args: &[Option>], + target: i64, ) -> Result<()> { let layout = reflection_attribute_layout(ctx)?; allocate_indexed_array(ctx, attr_names.len().max(1), 8); @@ -131,11 +150,15 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( .get(idx) .and_then(|args| args.as_deref()) .unwrap_or(&[]); - let factory_id = crate::codegen::reflection::attribute_factory_id( - &ctx.module.class_infos, - attr_name, - attr_arg_list, - ); + let factory_id = { + let function_attrs = function_attribute_sources(ctx.module); + crate::codegen::reflection::attribute_factory_id_with_extra( + &ctx.module.class_infos, + &function_attrs, + attr_name, + attr_arg_list, + ) + }; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); emit_reflection_attribute_object(ctx, &layout); @@ -143,12 +166,35 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( emit_set_name_property(ctx, attr_name, &layout); emit_set_args_property(ctx, attr_arg_list, &layout)?; emit_set_factory_property(ctx, factory_id, &layout); + emit_set_target_property(ctx, target, &layout); + emit_set_repeated_property( + ctx, + reflection_attribute_name_is_repeated(attr_names, attr_name), + &layout, + ); emit_append_reflection_attribute_object(ctx); } Ok(()) } +/// Returns reflection-visible top-level function attribute metadata sources. +fn function_attribute_sources( + module: &Module, +) -> Vec> { + module + .functions + .iter() + .filter(|function| !function.attribute_names.is_empty()) + .map(|function| { + ( + function.attribute_names.as_slice(), + function.attribute_args.as_slice(), + ) + }) + .collect() +} + /// Returns the synthetic `ReflectionAttribute` class layout from EIR metadata. fn reflection_attribute_layout(ctx: &FunctionContext<'_>) -> Result { let info = ctx @@ -159,6 +205,8 @@ fn reflection_attribute_layout(ctx: &FunctionContext<'_>) -> Result) -> Result { - ctx.emitter.instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage + ctx.emitter + .instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks ReflectionAttribute as an object ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload - ctx.emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id + ctx.emitter + .instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage + ctx.emitter + .instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload - ctx.emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id + ctx.emitter + .instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero } } @@ -276,6 +335,44 @@ fn emit_set_factory_property( abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.factory_hi); } +/// Stores the PHP `Attribute::TARGET_*` bitmask on the stacked reflection object. +fn emit_set_target_property( + ctx: &mut FunctionContext<'_>, + target: i64, + layout: &ReflectionAttributeLayout, +) { + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let target_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, target_reg, target); + abi::emit_store_to_address(ctx.emitter, target_reg, object_reg, layout.target_lo); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.target_hi); +} + +/// Stores whether this attribute name is repeated on the same owner. +fn emit_set_repeated_property( + ctx: &mut FunctionContext<'_>, + repeated: bool, + layout: &ReflectionAttributeLayout, +) { + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let repeated_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, repeated_reg, if repeated { 1 } else { 0 }); + abi::emit_store_to_address(ctx.emitter, repeated_reg, object_reg, layout.repeated_lo); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.repeated_hi); +} + +/// Returns true when an attribute name appears multiple times on one reflected owner. +fn reflection_attribute_name_is_repeated(attr_names: &[String], attr_name: &str) -> bool { + let needle = php_symbol_key(attr_name.trim_start_matches('\\')); + attr_names + .iter() + .filter(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == needle) + .nth(1) + .is_some() +} + /// Appends the stacked object to the stacked result array and leaves the array in result. fn emit_append_reflection_attribute_object(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { @@ -332,68 +429,145 @@ fn emit_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String] ctx.emitter.instruction("pop rax"); // restore the final attribute-name array as the result } -/// Allocates and fills an indexed array of boxed Mixed attribute arguments. +/// Allocates and fills a PHP hash array of boxed Mixed attribute arguments. fn emit_mixed_array(ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry]) -> Result<()> { - allocate_indexed_array(ctx, attr_args.len().max(1), 8); - crate::codegen::emit_array_value_type_stamp( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - &PhpType::Mixed, - ); + allocate_mixed_hash(ctx, attr_args.len().max(1)); match ctx.emitter.target.arch { Arch::AArch64 => emit_mixed_array_fill_aarch64(ctx, attr_args), Arch::X86_64 => emit_mixed_array_fill_x86_64(ctx, attr_args), } } -/// Appends boxed Mixed attribute arguments to the current result array on AArch64. +/// Inserts boxed Mixed attribute arguments into the current result hash on AArch64. fn emit_mixed_array_fill_aarch64( ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry], ) -> Result<()> { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the attribute-arg array while boxing values - for entry in attr_args { + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the attribute-arg hash while boxing values + for (index, entry) in attr_args.iter().enumerate() { emit_box_arg(ctx, &entry.value)?; - ctx.emitter.instruction("mov x1, x0"); // pass the boxed attribute argument as the append value - ctx.emitter.instruction("ldr x0, [sp]"); // reload the attribute-arg array for this append - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown attribute-arg array + ctx.emitter.instruction("mov x3, x0"); // pass the boxed argument as the hash value payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use a high payload word + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, + ); + ctx.emitter.instruction("ldr x0, [sp]"); // reload the attribute-arg hash for this insertion + emit_attribute_arg_key_aarch64(ctx, index, entry); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown attribute-arg hash } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final attribute-arg array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final attribute-arg hash as the result Ok(()) } -/// Appends boxed Mixed attribute arguments to the current result array on x86_64. +/// Inserts boxed Mixed attribute arguments into the current result hash on x86_64. fn emit_mixed_array_fill_x86_64( ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry], ) -> Result<()> { - ctx.emitter.instruction("push rax"); // park the attribute-arg array while boxing values + ctx.emitter.instruction("push rax"); // park the attribute-arg hash while boxing values ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across helper calls - for entry in attr_args { + for (index, entry) in attr_args.iter().enumerate() { emit_box_arg(ctx, &entry.value)?; - ctx.emitter.instruction("mov rsi, rax"); // pass the boxed attribute argument as the append value - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the attribute-arg array for this append - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown attribute-arg array + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed argument as the hash value payload + abi::emit_load_int_immediate(ctx.emitter, "r8", 0); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, + ); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the attribute-arg hash for this insertion + emit_attribute_arg_key_x86_64(ctx, index, entry); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown attribute-arg hash } ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final attribute-arg array as the result + ctx.emitter.instruction("pop rax"); // restore the final attribute-arg hash as the result Ok(()) } +/// Materializes the hash key for one attribute argument on AArch64. +fn emit_attribute_arg_key_aarch64( + ctx: &mut FunctionContext<'_>, + index: usize, + entry: &AttrArgEntry, +) { + match &entry.key { + Some(AttrKey::Str(name)) => { + let (label, len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); + } + Some(AttrKey::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, "x1", *value); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + None => { + abi::emit_load_int_immediate(ctx.emitter, "x1", index as i64); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + } +} + +/// Materializes the hash key for one attribute argument on x86_64. +fn emit_attribute_arg_key_x86_64( + ctx: &mut FunctionContext<'_>, + index: usize, + entry: &AttrArgEntry, +) { + match &entry.key { + Some(AttrKey::Str(name)) => { + let (label, len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); + } + Some(AttrKey::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", *value); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + None => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", index as i64); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + } +} + +/// Allocates a Mixed-valued PHP hash with room for captured attribute args. +fn allocate_mixed_hash(ctx: &mut FunctionContext<'_>, capacity: usize) { + let capacity = (capacity * 2).max(16); + let value_tag = crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", value_tag); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", value_tag); + } + } + abi::emit_call_label(ctx.emitter, "__rt_hash_new"); +} + /// Boxes one captured attribute argument into a Mixed cell, leaving the cell in /// the integer result register. A nested array is built recursively and boxed /// with the array tag; scalars dispatch to the per-architecture boxer. fn emit_box_arg(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue) -> Result<()> { if let AttrArgValue::Array(entries) = arg { // Build the nested array (leaves it in the result reg), then box the - // array pointer as a Mixed cell with the array runtime tag. The parent - // array being filled stays parked on the temporary stack across this. + // pointer as a Mixed cell. emit_mixed_array allocates a hash, so the + // cell must carry the assoc tag or Mixed readers dispatch the payload + // to the indexed-array helpers. The parent array being filled stays + // parked on the temporary stack across this. emit_mixed_array(ctx, entries)?; crate::codegen::emit_box_current_value_as_mixed( ctx.emitter, - &PhpType::Array(Box::new(PhpType::Mixed)), + &PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, ); return Ok(()); } @@ -440,7 +614,8 @@ fn emit_box_scalar_arg_aarch64(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue } AttrArgValue::Bool(value) => { ctx.emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean payload - ctx.emitter.instruction(&format!("mov x1, #{}", *value as u64)); // pass the captured boolean as the mixed low word + ctx.emitter + .instruction(&format!("mov x1, #{}", *value as u64)); // pass the captured boolean as the mixed low word ctx.emitter.instruction("mov x2, xzr"); // boolean mixed payloads do not use the high word } AttrArgValue::Str(value) => { @@ -487,7 +662,8 @@ fn emit_box_scalar_arg_x86_64(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue) } AttrArgValue::Bool(value) => { ctx.emitter.instruction("mov rax, 3"); // runtime tag 3 = boolean payload - ctx.emitter.instruction(&format!("mov rdi, {}", *value as u64)); // pass the captured boolean as the mixed low word + ctx.emitter + .instruction(&format!("mov rdi, {}", *value as u64)); // pass the captured boolean as the mixed low word ctx.emitter.instruction("xor rsi, rsi"); // boolean mixed payloads do not use the high word } AttrArgValue::Str(value) => { diff --git a/src/codegen/lower_inst/builtins/class_relations.rs b/src/codegen/lower_inst/builtins/class_relations.rs index ef19152104..b68571bb7e 100644 --- a/src/codegen/lower_inst/builtins/class_relations.rs +++ b/src/codegen/lower_inst/builtins/class_relations.rs @@ -19,7 +19,7 @@ use crate::names::php_symbol_key; use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::super::context::FunctionContext; -use super::{expect_operand, store_if_result}; +use super::{expect_operand, has_eval_context, lower_eval_class_relation, store_if_result}; enum ClassLikeTarget { Class(String), @@ -35,7 +35,12 @@ pub(crate) fn lower_class_relation( name: &str, ) -> Result<()> { super::ensure_arg_count_between(inst, name, 1, 2)?; - let target = resolve_relation_target(ctx, expect_operand(inst, 0)?)?; + let target_value = expect_operand(inst, 0)?; + if has_eval_context(ctx) { + return lower_eval_class_relation(ctx, inst, target_value, name); + } + + let target = resolve_relation_target(ctx, target_value)?; if matches!(target, ClassLikeTarget::Unknown) { emit_boxed_bool(ctx, false); return store_if_result(ctx, inst); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs new file mode 100644 index 0000000000..ed711e9350 --- /dev/null +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -0,0 +1,10880 @@ +//! Purpose: +//! Lowers PHP `eval()` calls to the optional libelephc-magician bridge ABI. +//! Materializes a persistent per-function eval scope handle, flushes visible +//! locals into that scope, calls the bridge, and reloads synchronized locals +//! from boxed Mixed cells after the call returns. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::lower_builtin_call()`. +//! +//! Key details: +//! - Argument evaluation has already happened in PHP source order during EIR +//! lowering; this module only materializes the bridge ABI call. +//! - The bridge is target-mangled like other C staticlib symbols. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; + +use crate::codegen::eval_ref_arg_helpers::eval_signature_ref_params_supported; +use crate::codegen::platform::Arch; +use crate::codegen::runtime_callable_invoker::RuntimeCallableInvoker; +use crate::codegen::{ + abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, +}; +use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Module, Op, ValueId}; +use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; +use crate::parser::ast::{ + BinOp, Expr, ExprKind, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, +}; +use crate::types::{ + is_php_integer_array_key, AttrArgEntry, AttrArgValue, AttrKey, ClassInfo, FunctionSig, + InterfaceInfo, PhpType, PropertyHookContract, +}; + +use super::super::super::context::FunctionContext; +use super::super::{ + expect_data, expect_global_name, expect_operand, function_signature_from_eir, predicates, + store_if_result, +}; + +const EVAL_STATUS_PARSE_ERROR: i64 = 1; +const EVAL_STATUS_UNCAUGHT_THROWABLE: i64 = 3; +const EVAL_STATUS_UNSUPPORTED: i64 = 4; +const EVAL_PARSE_ERROR_MESSAGE: &str = "Parse error: eval() fragment is invalid\n"; +const EVAL_UNSUPPORTED_MESSAGE: &str = + "Fatal error: eval() fragment uses an unsupported construct\n"; +const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; +const EVAL_STACK_BYTES: usize = 96; +const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; +const EVAL_RESULT_ERROR_OFFSET: usize = 16; +const EVAL_CONTEXT_HANDLE_OFFSET: usize = 24; +const EVAL_SCOPE_HANDLE_OFFSET: usize = 32; +const EVAL_TEMP_CELL_OFFSET: usize = 40; +const EVAL_CODE_PTR_OFFSET: usize = 48; +const EVAL_CODE_LEN_OFFSET: usize = 56; +const EVAL_GLOBAL_SCOPE_HANDLE_OFFSET: usize = 64; +const EVAL_CALLED_CLASS_PTR_OFFSET: usize = 72; +const EVAL_CALLED_CLASS_LEN_OFFSET: usize = 80; +const EVAL_LOCAL_SCALAR_SLOT_BYTES: usize = 32; +const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; +const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; +const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; +const EVAL_CLASS_LOOKUP_GET_PARENT_CLASS: i64 = 1; +const EVAL_MEMBER_LOOKUP_METHOD_EXISTS: i64 = 0; +const EVAL_MEMBER_LOOKUP_PROPERTY_EXISTS: i64 = 1; +const EVAL_CLASS_RELATION_IMPLEMENTS: i64 = 0; +const EVAL_CLASS_RELATION_PARENTS: i64 = 1; +const EVAL_CLASS_RELATION_USES: i64 = 2; +const EVAL_CALLABLE_ARG_ARRAY_OFFSET: usize = EVAL_CODE_PTR_OFFSET; +const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; +const NATIVE_DEFAULT_NULL: i64 = 0; +const NATIVE_DEFAULT_BOOL: i64 = 1; +const NATIVE_DEFAULT_INT: i64 = 2; +const NATIVE_DEFAULT_FLOAT: i64 = 3; +const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; +const NATIVE_PROPERTY_REQUIRES_GET: i64 = 1; +const NATIVE_PROPERTY_REQUIRES_SET: i64 = 2; +const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; +const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; +const NATIVE_MEMBER_ATTRIBUTE_CLASS: u8 = 3; +const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; +const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; +const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; +const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; +const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; +const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; +const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; +const MAX_NATIVE_DEFAULT_CONSTANT_DEPTH: usize = 16; + +/// Local slot metadata needed for conservative eval scope synchronization. +#[derive(Clone)] +struct EvalSyncLocal { + name: String, + slot: LocalSlotId, + ty: PhpType, +} + +/// Program-global metadata synchronized with eval `global` aliases. +#[derive(Clone)] +struct EvalSyncGlobal { + name: String, + ty: PhpType, +} + +/// Source location for one direct Mixed parameter passed into scope-read eval AOT. +enum EvalScopeReadParamSource { + Local(EvalSyncLocal), + Null, +} + +/// Local-to-global alias metadata inherited by eval from the caller function scope. +#[derive(Clone)] +struct EvalGlobalAlias { + name: String, + global_name: String, +} + +/// Straight-line literal eval instruction that can be emitted without the interpreter. +enum EvalLiteralAotInst { + Echo(EvalLiteralAotExpr), + Store { + name: String, + value: EvalLiteralAotExpr, + }, + Return(EvalLiteralAotExpr), +} + +/// Boxed-Mixed expression accepted by the literal eval AOT path. +enum EvalLiteralAotExpr { + Scalar(EvalLiteralAotScalar), + LoadVar(String), + Binary { + op: EvalLiteralAotBinaryOp, + left: Box, + right: Box, + }, +} + +/// Runtime-backed binary operation accepted by the literal eval AOT path. +enum EvalLiteralAotBinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + Concat, +} + +/// Scalar value accepted by the first conservative literal-eval AOT subset. +#[derive(Clone)] +enum EvalLiteralAotScalar { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +/// Parsed and validated literal eval fragment ready for direct code emission. +enum EvalLiteralAotProgram { + Boxed(EvalLiteralBoxedAotProgram), + LocalScalar(EvalLocalScalarAotProgram), +} + +/// Parsed boxed-Mixed literal eval fragment for scope-oriented direct emission. +struct EvalLiteralBoxedAotProgram { + instructions: Vec, + scope_reads: BTreeSet, + scope_writes: BTreeSet, + has_scope_writes: bool, + has_scope_access: bool, +} + +/// Local scalar eval statement emitted as stack-slot native code before final scope flush. +enum EvalLocalScalarStmt { + Noop, + Echo(EvalLocalScalarExpr), + Store { + name: String, + value: EvalLocalScalarExpr, + }, + If { + branches: Vec<(EvalLocalScalarExpr, Vec)>, + else_body: Vec, + }, + While { + condition: EvalLocalScalarExpr, + body: Vec, + }, + DoWhile { + body: Vec, + condition: EvalLocalScalarExpr, + }, + For { + init: Option>, + condition: Option, + update: Option>, + body: Vec, + }, + Switch { + subject: EvalLocalScalarExpr, + cases: Vec<(Vec, Vec)>, + default: Vec, + default_index: Option, + }, + Break(usize), + Continue(usize), + Return(Option), +} + +/// Local scalar expression accepted by the control-flow AOT subset. +struct EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind, + ty: EvalLocalScalarType, +} + +/// Expression payload for the local scalar AOT subset. +enum EvalLocalScalarExprKind { + Null, + Int(i64), + Float(f64), + Bool(bool), + String(String), + LoadVar(String), + Isset(Vec), + EmptyVar(String), + Negate(Box), + BitNot(Box), + Not(Box), + Print(Box), + Ternary { + condition: Box, + then_expr: Box, + else_expr: Box, + }, + Binary { + op: EvalLocalScalarBinaryOp, + left: Box, + right: Box, + }, + StaticFunctionCall { + name: String, + args: Vec, + }, +} + +/// Runtime scalar type tracked by the local eval AOT subset. +#[derive(Clone, Copy, PartialEq, Eq)] +enum EvalLocalScalarType { + Null, + Int, + Float, + Bool, + String, +} + +/// Binary operations accepted by the local scalar AOT subset. +enum EvalLocalScalarBinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, + Lt, + Gt, + LtEq, + GtEq, + Eq, + NotEq, + And, + Or, + Concat, +} + +/// Parsed and analyzed local-scalar eval fragment ready for native control-flow emission. +struct EvalLocalScalarAotProgram { + statements: Vec, + locals: BTreeMap, + local_types: BTreeMap, + scratch_slots: usize, +} + +/// Mutable compile-time state for local scalar eval eligibility analysis. +struct EvalLocalScalarAnalysis { + locals: BTreeMap, + local_types: BTreeMap, + max_scratch_slots: usize, +} + +/// Break and continue targets for one nested local AOT loop. +struct EvalLocalLoopLabels { + break_label: String, + continue_label: String, +} + +/// Selects how local scalar AOT boxes eval return values. +#[derive(Clone, Copy)] +enum EvalLocalScalarBoxing { + EvalRuntime, + CoreRuntime, +} + +/// A module-local function that can be registered with the eval context. +struct EvalNativeFunctionRegistration { + name: String, + signature: FunctionSig, + bridge_supported: bool, +} + +/// A module-local method signature that can be registered with the eval context. +struct EvalNativeMethodRegistration { + class_name: String, + method_name: String, + is_static: bool, + signature: FunctionSig, + bridge_supported: bool, +} + +/// A module-local constructor signature that can be registered with the eval context. +struct EvalNativeConstructorRegistration { + class_name: String, + signature: FunctionSig, + bridge_supported: bool, +} + +/// Static metadata used while converting AOT defaults into eval bridge values. +struct EvalNativeDefaultContext<'a> { + module: &'a Module, + current_class: Option<&'a str>, +} + +impl<'a> EvalNativeDefaultContext<'a> { + /// Builds a default-materialization context for global function defaults. + fn global(module: &'a Module) -> Self { + Self { + module, + current_class: None, + } + } + + /// Builds a default-materialization context for class-like member defaults. + fn for_class(module: &'a Module, class_name: &'a str) -> Self { + Self { + module, + current_class: Some(class_name), + } + } +} + +/// A module-local property type that can be registered with the eval context. +struct EvalNativePropertyTypeRegistration { + class_name: String, + property_name: String, + type_spec: String, +} + +/// A module-local interface property contract that can be registered with the eval context. +struct EvalNativeInterfacePropertyRegistration { + interface_name: String, + declaring_interface_name: String, + property_name: String, + type_spec: String, + requires_get: bool, + requires_set: bool, +} + +/// A module-local abstract class property contract that can be registered with the eval context. +struct EvalNativeAbstractPropertyRegistration { + class_name: String, + declaring_class_name: String, + property_name: String, + type_spec: String, + requires_get: bool, + requires_set: bool, +} + +/// A module-local property default that can be registered with the eval context. +struct EvalNativePropertyDefaultRegistration { + class_name: String, + property_name: String, + default: EvalNativeCallableDefault, +} + +/// A module-local member attribute that can be registered with the eval context. +struct EvalNativeMemberAttributeRegistration { + owner_kind: u8, + class_name: String, + member_name: String, + attribute_name: String, + attribute_args: Option>, +} + +/// Native callable default that can be registered with libelephc-magician. +enum EvalNativeCallableDefault { + Scalar { + kind: i64, + payload: i64, + }, + String(String), + Array(Vec), + Object { + class_name: String, + args: Vec, + }, +} + +/// Array element metadata for a native callable default registered with eval. +struct EvalNativeCallableArrayDefaultElement { + key: Option, + default: EvalNativeCallableDefault, +} + +/// Static array key metadata for a native callable default registered with eval. +enum EvalNativeCallableArrayDefaultKey { + Int(i64), + String(String), +} + +/// Constructor argument metadata for an object-valued native callable default. +struct EvalNativeCallableObjectDefaultArg { + name: Option, + default: EvalNativeCallableDefault, +} + +/// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. +pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count(inst, "eval", 1)?; + if let Some(fragment) = eval_literal_fragment(ctx, inst)? { + if crate::eval_aot::literal_fragment_direct_local_read_write_writes(&fragment).is_none() + && lower_eval_literal_eir_function(ctx, inst, &fragment)? + { + return Ok(()); + } + } + if let Some(program) = eval_literal_aot_program(ctx, inst)? { + if lower_eval_literal_aot(ctx, inst, &program)? { + return Ok(()); + } + } + emit_eval_literal_aot_marker(ctx, inst)?; + let code = expect_operand(inst, 0)?; + let ty = ctx.load_value_to_result(code)?.codegen_repr(); + if ty != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "eval() argument lowering for PHP type {:?}", + ty + ))); + } + + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + save_eval_code_string(ctx); + ensure_eval_context(ctx)?; + set_eval_call_site(ctx, inst); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let global_aliases = eval_global_aliases(ctx); + flush_eval_scope_locals(ctx, &sync_locals)?; + flush_eval_global_scope(ctx, &sync_globals)?; + mark_eval_scope_global_aliases(ctx, &global_aliases); + set_eval_context_global_scope(ctx); + let pushed_class_scope = push_eval_context_class_scope(ctx)?; + load_eval_context_to_arg(ctx, 0); + load_eval_scope_to_arg(ctx, 1); + move_saved_eval_code_to_eval_args(ctx); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); + abi::emit_call_label(ctx.emitter, &symbol); + pop_eval_context_class_scope(ctx, pushed_class_scope); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &sync_locals)?; + reload_eval_global_scope(ctx, &sync_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Calls a pre-lowered internal EIR function for no-scope literal eval fragments. +fn lower_eval_literal_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + fragment: &str, +) -> Result { + let function_name = crate::eval_aot::eir_function_name(fragment); + if let Some(callee) = ctx.callable_function_by_name(&function_name) { + if callee.params.is_empty() && callee.return_php_type.codegen_repr() == PhpType::Mixed { + ctx.emitter + .comment("eval literal AOT compiled EIR function"); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_call_label(ctx.emitter, &function_symbol(&function_name)); + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + store_if_result(ctx, inst)?; + return Ok(true); + } + } + lower_eval_literal_scope_read_eir_function(ctx, inst, fragment) +} + +/// Calls a pre-lowered internal EIR function that reads from the eval scope. +fn lower_eval_literal_scope_read_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + fragment: &str, +) -> Result { + let function_name = crate::eval_aot::eir_scope_read_function_name(fragment); + let Some(callee) = ctx.callable_function_by_name(&function_name) else { + return Ok(false); + }; + let param_types = callee + .params + .iter() + .map(|param| param.php_type.codegen_repr()) + .collect::>(); + let return_type = callee.return_php_type.codegen_repr(); + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_codegen(ctx, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_codegen(ctx, receiver, method, args) + }, + ); + if plan.uses_scope_read_params() { + return lower_eval_literal_scope_read_param_eir_function( + ctx, + inst, + &function_name, + ¶m_types, + &return_type, + plan.reads(), + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ); + } + if !eval_scope_read_constraints_supported( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) { + return Ok(false); + } + if param_types.len() != 1 || return_type != PhpType::Mixed { + return Ok(false); + } + ctx.emitter + .comment("eval literal AOT compiled EIR function with scope reads"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_scope(ctx)?; + let read_names = plan.reads().clone(); + let write_names = plan.writes().clone(); + let mut flush_names = read_names.clone(); + flush_names.extend(write_names.iter().cloned()); + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let flush_locals = filter_eval_sync_locals_by_name(sync_locals.clone(), &flush_names); + let flush_globals = filter_eval_sync_globals_by_name(sync_globals.clone(), &flush_names); + let reload_locals = filter_eval_sync_locals_by_name(sync_locals, &write_names); + let reload_globals = filter_eval_sync_globals_by_name(sync_globals, &write_names); + flush_eval_scope_locals(ctx, &flush_locals)?; + flush_eval_globals_to_local_scope(ctx, &flush_globals); + load_eval_scope_to_arg(ctx, 0); + abi::emit_call_label(ctx.emitter, &function_symbol(&function_name)); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &reload_locals)?; + reload_eval_globals_from_local_scope(ctx, &reload_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Calls a read-only scope eval AOT function by passing direct boxed Mixed params. +fn lower_eval_literal_scope_read_param_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + function_name: &str, + param_types: &[PhpType], + return_type: &PhpType, + read_names: &BTreeSet, + array_read_constraints: &BTreeSet, + assoc_array_read_constraints: &BTreeSet, + float_predicate_read_constraints: &BTreeSet, +) -> Result { + if param_types.len() != read_names.len() + || param_types + .iter() + .any(|ty| ty.codegen_repr() != PhpType::Mixed) + || return_type.codegen_repr() != PhpType::Mixed + { + return Ok(false); + } + if !eval_scope_read_constraints_supported( + ctx, + array_read_constraints, + assoc_array_read_constraints, + float_predicate_read_constraints, + ) { + return Ok(false); + } + let Some(param_sources) = eval_scope_read_param_sources(ctx, read_names) else { + return Ok(false); + }; + ctx.emitter + .comment("eval literal AOT compiled EIR function with direct read params"); + for source in ¶m_sources { + emit_eval_scope_read_param_source(ctx, source)?; + abi::emit_push_result_value(ctx.emitter, &PhpType::Mixed); + } + let assignments = + abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, param_types, 0); + let overflow_bytes = abi::materialize_outgoing_args(ctx.emitter, &assignments); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, overflow_bytes); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_call_label(ctx.emitter, &function_symbol(function_name)); + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(ctx.emitter, overflow_bytes); + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Resolves read-only eval variables to direct local values or undefined null. +fn eval_scope_read_param_sources( + ctx: &FunctionContext<'_>, + read_names: &BTreeSet, +) -> Option> { + let sync_locals = eval_sync_locals(ctx); + read_names + .iter() + .map(|name| { + if let Some(local) = sync_locals.iter().find(|local| local.name == *name) { + return Some(EvalScopeReadParamSource::Local(local.clone())); + } + if ctx.function.locals.iter().any(|local| { + local.name.as_deref() == Some(name.as_str()) + && local.kind == LocalKind::PhpLocal + && !local_uses_eval_global_sync(ctx, local.name.as_deref()) + && local.php_type.codegen_repr() == PhpType::Void + }) { + return Some(EvalScopeReadParamSource::Null); + } + let has_unsupported_local = ctx + .function + .locals + .iter() + .any(|local| local.name.as_deref() == Some(name.as_str())); + (!has_unsupported_local).then_some(EvalScopeReadParamSource::Null) + }) + .collect() +} + +/// Returns true when constrained direct read params have compatible local sources. +fn eval_scope_read_constraints_supported( + ctx: &FunctionContext<'_>, + array_read_constraints: &BTreeSet, + assoc_array_read_constraints: &BTreeSet, + float_predicate_read_constraints: &BTreeSet, +) -> bool { + let sync_locals = eval_sync_locals(ctx); + array_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_array_param_type_supported(&local.ty)) + }) && assoc_array_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_assoc_array_param_type_supported(&local.ty)) + }) && float_predicate_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_float_predicate_param_type_supported(&local.ty)) + }) +} + +/// Returns true when a direct read-param source has array-only semantics. +fn eval_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} + +/// Returns true when a direct read-param source has associative-array-only semantics. +fn eval_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) +} + +/// Returns true when a direct read-param source can feed IEEE float predicates. +fn eval_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) +} + +/// Emits one direct read-param value as a boxed Mixed result. +fn emit_eval_scope_read_param_source( + ctx: &mut FunctionContext<'_>, + source: &EvalScopeReadParamSource, +) -> Result<()> { + match source { + EvalScopeReadParamSource::Local(local) => { + let ty = ctx.load_local_to_result(local.slot)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + } + EvalScopeReadParamSource::Null => emit_eval_local_scalar_core_null_cell(ctx), + } + Ok(()) +} + +/// Returns true when a static function call matches the EIR eval AOT codegen subset. +fn eval_literal_static_function_supported_by_codegen( + ctx: &FunctionContext<'_>, + name: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(function) = ctx + .module + .functions + .iter() + .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) + else { + return false; + }; + let signature = function_signature_from_eir(function); + crate::eval_aot::static_function_signature_supported(&signature, args) +} + +/// Returns true when a static method call matches the EIR eval AOT codegen subset. +fn eval_literal_static_method_supported_by_codegen( + ctx: &FunctionContext<'_>, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + let class_name = class_name.as_str().trim_start_matches('\\'); + let method_key = php_symbol_key(method); + let Some(receiver_info) = ctx.module.class_infos.get(class_name) else { + return false; + }; + if receiver_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; + } + let impl_class = receiver_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + let Some(signature) = ctx + .module + .class_infos + .get(impl_class) + .and_then(|class_info| class_info.static_methods.get(&method_key)) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Lowers an EIR eval-scope read for a static variable name. +pub(super) fn lower_eval_scope_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval scope get", 1)?; + let scope = expect_operand(inst, 0)?; + let name = eval_scope_instruction_name(ctx, inst)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + load_eval_scope_operand_to_arg(ctx, scope, 0)?; + emit_eval_scope_get_for_loaded_scope(ctx, &name, 0, 8); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers an EIR eval-scope write for a static variable name. +pub(super) fn lower_eval_scope_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval scope set", 2)?; + let scope = expect_operand(inst, 0)?; + let value = expect_operand(inst, 1)?; + let name = eval_scope_instruction_name(ctx, inst)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + let value_ty = ctx.load_value_to_result(value)?.codegen_repr(); + let flags = if matches!(value_ty, PhpType::Mixed | PhpType::Union(_)) { + abi::emit_call_label(ctx.emitter, "__rt_incref"); + EVAL_SCOPE_FLAG_OWNED + } else { + emit_box_current_value_as_mixed(ctx.emitter, &value_ty); + scope_set_flags_for_type(&value_ty) + }; + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_scope_operand_to_arg(ctx, scope, 0)?; + emit_eval_scope_set_for_loaded_scope(ctx, &name, flags); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + Ok(()) +} + +/// Returns the static PHP variable name attached to an eval-scope instruction. +fn eval_scope_instruction_name(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result { + let data = expect_global_name(inst)?; + ctx.module + .data + .global_names + .get(data.as_raw() as usize) + .cloned() + .ok_or_else(|| CodegenIrError::missing_entry("global name", data.as_raw())) +} + +/// Loads an eval-scope handle operand into the requested ABI argument register. +fn load_eval_scope_operand_to_arg( + ctx: &mut FunctionContext<'_>, + scope: ValueId, + arg_index: usize, +) -> Result<()> { + let arg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + let ty = ctx.load_value_to_reg(scope, arg)?.codegen_repr(); + if ty == PhpType::Int { + return Ok(()); + } + Err(CodegenIrError::unsupported(format!( + "eval scope handle operand for PHP type {:?}", + ty + ))) +} + +/// Calls `__elephc_eval_scope_get` using an already-loaded scope handle arg. +fn emit_eval_scope_get_for_loaded_scope( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_set` using an already-loaded scope handle arg. +fn emit_eval_scope_set_for_loaded_scope(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Parses an `EvalLiteralCall` payload into the conservative scalar AOT subset. +fn eval_literal_aot_program( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let Some(fragment) = eval_literal_fragment(ctx, inst)? else { + return Ok(None); + }; + Ok(parse_eval_literal_aot_program(&fragment)) +} + +/// Returns the literal fragment attached to an `EvalLiteralCall`, if this is one. +fn eval_literal_fragment(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result> { + if inst.op != Op::EvalLiteralCall { + return Ok(None); + } + let Some(Immediate::Data(data)) = inst.immediate else { + return Ok(None); + }; + let fragment = ctx + .module + .data + .strings + .get(data.as_raw() as usize) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw()))?; + Ok(Some(fragment.clone())) +} + +/// Parses a PHP eval fragment and accepts only side-effect-free scalar statements plus scalar stores. +fn parse_eval_literal_aot_program(fragment: &str) -> Option { + let program = crate::eval_aot::parse_literal_fragment(fragment)?; + if let Some(local_scalar) = parse_eval_local_scalar_aot_program(&program) { + return Some(EvalLiteralAotProgram::LocalScalar(local_scalar)); + } + if let Some(boxed) = parse_eval_literal_boxed_aot_program(&program) { + return Some(EvalLiteralAotProgram::Boxed(boxed)); + } + None +} + +/// Parses a PHP eval fragment into the boxed-Mixed AOT subset. +fn parse_eval_literal_boxed_aot_program(program: &[Stmt]) -> Option { + let mut instructions = Vec::new(); + let mut terminated = false; + for stmt in program { + if terminated { + break; + } + terminated = push_eval_literal_aot_stmt(stmt, &mut instructions)?; + } + if !terminated { + instructions.push(EvalLiteralAotInst::Return(EvalLiteralAotExpr::Scalar( + EvalLiteralAotScalar::Null, + ))); + } + let has_scope_writes = instructions + .iter() + .any(|inst| matches!(inst, EvalLiteralAotInst::Store { .. })); + let has_scope_access = instructions + .iter() + .any(EvalLiteralAotInst::has_scope_access); + let scope_reads = instructions + .iter() + .flat_map(EvalLiteralAotInst::scope_reads) + .collect(); + let scope_writes = instructions + .iter() + .filter_map(EvalLiteralAotInst::scope_write) + .collect(); + Some(EvalLiteralBoxedAotProgram { + instructions, + scope_reads, + scope_writes, + has_scope_writes, + has_scope_access, + }) +} + +/// Parses a PHP eval fragment into the local int/bool control-flow AOT subset. +fn parse_eval_local_scalar_aot_program(program: &[Stmt]) -> Option { + let mut analysis = EvalLocalScalarAnalysis { + locals: BTreeMap::new(), + local_types: BTreeMap::new(), + max_scratch_slots: 0, + }; + let mut assigned = BTreeSet::new(); + let statements = parse_eval_local_scalar_block(program, &mut analysis, &mut assigned, 0, true)?; + Some(EvalLocalScalarAotProgram { + statements, + locals: analysis.locals, + local_types: analysis.local_types, + scratch_slots: analysis.max_scratch_slots.max(1), + }) +} + +/// Parses a statement block for the local scalar AOT subset. +fn parse_eval_local_scalar_block( + statements: &[Stmt], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option> { + let mut out = Vec::new(); + let mut terminated = false; + for stmt in statements { + if terminated { + break; + } + let (local_stmt, terminates_block) = + parse_eval_local_scalar_stmt(stmt, analysis, assigned, loop_depth, allow_type_changes)?; + out.push(local_stmt); + terminated = terminates_block; + } + Some(out) +} + +/// Parses one statement for the local scalar AOT subset. +fn parse_eval_local_scalar_stmt( + stmt: &Stmt, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option<(EvalLocalScalarStmt, bool)> { + match &stmt.kind { + StmtKind::Echo(expr) => { + let expr = eval_local_scalar_echo_expr(expr, analysis, assigned)?; + Some((EvalLocalScalarStmt::Echo(expr), false)) + } + StmtKind::Assign { name, value } => { + let value = eval_local_scalar_value_expr(value, analysis, assigned)?; + analysis.ensure_local(name, value.ty, allow_type_changes)?; + assigned.insert(name.clone()); + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + let mut branches = Vec::new(); + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let mut then_assigned = assigned.clone(); + let then_body = parse_eval_local_scalar_block( + then_body, + analysis, + &mut then_assigned, + loop_depth, + false, + )?; + branches.push((condition, then_body)); + for (elseif_condition, elseif_body) in elseif_clauses { + let condition = + eval_local_scalar_condition_expr(elseif_condition, analysis, assigned)?; + let mut elseif_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + elseif_body, + analysis, + &mut elseif_assigned, + loop_depth, + false, + )?; + branches.push((condition, body)); + } + let else_body = if let Some(else_body) = else_body { + let mut else_assigned = assigned.clone(); + parse_eval_local_scalar_block( + else_body, + analysis, + &mut else_assigned, + loop_depth, + false, + )? + } else { + Vec::new() + }; + Some(( + EvalLocalScalarStmt::If { + branches, + else_body, + }, + false, + )) + } + StmtKind::While { condition, body } => { + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + Some((EvalLocalScalarStmt::While { condition, body }, false)) + } + StmtKind::DoWhile { body, condition } => { + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + let condition = eval_local_scalar_condition_expr(condition, analysis, &body_assigned)?; + Some((EvalLocalScalarStmt::DoWhile { body, condition }, false)) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + let init = if let Some(init) = init.as_deref() { + Some(Box::new(parse_eval_local_scalar_for_clause( + init, + analysis, + assigned, + allow_type_changes, + )?)) + } else { + None + }; + let condition = match condition { + Some(condition) => Some(eval_local_scalar_condition_expr( + condition, analysis, assigned, + )?), + None => None, + }; + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + let update = if let Some(update) = update.as_deref() { + Some(Box::new(parse_eval_local_scalar_for_clause( + update, + analysis, + &mut body_assigned, + false, + )?)) + } else { + None + }; + Some(( + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + }, + false, + )) + } + StmtKind::Switch { + subject, + cases, + default, + } => parse_eval_local_scalar_switch_stmt( + subject, + cases, + default.as_deref(), + analysis, + assigned, + loop_depth, + ) + .map(|stmt| (stmt, false)), + StmtKind::Break(level) if *level > 0 && *level <= loop_depth => { + Some((EvalLocalScalarStmt::Break(*level), true)) + } + StmtKind::Continue(level) if *level > 0 && *level <= loop_depth => { + Some((EvalLocalScalarStmt::Continue(*level), true)) + } + StmtKind::Return(Some(expr)) => { + let expr = eval_local_scalar_value_expr(expr, analysis, assigned)?; + Some((EvalLocalScalarStmt::Return(Some(expr)), true)) + } + StmtKind::Return(None) => Some((EvalLocalScalarStmt::Return(None), true)), + StmtKind::ExprStmt(expr) => { + parse_eval_local_scalar_expr_stmt(expr, analysis, assigned, allow_type_changes) + } + _ => None, + } +} + +/// Parses a switch statement accepted by the local scalar AOT subset. +fn parse_eval_local_scalar_switch_stmt( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, +) -> Option { + let default_index = eval_local_scalar_switch_default_index(cases, default)?; + let subject = eval_local_scalar_condition_expr(subject, analysis, assigned)?; + let mut parsed_cases = Vec::new(); + for (conditions, body) in cases { + let conditions = conditions + .iter() + .map(|condition| eval_local_scalar_condition_expr(condition, analysis, assigned)) + .collect::>>()?; + conditions + .iter() + .all(|condition| condition.ty == subject.ty) + .then_some(())?; + let mut case_assigned = assigned.clone(); + let body = parse_eval_local_scalar_switch_block( + body, + analysis, + &mut case_assigned, + loop_depth + 1, + false, + )?; + parsed_cases.push((conditions, body)); + } + let default = if let Some(default) = default { + let mut default_assigned = assigned.clone(); + parse_eval_local_scalar_switch_block( + default, + analysis, + &mut default_assigned, + loop_depth + 1, + false, + )? + } else { + Vec::new() + }; + let subject_slots = 1 + subject.scratch_slots(); + let condition_slots = parsed_cases + .iter() + .flat_map(|(conditions, _)| conditions) + .map(|condition| 1 + condition.scratch_slots()) + .max() + .unwrap_or(1); + analysis.max_scratch_slots = analysis + .max_scratch_slots + .max(subject_slots) + .max(condition_slots); + Some(EvalLocalScalarStmt::Switch { + subject, + cases: parsed_cases, + default, + default_index, + }) +} + +/// Parses one switch case/default body for the local scalar AOT subset. +fn parse_eval_local_scalar_switch_block( + statements: &[Stmt], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option> { + parse_eval_local_scalar_block( + statements, + analysis, + assigned, + loop_depth, + allow_type_changes, + ) +} + +/// Returns the source-order insertion point for a switch default body. +fn eval_local_scalar_switch_default_index( + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, +) -> Option> { + let Some(default) = default else { + return Some(None); + }; + if cases.is_empty() { + return Some(Some(0)); + } + let Some(default_start) = default.first().map(|stmt| stmt.span) else { + return None; + }; + if default_start == crate::span::Span::dummy() { + return None; + } + let mut default_index = 0; + for (conditions, _) in cases { + let case_start = conditions.first()?.span; + if case_start == crate::span::Span::dummy() { + return None; + } + if eval_local_scalar_span_is_before(case_start, default_start) { + default_index += 1; + } + } + Some(Some(default_index)) +} + +/// Returns true when `span` appears before `pivot` in the same eval fragment. +fn eval_local_scalar_span_is_before(span: crate::span::Span, pivot: crate::span::Span) -> bool { + span.line < pivot.line || (span.line == pivot.line && span.col < pivot.col) +} + +/// Parses one inline `for` init/update statement for local scalar AOT. +fn parse_eval_local_scalar_for_clause( + stmt: &Stmt, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + allow_type_changes: bool, +) -> Option { + match &stmt.kind { + StmtKind::Assign { .. } | StmtKind::ExprStmt(_) => { + let (stmt, terminates) = + parse_eval_local_scalar_stmt(stmt, analysis, assigned, 0, allow_type_changes)?; + (!terminates).then_some(stmt) + } + _ => None, + } +} + +/// Parses expression statements accepted by the local scalar AOT subset. +fn parse_eval_local_scalar_expr_stmt( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + allow_type_changes: bool, +) -> Option<(EvalLocalScalarStmt, bool)> { + match &expr.kind { + ExprKind::Print(inner) => { + let expr = eval_local_scalar_echo_expr(inner, analysis, assigned)?; + Some((EvalLocalScalarStmt::Echo(expr), false)) + } + ExprKind::Assignment { + target, + value, + prelude, + conditional_value_temp, + .. + } if prelude.is_empty() && conditional_value_temp.is_none() => { + let ExprKind::Variable(name) = &target.kind else { + return None; + }; + let value = eval_local_scalar_value_expr(value, analysis, assigned)?; + analysis.ensure_local(name, value.ty, allow_type_changes)?; + assigned.insert(name.clone()); + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + ExprKind::PreIncrement(name) | ExprKind::PostIncrement(name) => { + let value = eval_local_scalar_inc_dec_expr(name, true, analysis, assigned)?; + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + ExprKind::PreDecrement(name) | ExprKind::PostDecrement(name) => { + let value = eval_local_scalar_inc_dec_expr(name, false, analysis, assigned)?; + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + _ => { + let parsed = eval_local_scalar_value_expr(expr, analysis, assigned)?; + // A bare expression statement lowers to Noop, discarding the value. + // Calls and prints inside it would lose their side effects (e.g. a + // dropped `define(...)`), so those fragments must fall back to the + // bridge instead of the local scalar AOT subset. + if eval_local_scalar_expr_has_side_effects(&parsed) { + return None; + } + Some((EvalLocalScalarStmt::Noop, false)) + } + } +} + +/// Returns true when a parsed local-scalar expression carries side effects +/// that a discarded expression statement would lose. +fn eval_local_scalar_expr_has_side_effects(expr: &EvalLocalScalarExpr) -> bool { + match &expr.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => false, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) => eval_local_scalar_expr_has_side_effects(inner), + EvalLocalScalarExprKind::Print(_) => true, + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + eval_local_scalar_expr_has_side_effects(condition) + || eval_local_scalar_expr_has_side_effects(then_expr) + || eval_local_scalar_expr_has_side_effects(else_expr) + } + EvalLocalScalarExprKind::Binary { left, right, .. } => { + eval_local_scalar_expr_has_side_effects(left) + || eval_local_scalar_expr_has_side_effects(right) + } + EvalLocalScalarExprKind::StaticFunctionCall { .. } => true, + } +} + +/// Builds an increment/decrement assignment value for local scalar expression statements. +fn eval_local_scalar_inc_dec_expr( + name: &str, + increment: bool, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + if !assigned.contains(name) || analysis.local_types.get(name) != Some(&EvalLocalScalarType::Int) + { + return None; + } + let op = if increment { + EvalLocalScalarBinaryOp::Add + } else { + EvalLocalScalarBinaryOp::Sub + }; + let expr = EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op, + left: Box::new(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::LoadVar(name.to_string()), + ty: EvalLocalScalarType::Int, + }), + right: Box::new(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(1), + ty: EvalLocalScalarType::Int, + }), + }, + ty: EvalLocalScalarType::Int, + }; + analysis.record_expr(&expr); + Some(expr) +} + +/// Parses a condition expression accepted by local scalar AOT control flow. +fn eval_local_scalar_condition_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let expr = eval_local_scalar_value_expr(expr, analysis, assigned)?; + matches!( + expr.ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) + .then_some(expr) +} + +/// Parses an echo expression accepted by the local scalar AOT subset. +fn eval_local_scalar_echo_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let parsed = match &expr.kind { + ExprKind::StringLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::String(value.clone()), + ty: EvalLocalScalarType::String, + }, + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + let left = eval_local_scalar_echo_expr(left, analysis, assigned)?; + let right = eval_local_scalar_echo_expr(right, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op: EvalLocalScalarBinaryOp::Concat, + left: Box::new(left), + right: Box::new(right), + }, + ty: EvalLocalScalarType::String, + } + } + _ => eval_local_scalar_value_expr(expr, analysis, assigned)?, + }; + analysis.record_expr(&parsed); + Some(parsed) +} + +/// Parses an int/bool value expression accepted by the local scalar AOT subset. +fn eval_local_scalar_value_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let parsed = match &expr.kind { + ExprKind::Null => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Null, + ty: EvalLocalScalarType::Null, + }, + ExprKind::IntLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(*value), + ty: EvalLocalScalarType::Int, + }, + ExprKind::FloatLiteral(value) if value.is_finite() => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Float(*value), + ty: EvalLocalScalarType::Float, + }, + ExprKind::BoolLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Bool(*value), + ty: EvalLocalScalarType::Bool, + }, + ExprKind::StringLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::String(value.clone()), + ty: EvalLocalScalarType::String, + }, + ExprKind::Variable(name) if assigned.contains(name) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::LoadVar(name.clone()), + ty: *analysis.local_types.get(name)?, + }, + ExprKind::Negate(inner) => { + let inner = eval_local_scalar_value_expr(inner, analysis, assigned)?; + if !matches!( + inner.ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Float + ) { + return None; + } + let ty = inner.ty; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Negate(Box::new(inner)), + ty, + } + } + ExprKind::BitNot(inner) => { + let inner = eval_local_scalar_value_expr(inner, analysis, assigned)?; + if inner.ty != EvalLocalScalarType::Int { + return None; + } + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::BitNot(Box::new(inner)), + ty: EvalLocalScalarType::Int, + } + } + ExprKind::Not(inner) => { + let inner = eval_local_scalar_condition_expr(inner, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Not(Box::new(inner)), + ty: EvalLocalScalarType::Bool, + } + } + ExprKind::ErrorSuppress(inner) => eval_local_scalar_value_expr(inner, analysis, assigned)?, + ExprKind::Print(inner) => { + let inner = eval_local_scalar_echo_expr(inner, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Print(Box::new(inner)), + ty: EvalLocalScalarType::Int, + } + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let then_expr = eval_local_scalar_value_expr(then_expr, analysis, assigned)?; + let else_expr = eval_local_scalar_value_expr(else_expr, analysis, assigned)?; + if then_expr.ty != else_expr.ty { + return None; + } + EvalLocalScalarExpr { + ty: then_expr.ty, + kind: EvalLocalScalarExprKind::Ternary { + condition: Box::new(condition), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + }, + } + } + ExprKind::BinaryOp { left, op, right } => { + eval_local_scalar_binary_expr(left, op, right, analysis, assigned)? + } + ExprKind::FunctionCall { name, args } => { + eval_local_scalar_call_expr(name, args, analysis, assigned)? + } + _ => return None, + }; + analysis.record_expr(&parsed); + Some(parsed) +} + +/// Parses a static call accepted by the local scalar AOT subset. +fn eval_local_scalar_call_expr( + name: &crate::names::Name, + args: &[Expr], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let short_name = name.as_str().trim_start_matches('\\'); + if let Some(expr) = eval_local_scalar_construct_call_expr(short_name, args, analysis, assigned) + { + return Some(expr); + } + if let Some(value) = crate::eval_aot::fold_static_builtin_int_call(short_name, args) { + return Some(eval_local_scalar_int_literal(value)); + } + let args = args + .iter() + .map(|arg| eval_local_scalar_value_expr(arg, analysis, assigned)) + .collect::>>()?; + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::StaticFunctionCall { + name: name.as_str().to_string(), + args, + }, + ty: EvalLocalScalarType::Int, + }) +} + +/// Parses local-scalar language constructs that behave like expressions. +fn eval_local_scalar_construct_call_expr( + name: &str, + args: &[Expr], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + if args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_))) + { + return None; + } + match php_symbol_key(name).as_str() { + "isset" => { + if args.is_empty() { + return None; + } + let names = args + .iter() + .map(|arg| match &arg.kind { + ExprKind::Variable(name) if assigned.contains(name) => Some(name.clone()), + _ => None, + }) + .collect::>>()?; + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Isset(names), + ty: EvalLocalScalarType::Bool, + }) + } + "empty" if args.len() == 1 => { + let ExprKind::Variable(name) = &args[0].kind else { + return None; + }; + if !assigned.contains(name) || !analysis.local_types.contains_key(name) { + return None; + } + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::EmptyVar(name.clone()), + ty: EvalLocalScalarType::Bool, + }) + } + _ => None, + } +} + +/// Constructs a local-scalar integer literal expression. +fn eval_local_scalar_int_literal(value: i64) -> EvalLocalScalarExpr { + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(value), + ty: EvalLocalScalarType::Int, + } +} + +/// Parses a binary int/bool expression accepted by the local scalar AOT subset. +fn eval_local_scalar_binary_expr( + left: &Expr, + op: &BinOp, + right: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let left = eval_local_scalar_value_expr(left, analysis, assigned)?; + let right = eval_local_scalar_value_expr(right, analysis, assigned)?; + let (op, ty) = EvalLocalScalarBinaryOp::from_value_binop(op, left.ty, right.ty)?; + let expr = EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + ty, + }; + analysis.record_expr(&expr); + Some(expr) +} + +/// Appends AOT instructions for one eligible eval-fragment statement. +fn push_eval_literal_aot_stmt( + stmt: &Stmt, + instructions: &mut Vec, +) -> Option { + match &stmt.kind { + StmtKind::Echo(expr) => { + instructions.push(EvalLiteralAotInst::Echo(eval_literal_aot_expr(expr)?)); + Some(false) + } + StmtKind::Assign { name, value } => { + instructions.push(EvalLiteralAotInst::Store { + name: name.clone(), + value: eval_literal_aot_expr(value)?, + }); + Some(false) + } + StmtKind::Return(Some(expr)) => { + instructions.push(EvalLiteralAotInst::Return(eval_literal_aot_expr(expr)?)); + Some(true) + } + StmtKind::Return(None) => { + instructions.push(EvalLiteralAotInst::Return(EvalLiteralAotExpr::Scalar( + EvalLiteralAotScalar::Null, + ))); + Some(true) + } + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + instructions.push(EvalLiteralAotInst::Echo(eval_literal_aot_expr(inner)?)); + Some(false) + } + _ => { + let _ = eval_literal_aot_scalar_expr(expr)?; + Some(false) + } + }, + _ => None, + } +} + +/// Builds a boxed-Mixed AOT expression, folding pure scalar expressions when possible. +fn eval_literal_aot_expr(expr: &Expr) -> Option { + if let Some(value) = eval_literal_aot_scalar_expr(expr) { + return Some(EvalLiteralAotExpr::Scalar(value)); + } + match &expr.kind { + ExprKind::Variable(name) => Some(EvalLiteralAotExpr::LoadVar(name.clone())), + ExprKind::BinaryOp { left, op, right } => Some(EvalLiteralAotExpr::Binary { + op: EvalLiteralAotBinaryOp::from_binop(op)?, + left: Box::new(eval_literal_aot_expr(left)?), + right: Box::new(eval_literal_aot_expr(right)?), + }), + _ => None, + } +} + +/// Evaluates a scalar-only expression at compile time for the literal eval AOT subset. +fn eval_literal_aot_scalar_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(EvalLiteralAotScalar::Null), + ExprKind::BoolLiteral(value) => Some(EvalLiteralAotScalar::Bool(*value)), + ExprKind::IntLiteral(value) => Some(EvalLiteralAotScalar::Int(*value)), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(EvalLiteralAotScalar::Float(*value)) + } + ExprKind::StringLiteral(value) => Some(EvalLiteralAotScalar::String(value.clone())), + ExprKind::Negate(inner) => eval_literal_aot_negate(inner), + ExprKind::Not(inner) => { + let value = eval_literal_aot_scalar_expr(inner)?; + Some(EvalLiteralAotScalar::Bool(!value.truthy())) + } + ExprKind::BinaryOp { left, op, right } => { + let left = eval_literal_aot_scalar_expr(left)?; + let right = eval_literal_aot_scalar_expr(right)?; + eval_literal_aot_binary(&left, op, &right) + } + _ => None, + } +} + +/// Applies unary minus for scalar integer and float literals. +fn eval_literal_aot_negate(expr: &Expr) -> Option { + match eval_literal_aot_scalar_expr(expr)? { + EvalLiteralAotScalar::Int(value) => value.checked_neg().map(EvalLiteralAotScalar::Int), + EvalLiteralAotScalar::Float(value) => Some(EvalLiteralAotScalar::Float(-value)), + _ => None, + } +} + +/// Applies a safe scalar binary operation for the literal eval AOT subset. +fn eval_literal_aot_binary( + left: &EvalLiteralAotScalar, + op: &BinOp, + right: &EvalLiteralAotScalar, +) -> Option { + match op { + BinOp::Add => eval_literal_aot_int_binop(left, right, i64::checked_add), + BinOp::Sub => eval_literal_aot_int_binop(left, right, i64::checked_sub), + BinOp::Mul => eval_literal_aot_int_binop(left, right, i64::checked_mul), + BinOp::Mod => match (left, right) { + (EvalLiteralAotScalar::Int(_), EvalLiteralAotScalar::Int(0)) => None, + (EvalLiteralAotScalar::Int(left), EvalLiteralAotScalar::Int(right)) => { + left.checked_rem(*right).map(EvalLiteralAotScalar::Int) + } + _ => None, + }, + BinOp::Concat => Some(EvalLiteralAotScalar::String(format!( + "{}{}", + left.as_php_string()?, + right.as_php_string()? + ))), + _ => None, + } +} + +/// Applies an integer-only checked binary operation. +fn eval_literal_aot_int_binop( + left: &EvalLiteralAotScalar, + right: &EvalLiteralAotScalar, + op: fn(i64, i64) -> Option, +) -> Option { + match (left, right) { + (EvalLiteralAotScalar::Int(left), EvalLiteralAotScalar::Int(right)) => { + op(*left, *right).map(EvalLiteralAotScalar::Int) + } + _ => None, + } +} + +impl EvalLiteralAotInst { + /// Returns true when this AOT instruction needs eval scope setup. + fn has_scope_access(&self) -> bool { + match self { + Self::Echo(expr) | Self::Return(expr) => expr.has_scope_access(), + Self::Store { .. } => true, + } + } + + /// Returns variable names read from eval scope while executing this instruction. + fn scope_reads(&self) -> BTreeSet { + let mut reads = BTreeSet::new(); + match self { + Self::Echo(expr) | Self::Return(expr) => expr.collect_scope_reads(&mut reads), + Self::Store { value, .. } => value.collect_scope_reads(&mut reads), + } + reads + } + + /// Returns the eval scope variable written by this instruction, if any. + fn scope_write(&self) -> Option { + match self { + Self::Store { name, .. } => Some(name.clone()), + Self::Echo(_) | Self::Return(_) => None, + } + } +} + +impl EvalLiteralAotExpr { + /// Returns true when this expression reads a variable from the eval scope. + fn has_scope_access(&self) -> bool { + match self { + Self::Scalar(_) => false, + Self::LoadVar(_) => true, + Self::Binary { left, right, .. } => left.has_scope_access() || right.has_scope_access(), + } + } + + /// Adds all variable names read from eval scope by this expression. + fn collect_scope_reads(&self, reads: &mut BTreeSet) { + match self { + Self::Scalar(_) => {} + Self::LoadVar(name) => { + reads.insert(name.clone()); + } + Self::Binary { left, right, .. } => { + left.collect_scope_reads(reads); + right.collect_scope_reads(reads); + } + } + } +} + +impl EvalLiteralAotBinaryOp { + /// Maps an AST operator to the boxed-Mixed helper used by literal eval AOT. + fn from_binop(op: &BinOp) -> Option { + match op { + BinOp::Add => Some(Self::Add), + BinOp::Sub => Some(Self::Sub), + BinOp::Mul => Some(Self::Mul), + BinOp::Div => Some(Self::Div), + BinOp::Mod => Some(Self::Mod), + BinOp::Concat => Some(Self::Concat), + _ => None, + } + } + + /// Returns the runtime helper symbol for this boxed-Mixed binary operation. + fn helper(&self) -> &'static str { + match self { + Self::Add => "__elephc_eval_value_add", + Self::Sub => "__elephc_eval_value_sub", + Self::Mul => "__elephc_eval_value_mul", + Self::Div => "__elephc_eval_value_div", + Self::Mod => "__elephc_eval_value_mod", + Self::Concat => "__elephc_eval_value_concat", + } + } +} + +impl EvalLiteralAotScalar { + /// Returns PHP truthiness for the scalar forms accepted by the AOT subset. + fn truthy(&self) -> bool { + match self { + Self::Null => false, + Self::Bool(value) => *value, + Self::Int(value) => *value != 0, + Self::Float(value) => *value != 0.0, + Self::String(value) => !value.is_empty() && value != "0", + } + } + + /// Converts scalar forms to the PHP string form safe for compile-time concat. + fn as_php_string(&self) -> Option { + match self { + Self::Null => Some(String::new()), + Self::Bool(false) => Some(String::new()), + Self::Bool(true) => Some("1".to_string()), + Self::Int(value) => Some(value.to_string()), + Self::String(value) => Some(value.clone()), + Self::Float(_) => None, + } + } +} + +impl EvalLocalScalarAnalysis { + /// Registers or validates a local variable slot and its current scalar type. + fn ensure_local( + &mut self, + name: &str, + ty: EvalLocalScalarType, + allow_type_change: bool, + ) -> Option<()> { + if let Some(existing) = self.local_types.get(name) { + if *existing != ty { + allow_type_change.then_some(())?; + self.local_types.insert(name.to_string(), ty); + } + } else { + self.local_types.insert(name.to_string(), ty); + } + if !self.locals.contains_key(name) { + let slot = self.locals.len(); + self.locals.insert(name.to_string(), slot); + } + Some(()) + } + + /// Records scratch-slot demand for a parsed local scalar expression. + fn record_expr(&mut self, expr: &EvalLocalScalarExpr) { + self.max_scratch_slots = self.max_scratch_slots.max(expr.scratch_slots()); + } +} + +impl EvalLocalScalarAotProgram { + /// Returns the byte offset of a local scalar value slot from the reserved eval stack base. + fn value_offset(&self, name: &str) -> usize { + EVAL_STACK_BYTES + self.locals[name] * EVAL_LOCAL_SCALAR_SLOT_BYTES + } + + /// Returns the byte offset of a local scalar secondary payload word. + fn value_aux_offset(&self, name: &str) -> usize { + self.value_offset(name) + 8 + } + + /// Returns the byte offset of a local scalar defined-flag slot from the eval stack base. + fn defined_offset(&self, name: &str) -> usize { + self.value_offset(name) + 16 + } + + /// Returns the byte offset for a recursive expression scratch slot. + fn scratch_offset(&self, depth: usize) -> usize { + EVAL_STACK_BYTES + self.locals.len() * EVAL_LOCAL_SCALAR_SLOT_BYTES + depth * 8 + } + + /// Returns the byte offset used to preserve the eval return cell across scope flushes. + fn result_cell_offset(&self) -> usize { + self.scratch_offset(self.scratch_slots) + } + + /// Returns the total temporary stack bytes needed by this local AOT fragment. + fn stack_bytes(&self) -> usize { + align_to_16(self.result_cell_offset() + 8) + } + + /// Returns the scalar type tracked for a local name. + fn local_type(&self, name: &str) -> EvalLocalScalarType { + self.local_types[name] + } +} + +impl EvalLocalScalarExpr { + /// Returns how many fixed scratch slots this expression can need during lowering. + fn scratch_slots(&self) -> usize { + match &self.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => 0, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) + | EvalLocalScalarExprKind::Print(inner) => inner.scratch_slots(), + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => condition + .scratch_slots() + .max(then_expr.scratch_slots()) + .max(else_expr.scratch_slots()), + EvalLocalScalarExprKind::Binary { op, left, right } => { + if matches!( + op, + EvalLocalScalarBinaryOp::And | EvalLocalScalarBinaryOp::Or + ) { + left.scratch_slots().max(right.scratch_slots()) + } else if matches!(op, EvalLocalScalarBinaryOp::Concat) { + left.scratch_slots().max(right.scratch_slots()) + } else { + left.scratch_slots().max(1 + right.scratch_slots()).max(1) + } + } + EvalLocalScalarExprKind::StaticFunctionCall { args, .. } => { + let arg_slots = args.len(); + args.iter() + .map(EvalLocalScalarExpr::scratch_slots) + .max() + .unwrap_or(0) + + arg_slots + } + } + } +} + +impl EvalLocalScalarType { + /// Returns true when the type can participate in numeric local-scalar operations. + fn is_numeric(self) -> bool { + matches!(self, Self::Int | Self::Float) + } + + /// Maps the local scalar type to the nearest PHP codegen representation. + fn php_type(self) -> PhpType { + match self { + Self::Null => PhpType::Void, + Self::Int => PhpType::Int, + Self::Float => PhpType::Float, + Self::Bool => PhpType::Bool, + Self::String => PhpType::Str, + } + } +} + +impl EvalLocalScalarBinaryOp { + /// Maps an AST binary operator and operand types into the local scalar AOT subset. + fn from_value_binop( + op: &BinOp, + left_ty: EvalLocalScalarType, + right_ty: EvalLocalScalarType, + ) -> Option<(Self, EvalLocalScalarType)> { + match op { + BinOp::Add + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Add, EvalLocalScalarType::Int)) + } + BinOp::Sub + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Sub, EvalLocalScalarType::Int)) + } + BinOp::Mul + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Mul, EvalLocalScalarType::Int)) + } + BinOp::Div if left_ty.is_numeric() && right_ty.is_numeric() => { + Some((Self::Div, EvalLocalScalarType::Float)) + } + BinOp::Mod if left_ty.is_numeric() && right_ty.is_numeric() => { + Some((Self::Mod, EvalLocalScalarType::Int)) + } + BinOp::BitAnd + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitAnd, EvalLocalScalarType::Int)) + } + BinOp::BitOr + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitOr, EvalLocalScalarType::Int)) + } + BinOp::BitXor + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitXor, EvalLocalScalarType::Int)) + } + BinOp::ShiftLeft + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::ShiftLeft, EvalLocalScalarType::Int)) + } + BinOp::ShiftRight + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::ShiftRight, EvalLocalScalarType::Int)) + } + BinOp::Lt + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Lt, EvalLocalScalarType::Bool)) + } + BinOp::Gt + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Gt, EvalLocalScalarType::Bool)) + } + BinOp::LtEq + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::LtEq, EvalLocalScalarType::Bool)) + } + BinOp::GtEq + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::GtEq, EvalLocalScalarType::Bool)) + } + BinOp::Eq + if left_ty == right_ty + && matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::Eq, EvalLocalScalarType::Bool)) + } + BinOp::NotEq + if left_ty == right_ty + && matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::NotEq, EvalLocalScalarType::Bool)) + } + BinOp::And + if matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) && matches!( + right_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::And, EvalLocalScalarType::Bool)) + } + BinOp::Or + if matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) && matches!( + right_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::Or, EvalLocalScalarType::Bool)) + } + _ => None, + } + } +} + +/// Rounds a byte count up to the next 16-byte stack-aligned size. +fn align_to_16(value: usize) -> usize { + (value + 15) & !15 +} + +/// Lowers an eligible literal eval fragment directly, returning false when codegen must fall back. +fn lower_eval_literal_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralAotProgram, +) -> Result { + match program { + EvalLiteralAotProgram::Boxed(program) => lower_eval_literal_boxed_aot(ctx, inst, program), + EvalLiteralAotProgram::LocalScalar(program) => { + lower_eval_local_scalar_aot(ctx, inst, program) + } + } +} + +/// Lowers an eligible boxed-Mixed literal eval fragment directly. +fn lower_eval_literal_boxed_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, +) -> Result { + if let Some(targets) = eval_literal_boxed_direct_local_store_targets(ctx, program)? { + lower_eval_literal_boxed_aot_direct_local_stores(ctx, inst, program, &targets)?; + return Ok(true); + } + if let Some(targets) = eval_literal_boxed_direct_read_write_targets(ctx, program)? { + lower_eval_literal_boxed_aot_direct_read_writes(ctx, inst, program, &targets)?; + return Ok(true); + } + if program.has_scope_writes && !eval_global_aliases(ctx).is_empty() { + ctx.emitter.comment( + "eval literal AOT fallback: scope writes with global aliases need bridge semantics", + ); + return Ok(false); + } + + ctx.emitter.comment(&format!( + "eval literal AOT compiled ({} ops)", + program.instructions.len() + )); + if program.has_scope_access { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + let flush_names = program + .scope_reads + .union(&program.scope_writes) + .cloned() + .collect::>(); + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let flush_locals = filter_eval_sync_locals_by_name(sync_locals.clone(), &flush_names); + let flush_globals = filter_eval_sync_globals_by_name(sync_globals.clone(), &flush_names); + let reload_locals = filter_eval_sync_locals_by_name(sync_locals, &program.scope_writes); + let reload_globals = filter_eval_sync_globals_by_name(sync_globals, &program.scope_writes); + flush_eval_scope_locals(ctx, &flush_locals)?; + flush_eval_global_scope(ctx, &flush_globals)?; + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Echo(value) => emit_eval_literal_aot_echo(ctx, value, 0), + EvalLiteralAotInst::Store { name, value } => { + emit_eval_literal_aot_scope_store(ctx, name, value, 0)?; + } + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_expr_cell(ctx, value, 0); + break; + } + } + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &reload_locals)?; + reload_eval_global_scope(ctx, &reload_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + } else { + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Echo(value) => emit_eval_literal_aot_echo(ctx, value, 0), + EvalLiteralAotInst::Store { .. } => unreachable!("scope writes are handled above"), + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_expr_cell(ctx, value, 0); + break; + } + } + } + } + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Lowers write-only boxed AOT stores directly into caller local slots without eval scope. +fn lower_eval_literal_boxed_aot_direct_local_stores( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + ctx.emitter.comment(&format!( + "eval literal AOT compiled direct local stores ({} ops)", + program.instructions.len() + )); + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Store { name, value } => { + let target = targets.get(name).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "direct eval local store target ${} was not prepared", + name + )) + })?; + if let Some(slot) = target { + emit_eval_literal_aot_direct_local_store(ctx, *slot, value)?; + } + } + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_core_mixed_expr(ctx, value)?; + store_if_result(ctx, inst)?; + return Ok(()); + } + EvalLiteralAotInst::Echo(_) => { + return Err(CodegenIrError::unsupported( + "direct eval local store echo should be rejected before lowering".to_string(), + )); + } + } + } + emit_eval_local_scalar_core_null_cell(ctx); + store_if_result(ctx, inst) +} + +/// Lowers read/write eval stores by reading and writing caller integer locals directly. +fn lower_eval_literal_boxed_aot_direct_read_writes( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, + targets: &BTreeMap, +) -> Result<()> { + ctx.emitter + .comment("eval literal AOT compiled direct local read/write stores"); + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Store { name, value } => { + let slot = targets.get(name).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "direct eval read/write target ${} was not prepared", + name + )) + })?; + let source_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, *slot, value)?; + emit_eval_literal_prepare_direct_read_write_store(ctx, *slot, &source_ty)?; + ctx.store_current_result_to_local(*slot)?; + } + EvalLiteralAotInst::Return(value) => { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval read/write return requires a static scalar".to_string(), + )); + }; + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + store_if_result(ctx, inst)?; + return Ok(()); + } + EvalLiteralAotInst::Echo(_) => { + return Err(CodegenIrError::unsupported( + "direct eval read/write echo should be rejected before lowering".to_string(), + )); + } + } + } + emit_eval_local_scalar_core_null_cell(ctx); + store_if_result(ctx, inst) +} + +/// Resolves direct read/write eval targets, returning `None` when scope semantics are needed. +fn eval_literal_boxed_direct_read_write_targets( + ctx: &FunctionContext<'_>, + program: &EvalLiteralBoxedAotProgram, +) -> Result>> { + if !program.has_scope_writes || program.scope_reads.is_empty() { + return Ok(None); + } + let mut targets = BTreeMap::new(); + for inst in &program.instructions { + match inst { + EvalLiteralAotInst::Store { name, value } => { + let Some(slot) = eval_literal_direct_read_write_local_slot(ctx, name)? else { + return Ok(None); + }; + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + let Some(result_ty) = + eval_literal_direct_read_write_expr_type(value, name, &target_ty) + else { + return Ok(None); + }; + if !eval_literal_direct_read_write_result_supported(&target_ty, &result_ty) { + return Ok(None); + } + targets.insert(name.clone(), slot); + } + EvalLiteralAotInst::Return(value) => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + } + EvalLiteralAotInst::Echo(_) => return Ok(None), + } + } + Ok(Some(targets)) +} + +/// Returns an initialized scalar caller local slot for direct read/write eval. +fn eval_literal_direct_read_write_local_slot( + ctx: &FunctionContext<'_>, + name: &str, +) -> Result> { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + return Ok(None); + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal + || ctx.local_stores_ref_cell_pointer(slot) + || !matches!( + ctx.local_php_type(slot)?.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Mixed | PhpType::Union(_) + ) + // Slots initialized by function parameters (including by-value closure + // captures) hold a defined value without an EIR store instruction. + || !(eval_literal_local_slot_has_eir_write(ctx, slot) + || ctx.function.params.iter().any(|param| param.name == name)) + { + return Ok(None); + } + Ok(Some(slot)) +} + +/// Returns the native result type for a direct scalar read/write expression. +fn eval_literal_direct_read_write_expr_type( + value: &EvalLiteralAotExpr, + target_name: &str, + target_ty: &PhpType, +) -> Option { + match value { + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Int(_)) => Some(PhpType::Int), + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Float(_)) => Some(PhpType::Float), + EvalLiteralAotExpr::LoadVar(name) if name == target_name => Some(target_ty.clone()), + EvalLiteralAotExpr::Binary { op, left, right } => { + let left_ty = eval_literal_direct_read_write_expr_type(left, target_name, target_ty)?; + let right_ty = eval_literal_direct_read_write_expr_type(right, target_name, target_ty)?; + match op { + EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul + if left_ty == PhpType::Int && right_ty == PhpType::Int => + { + Some(PhpType::Int) + } + EvalLiteralAotBinaryOp::Div + if eval_literal_direct_read_write_numeric_type(&left_ty) + && eval_literal_direct_read_write_numeric_type(&right_ty) => + { + Some(PhpType::Float) + } + EvalLiteralAotBinaryOp::Mod + if eval_literal_direct_read_write_numeric_type(&left_ty) + && eval_literal_direct_read_write_numeric_type(&right_ty) => + { + Some(PhpType::Int) + } + EvalLiteralAotBinaryOp::Concat + | EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul + | EvalLiteralAotBinaryOp::Div + | EvalLiteralAotBinaryOp::Mod => None, + } + } + _ => None, + } +} + +/// Returns true when a direct read/write native result can be stored in the caller slot. +fn eval_literal_direct_read_write_result_supported( + target_ty: &PhpType, + result_ty: &PhpType, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Int => result_ty.codegen_repr() == PhpType::Int, + PhpType::Float => result_ty.codegen_repr() == PhpType::Float, + PhpType::Mixed | PhpType::Union(_) => result_ty.codegen_repr() == PhpType::Float, + _ => false, + } +} + +/// Returns true for native numeric types accepted by direct read/write arithmetic. +fn eval_literal_direct_read_write_numeric_type(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Mixed | PhpType::Union(_) + ) +} + +/// Emits a direct scalar read/write expression into the matching result register. +fn emit_eval_literal_aot_direct_read_write_expr( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result { + match value { + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + Ok(PhpType::Int) + } + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Float(value)) => { + emit_eval_literal_aot_scalar_native_result(ctx, &EvalLiteralAotScalar::Float(*value)); + Ok(PhpType::Float) + } + EvalLiteralAotExpr::LoadVar(_) => ctx.load_local_to_result(slot), + EvalLiteralAotExpr::Binary { op, left, right } => { + let left_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, slot, left)?; + emit_eval_literal_direct_read_write_push_result(ctx, &left_ty)?; + let right_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, slot, right)?; + match op { + EvalLiteralAotBinaryOp::Div => { + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &right_ty)?; + let rhs_reg = abi::float_arg_reg_name(ctx.emitter.target, 1); + if matches!(left_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 16, + ); + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &left_ty)?; + abi::emit_pop_float_reg(ctx.emitter, rhs_reg); + abi::emit_release_temporary_stack(ctx.emitter, 16); + } else { + abi::emit_reg_move( + ctx.emitter, + rhs_reg, + abi::float_result_reg(ctx.emitter), + ); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &left_ty)?; + } + emit_eval_literal_direct_read_write_div(ctx, rhs_reg); + Ok(PhpType::Float) + } + EvalLiteralAotBinaryOp::Mod => { + emit_eval_literal_direct_read_write_numeric_to_int(ctx, &right_ty)?; + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + emit_eval_literal_direct_read_write_numeric_to_int(ctx, &left_ty)?; + emit_eval_local_scalar_mod(ctx, rhs_reg); + Ok(PhpType::Int) + } + EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul => { + if left_ty != PhpType::Int || right_ty != PhpType::Int { + return Err(CodegenIrError::unsupported( + "direct eval read/write float arithmetic".to_string(), + )); + } + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + let local_op = match op { + EvalLiteralAotBinaryOp::Add => EvalLocalScalarBinaryOp::Add, + EvalLiteralAotBinaryOp::Sub => EvalLocalScalarBinaryOp::Sub, + EvalLiteralAotBinaryOp::Mul => EvalLocalScalarBinaryOp::Mul, + EvalLiteralAotBinaryOp::Div + | EvalLiteralAotBinaryOp::Mod + | EvalLiteralAotBinaryOp::Concat => unreachable!( + "direct read/write arithmetic op filtered before integer lowering" + ), + }; + emit_eval_local_scalar_binary_result(ctx, &local_op, rhs_reg); + Ok(PhpType::Int) + } + EvalLiteralAotBinaryOp::Concat => { + return Err(CodegenIrError::unsupported( + "direct eval read/write concat".to_string(), + )); + } + } + } + _ => Err(CodegenIrError::unsupported( + "direct eval read/write expression".to_string(), + )), + } +} + +/// Preserves the current direct read/write result on the temporary stack. +fn emit_eval_literal_direct_read_write_push_result( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Float => { + abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write push for PHP type {:?}", + other + ))), + } +} + +/// Restores a preserved direct read/write result from the temporary stack. +fn emit_eval_literal_direct_read_write_pop_result( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Float => { + abi::emit_pop_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write pop for PHP type {:?}", + other + ))), + } +} + +/// Coerces the current direct read/write numeric result into the float register. +fn emit_eval_literal_direct_read_write_numeric_to_float( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + PhpType::Float => Ok(()), + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write float coercion for PHP type {:?}", + other + ))), + } +} + +/// Coerces the current direct read/write numeric result into the integer register. +fn emit_eval_literal_direct_read_write_numeric_to_int( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => Ok(()), + PhpType::Float => { + abi::emit_float_result_to_int_result(ctx.emitter); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write int coercion for PHP type {:?}", + other + ))), + } +} + +/// Emits the target-specific direct read/write floating division. +fn emit_eval_literal_direct_read_write_div(ctx: &mut FunctionContext<'_>, rhs_reg: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "fdiv {}, {}, {}", + abi::float_result_reg(ctx.emitter), + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the direct eval read/write floating-point quotient + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!( + "divsd {}, {}", + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the direct eval read/write floating-point quotient + } + } +} + +/// Converts a direct read/write result to the caller slot representation and releases old ownership. +fn emit_eval_literal_prepare_direct_read_write_store( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + source_ty: &PhpType, +) -> Result<()> { + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + if matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &source_ty.codegen_repr()); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + } + Ok(()) +} + +/// Releases the previous refcounted value stored in a direct eval local target. +fn emit_eval_literal_release_old_direct_local_value( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + target_ty: &PhpType, +) -> Result<()> { + if target_ty.codegen_repr().is_refcounted() { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset_scratch( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + offset, + abi::secondary_scratch_reg(ctx.emitter), + ); + abi::emit_decref_if_refcounted(ctx.emitter, &target_ty.codegen_repr()); + } + Ok(()) +} + +/// Resolves direct local-store targets, returning `None` when scope semantics are still needed. +fn eval_literal_boxed_direct_local_store_targets( + ctx: &FunctionContext<'_>, + program: &EvalLiteralBoxedAotProgram, +) -> Result>>> { + if !program.has_scope_writes || !program.scope_reads.is_empty() { + return Ok(None); + } + let mut targets = BTreeMap::new(); + for inst in &program.instructions { + match inst { + EvalLiteralAotInst::Store { name, value } => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + let target = match eval_literal_direct_store_local_slot(ctx, name)? { + Some(slot) + if eval_literal_aot_expr_direct_store_supported(ctx, slot, value)? => + { + Some(slot) + } + _ => None, + }; + targets.insert(name.clone(), target); + } + EvalLiteralAotInst::Return(value) => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + } + EvalLiteralAotInst::Echo(_) => return Ok(None), + } + } + Ok(Some(targets)) +} + +/// Returns the caller local slot for a direct eval store when overwriting is known safe. +fn eval_literal_direct_store_local_slot( + ctx: &FunctionContext<'_>, + name: &str, +) -> Result> { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + return Ok(None); + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal || ctx.local_stores_ref_cell_pointer(slot) { + return Ok(None); + } + // Slots the native code already writes are fine: the store emitter + // releases the previous refcounted value before overwriting. + Ok(Some(slot)) +} + +/// Returns true when existing EIR already writes a local slot and old-value release matters. +fn eval_literal_local_slot_has_eir_write(ctx: &FunctionContext<'_>, slot: LocalSlotId) -> bool { + ctx.function.instructions.iter().any(|inst| { + matches!( + inst.op, + Op::StoreLocal + | Op::StoreRefCell + | Op::UnsetLocal + | Op::PromoteLocalRefCell + | Op::AliasLocalRefCell + | Op::ReleaseLocalRefCell + ) && inst.immediate == Some(Immediate::LocalSlot(slot)) + }) +} + +/// Returns true when a boxed AOT expression can be stored into a local target type. +fn eval_literal_aot_expr_direct_store_supported( + ctx: &FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Ok(false); + }; + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + Ok(eval_literal_aot_scalar_direct_store_supported( + value, &target_ty, + )) +} + +/// Returns true when a scalar value can be materialized in a direct local target format. +fn eval_literal_aot_scalar_direct_store_supported( + value: &EvalLiteralAotScalar, + target_ty: &PhpType, +) -> bool { + if matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) { + return true; + } + match (value, target_ty) { + (EvalLiteralAotScalar::Int(_), PhpType::Int) => true, + (EvalLiteralAotScalar::Bool(_), PhpType::Bool) => true, + (EvalLiteralAotScalar::Float(_), PhpType::Float) => true, + (EvalLiteralAotScalar::String(_), PhpType::Str) => true, + (EvalLiteralAotScalar::Null | EvalLiteralAotScalar::Int(_), PhpType::TaggedScalar) => true, + _ => false, + } +} + +/// Stores one static scalar eval assignment directly into an already allocated local slot. +fn emit_eval_literal_aot_direct_local_store( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result<()> { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval local store requires a static scalar value".to_string(), + )); + }; + match ctx.local_php_type(slot)?.codegen_repr() { + target_ty @ (PhpType::Mixed | PhpType::Union(_)) => { + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + ctx.store_current_result_to_local(slot) + } + PhpType::Str => { + // Static scalar strings live in the data section; the old value + // may be a heap string and must be released before overwriting. + emit_eval_literal_release_old_direct_local_value(ctx, slot, &PhpType::Str)?; + emit_eval_literal_aot_scalar_native_result(ctx, value); + ctx.store_current_result_to_local(slot) + } + PhpType::TaggedScalar => { + emit_eval_literal_aot_tagged_scalar_result(ctx, value)?; + ctx.store_current_result_to_local(slot) + } + target_ty => { + emit_eval_literal_aot_scalar_native_result(ctx, value); + if eval_literal_aot_scalar_direct_store_supported(value, &target_ty) { + ctx.store_current_result_to_local(slot) + } else { + Err(CodegenIrError::unsupported(format!( + "direct eval local store to PHP type {:?}", + target_ty + ))) + } + } + } +} + +/// Emits a scalar eval expression as a core-runtime Mixed cell in the result register. +fn emit_eval_literal_aot_core_mixed_expr( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, +) -> Result<()> { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval local store return requires a static scalar value".to_string(), + )); + }; + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + Ok(()) +} + +/// Emits a scalar value as a core-runtime Mixed cell without eval bridge helpers. +fn emit_eval_literal_aot_core_mixed_scalar( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) { + if matches!(value, EvalLiteralAotScalar::Null) { + emit_eval_local_scalar_core_null_cell(ctx); + return; + } + let source_ty = emit_eval_literal_aot_scalar_native_result(ctx, value); + emit_box_current_value_as_mixed(ctx.emitter, &source_ty); +} + +/// Emits a scalar value in its native result-register representation. +fn emit_eval_literal_aot_scalar_native_result( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) -> PhpType { + match value { + EvalLiteralAotScalar::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + PhpType::TaggedScalar + } + EvalLiteralAotScalar::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + i64::from(*value), + ); + PhpType::Bool + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + PhpType::Int + } + EvalLiteralAotScalar::Float(value) => { + let label = ctx.data.add_float(*value); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + &label, + 0, + ); + PhpType::Float + } + EvalLiteralAotScalar::String(value) => { + let (label, len) = ctx.data.add_string(value.as_bytes()); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); + PhpType::Str + } + } +} + +/// Emits a scalar value as the tagged-scalar local representation. +fn emit_eval_literal_aot_tagged_scalar_result( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) -> Result<()> { + match value { + EvalLiteralAotScalar::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + Ok(()) + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(ctx.emitter); + Ok(()) + } + _ => Err(CodegenIrError::unsupported( + "direct eval local store tagged-scalar source".to_string(), + )), + } +} + +/// Lowers a self-contained int/bool literal eval fragment as native local control flow. +fn lower_eval_local_scalar_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result { + if !eval_global_aliases(ctx).is_empty() { + ctx.emitter.comment( + "eval literal AOT fallback: local scalar writes with global aliases need bridge semantics", + ); + return Ok(false); + } + if !eval_local_scalar_codegen_supported(ctx, program) { + ctx.emitter + .comment("eval literal AOT fallback: local scalar static call is not supported"); + return Ok(false); + } + + ctx.emitter.comment(&format!( + "eval literal AOT compiled local scalar ({} locals, {} stmts)", + program.locals.len(), + program.statements.len() + )); + if let Some(targets) = eval_local_scalar_direct_sync_targets(ctx, program)? { + lower_eval_local_scalar_aot_with_direct_sync(ctx, inst, program, &targets)?; + } else if eval_local_scalar_needs_scope_sync(ctx, program) { + lower_eval_local_scalar_aot_with_scope_sync(ctx, inst, program)?; + } else { + lower_eval_local_scalar_aot_native_only(ctx, inst, program)?; + } + Ok(true) +} + +/// Returns true when local scalar AOT must synchronize variables with eval scope. +fn eval_local_scalar_needs_scope_sync( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> bool { + if !program.locals.is_empty() { + return true; + } + let sync_local_names = eval_sync_locals(ctx) + .into_iter() + .map(|local| local.name) + .collect::>(); + let sync_global_names = eval_sync_globals(ctx) + .into_iter() + .map(|global| global.name) + .collect::>(); + program + .locals + .keys() + .any(|name| sync_local_names.contains(name) || sync_global_names.contains(name)) +} + +/// Resolves caller local slots that can receive final local-scalar eval writes directly. +fn eval_local_scalar_direct_sync_targets( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> Result>>> { + if program.locals.is_empty() { + return Ok(Some(BTreeMap::new())); + } + let mut targets = BTreeMap::new(); + for (name, local_type) in &program.local_types { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + targets.insert(name.clone(), None); + continue; + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal + || ctx.local_stores_ref_cell_pointer(slot) + || !eval_local_scalar_direct_sync_type_supported(ctx.local_php_type(slot)?, *local_type) + { + return Ok(None); + } + // Slots the native code already writes are fine: the sync store + // releases the previous refcounted value before overwriting. + targets.insert(name.clone(), Some(slot)); + } + Ok(Some(targets)) +} + +/// Returns true when one local-scalar value type fits the caller local slot type. +fn eval_local_scalar_direct_sync_type_supported( + target_ty: PhpType, + local_type: EvalLocalScalarType, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => local_type == EvalLocalScalarType::Int, + PhpType::Float => local_type == EvalLocalScalarType::Float, + PhpType::Bool => local_type == EvalLocalScalarType::Bool, + PhpType::Str => local_type == EvalLocalScalarType::String, + PhpType::TaggedScalar => matches!( + local_type, + EvalLocalScalarType::Null | EvalLocalScalarType::Int + ), + _ => false, + } +} + +/// Lowers local scalar AOT without any eval bridge or eval scope runtime dependency. +fn lower_eval_local_scalar_aot_native_only( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result<()> { + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::CoreRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::CoreRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers local scalar AOT and writes supported final locals directly to caller slots. +fn lower_eval_local_scalar_aot_with_direct_sync( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + ctx.emitter + .comment("eval literal AOT compiled local scalar with direct local sync"); + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::CoreRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::CoreRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + emit_eval_local_scalar_flush_direct_locals(ctx, program, targets)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers local scalar AOT and syncs defined locals through eval scope. +fn lower_eval_local_scalar_aot_with_scope_sync( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result<()> { + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::EvalRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::EvalRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + emit_eval_local_scalar_flush_defined_locals(ctx, program); + let sync_locals = eval_sync_locals(ctx) + .into_iter() + .filter(|local| program.locals.contains_key(&local.name)) + .collect::>(); + let sync_globals = eval_sync_globals(ctx) + .into_iter() + .filter(|global| program.locals.contains_key(&global.name)) + .collect::>(); + reload_eval_scope_locals(ctx, &sync_locals)?; + reload_eval_global_scope(ctx, &sync_globals)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Returns true when all parsed local scalar statements are supported by codegen. +fn eval_local_scalar_codegen_supported( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> bool { + eval_local_scalar_stmts_codegen_supported(ctx, &program.statements) +} + +/// Returns true when all statements in a local scalar block are supported by codegen. +fn eval_local_scalar_stmts_codegen_supported( + ctx: &FunctionContext<'_>, + statements: &[EvalLocalScalarStmt], +) -> bool { + statements.iter().all(|stmt| match stmt { + EvalLocalScalarStmt::Noop + | EvalLocalScalarStmt::Break(_) + | EvalLocalScalarStmt::Continue(_) + | EvalLocalScalarStmt::Return(None) => true, + EvalLocalScalarStmt::Echo(expr) | EvalLocalScalarStmt::Return(Some(expr)) => { + eval_local_scalar_expr_codegen_supported(ctx, expr) + } + EvalLocalScalarStmt::Store { value, .. } => { + eval_local_scalar_expr_codegen_supported(ctx, value) + } + EvalLocalScalarStmt::If { + branches, + else_body, + } => { + branches.iter().all(|(condition, body)| { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + }) && eval_local_scalar_stmts_codegen_supported(ctx, else_body) + } + EvalLocalScalarStmt::While { condition, body } => { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + } + EvalLocalScalarStmt::DoWhile { body, condition } => { + eval_local_scalar_stmts_codegen_supported(ctx, body) + && eval_local_scalar_expr_codegen_supported(ctx, condition) + } + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + } => { + init.as_deref().is_none_or(|stmt| { + eval_local_scalar_stmts_codegen_supported(ctx, std::slice::from_ref(stmt)) + }) && condition + .as_ref() + .is_none_or(|condition| eval_local_scalar_expr_codegen_supported(ctx, condition)) + && update.as_deref().is_none_or(|stmt| { + eval_local_scalar_stmts_codegen_supported(ctx, std::slice::from_ref(stmt)) + }) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + } + EvalLocalScalarStmt::Switch { + subject, + cases, + default, + default_index: _, + } => { + eval_local_scalar_expr_codegen_supported(ctx, subject) + && cases.iter().all(|(conditions, body)| { + conditions + .iter() + .all(|condition| eval_local_scalar_expr_codegen_supported(ctx, condition)) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + }) + && eval_local_scalar_stmts_codegen_supported(ctx, default) + } + }) +} + +/// Returns true when a local scalar expression is supported by codegen. +fn eval_local_scalar_expr_codegen_supported( + ctx: &FunctionContext<'_>, + expr: &EvalLocalScalarExpr, +) -> bool { + match &expr.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => true, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) + | EvalLocalScalarExprKind::Print(inner) => { + eval_local_scalar_expr_codegen_supported(ctx, inner) + } + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_expr_codegen_supported(ctx, then_expr) + && eval_local_scalar_expr_codegen_supported(ctx, else_expr) + } + EvalLocalScalarExprKind::Binary { left, right, .. } => { + eval_local_scalar_expr_codegen_supported(ctx, left) + && eval_local_scalar_expr_codegen_supported(ctx, right) + } + EvalLocalScalarExprKind::StaticFunctionCall { name, args } => { + eval_local_static_function_codegen_supported(ctx, name, args) + } + } +} + +/// Returns true when a static user-function call can be emitted by local scalar AOT. +fn eval_local_static_function_codegen_supported( + ctx: &FunctionContext<'_>, + name: &str, + args: &[EvalLocalScalarExpr], +) -> bool { + if args.len() > 6 { + return false; + } + let Some(callee) = ctx.callable_function_by_name(name) else { + return false; + }; + if callee.params.len() != args.len() || callee.return_php_type.codegen_repr() != PhpType::Int { + return false; + } + callee.params.iter().zip(args).all(|(param, arg)| { + !param.by_ref + && !param.variadic + && matches!( + (param.php_type.codegen_repr(), arg.ty), + (PhpType::Int, EvalLocalScalarType::Int) + | (PhpType::Bool, EvalLocalScalarType::Bool) + ) + }) +} + +/// Clears local value/defined slots before running the local scalar eval fragment. +fn emit_eval_local_scalar_init_slots( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + for name in program.locals.keys() { + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_aux_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.defined_offset(name)); + } +} + +/// Emits one local scalar AOT statement. +fn emit_eval_local_scalar_stmt( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + stmt: &EvalLocalScalarStmt, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + match stmt { + EvalLocalScalarStmt::Noop => {} + EvalLocalScalarStmt::Echo(expr) => { + emit_eval_local_scalar_echo_expr(ctx, program, expr, 0); + } + EvalLocalScalarStmt::Store { name, value } => { + emit_eval_local_scalar_store(ctx, program, name, value); + } + EvalLocalScalarStmt::If { + branches, + else_body, + } => { + emit_eval_local_scalar_if( + ctx, + program, + branches, + else_body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::While { condition, body } => { + emit_eval_local_scalar_while( + ctx, + program, + condition, + body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::DoWhile { body, condition } => { + emit_eval_local_scalar_do_while( + ctx, + program, + body, + condition, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + } => { + emit_eval_local_scalar_for( + ctx, + program, + init.as_deref(), + condition.as_ref(), + update.as_deref(), + body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::Switch { + subject, + cases, + default, + default_index, + } => { + emit_eval_local_scalar_switch( + ctx, + program, + subject, + cases, + default, + *default_index, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::Break(level) => { + let target_index = loop_stack.len() - level; + abi::emit_jump(ctx.emitter, &loop_stack[target_index].break_label); + } + EvalLocalScalarStmt::Continue(level) => { + let target_index = loop_stack.len() - level; + abi::emit_jump(ctx.emitter, &loop_stack[target_index].continue_label); + } + EvalLocalScalarStmt::Return(value) => { + emit_eval_local_scalar_return_cell(ctx, program, value.as_ref(), boxing); + abi::emit_jump(ctx.emitter, return_label); + } + } +} + +/// Stores one local scalar expression into its stack slot and marks the slot defined. +fn emit_eval_local_scalar_store( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + value: &EvalLocalScalarExpr, +) { + emit_eval_local_scalar_expr_value(ctx, program, value, 0); + emit_eval_local_scalar_store_current_value(ctx, program, name, value.ty); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 1); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.defined_offset(name)); +} + +/// Stores the current expression result registers into one local scalar slot. +fn emit_eval_local_scalar_store_current_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + ty: EvalLocalScalarType, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ty { + EvalLocalScalarType::String => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, len_reg, program.value_aux_offset(name)); + } + EvalLocalScalarType::Null => { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_aux_offset(name)); + } + EvalLocalScalarType::Float => { + abi::emit_store_to_sp( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + } + } +} + +/// Emits a local scalar if/elseif/else chain. +fn emit_eval_local_scalar_if( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + branches: &[(EvalLocalScalarExpr, Vec)], + else_body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let done_label = ctx.next_label("eval_local_if_done"); + for (condition, body) in branches { + let next_label = ctx.next_label("eval_local_if_next"); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &next_label); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&next_label); + } + for stmt in else_body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar while loop. +fn emit_eval_local_scalar_while( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + condition: &EvalLocalScalarExpr, + body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let start_label = ctx.next_label("eval_local_while_start"); + let done_label = ctx.next_label("eval_local_while_done"); + ctx.emitter.label(&start_label); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done_label); + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: start_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + abi::emit_jump(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar do/while loop. +fn emit_eval_local_scalar_do_while( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + body: &[EvalLocalScalarStmt], + condition: &EvalLocalScalarExpr, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let start_label = ctx.next_label("eval_local_do_start"); + let condition_label = ctx.next_label("eval_local_do_condition"); + let done_label = ctx.next_label("eval_local_do_done"); + ctx.emitter.label(&start_label); + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: condition_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + ctx.emitter.label(&condition_label); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar for loop with PHP continue-to-update behavior. +fn emit_eval_local_scalar_for( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + init: Option<&EvalLocalScalarStmt>, + condition: Option<&EvalLocalScalarExpr>, + update: Option<&EvalLocalScalarStmt>, + body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + if let Some(init) = init { + emit_eval_local_scalar_stmt(ctx, program, init, loop_stack, return_label, boxing); + } + let start_label = ctx.next_label("eval_local_for_start"); + let update_label = ctx.next_label("eval_local_for_update"); + let done_label = ctx.next_label("eval_local_for_done"); + ctx.emitter.label(&start_label); + if let Some(condition) = condition { + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done_label); + } + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: update_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + ctx.emitter.label(&update_label); + if let Some(update) = update { + emit_eval_local_scalar_stmt(ctx, program, update, loop_stack, return_label, boxing); + } + abi::emit_jump(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar switch with PHP-style fallthrough between case bodies. +fn emit_eval_local_scalar_switch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + subject: &EvalLocalScalarExpr, + cases: &[(Vec, Vec)], + default: &[EvalLocalScalarStmt], + default_index: Option, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let done_label = ctx.next_label("eval_local_switch_done"); + let default_label = default_index.map(|_| ctx.next_label("eval_local_switch_default")); + let case_labels = cases + .iter() + .map(|_| ctx.next_label("eval_local_switch_case")) + .collect::>(); + emit_eval_local_scalar_expr_value(ctx, program, subject, 0); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(0), + ); + for ((conditions, _), case_label) in cases.iter().zip(&case_labels) { + for condition in conditions { + emit_eval_local_scalar_expr_value(ctx, program, condition, 1); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(0), + ); + emit_eval_local_scalar_cmp(ctx, rhs_reg, "eq", "e"); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, case_label); + } + } + if let Some(default_label) = &default_label { + abi::emit_jump(ctx.emitter, default_label); + } else { + abi::emit_jump(ctx.emitter, &done_label); + } + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: done_label.clone(), + }); + for index in 0..=cases.len() { + if default_index == Some(index) { + if let Some(default_label) = &default_label { + ctx.emitter.label(default_label); + for stmt in default { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + loop_stack, + return_label, + boxing, + ); + } + } + } + if index < cases.len() { + ctx.emitter.label(&case_labels[index]); + for stmt in &cases[index].1 { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + } + } + loop_stack.pop(); + ctx.emitter.label(&done_label); +} + +/// Emits an AOT echo expression, splitting concat into sequential writes. +fn emit_eval_local_scalar_echo_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match &expr.kind { + EvalLocalScalarExprKind::String(value) => { + emit_eval_local_scalar_string_stdout(ctx, value.as_bytes()); + } + EvalLocalScalarExprKind::Binary { + op: EvalLocalScalarBinaryOp::Concat, + left, + right, + } => { + emit_eval_local_scalar_echo_expr(ctx, program, left, scratch_depth); + emit_eval_local_scalar_echo_expr(ctx, program, right, scratch_depth); + } + _ => { + emit_eval_local_scalar_expr_value(ctx, program, expr, scratch_depth); + match expr.ty { + EvalLocalScalarType::Null => {} + EvalLocalScalarType::Bool => { + let skip_label = ctx.next_label("eval_local_echo_skip_false"); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + abi::emit_write_stdout(ctx.emitter, &PhpType::Bool); + ctx.emitter.label(&skip_label); + } + EvalLocalScalarType::Int + | EvalLocalScalarType::Float + | EvalLocalScalarType::String => { + abi::emit_write_stdout(ctx.emitter, &expr.ty.php_type()); + } + } + } + } +} + +/// Emits a static string directly to stdout for local scalar AOT echo. +fn emit_eval_local_scalar_string_stdout(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); + abi::emit_write_stdout(ctx.emitter, &PhpType::Str); +} + +/// Emits one local scalar expression value into the integer result register. +fn emit_eval_local_scalar_expr_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match &expr.kind { + EvalLocalScalarExprKind::Null => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + } + EvalLocalScalarExprKind::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + } + EvalLocalScalarExprKind::Float(value) => { + emit_eval_local_scalar_float_value(ctx, *value); + } + EvalLocalScalarExprKind::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + i64::from(*value), + ); + } + EvalLocalScalarExprKind::String(value) => { + emit_eval_local_scalar_string_value(ctx, value); + } + EvalLocalScalarExprKind::LoadVar(name) => { + emit_eval_local_scalar_load_local_value_as(ctx, program, name, expr.ty); + } + EvalLocalScalarExprKind::Isset(names) => { + emit_eval_local_scalar_isset(ctx, program, names); + } + EvalLocalScalarExprKind::EmptyVar(name) => { + emit_eval_local_scalar_empty_var(ctx, program, name); + } + EvalLocalScalarExprKind::Negate(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_negate(ctx, inner.ty); + } + EvalLocalScalarExprKind::BitNot(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_bitnot(ctx); + } + EvalLocalScalarExprKind::Not(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_not(ctx); + } + EvalLocalScalarExprKind::Print(inner) => { + emit_eval_local_scalar_echo_expr(ctx, program, inner, scratch_depth); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + } + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + emit_eval_local_scalar_ternary_expr( + ctx, + program, + condition, + then_expr, + else_expr, + scratch_depth, + ); + } + EvalLocalScalarExprKind::Binary { op, left, right } => { + emit_eval_local_scalar_binary_expr(ctx, program, op, left, right, scratch_depth); + } + EvalLocalScalarExprKind::StaticFunctionCall { name, args } => { + emit_eval_local_scalar_static_function_call(ctx, program, name, args, scratch_depth); + } + } +} + +/// Emits a static string into the local-scalar string result registers. +fn emit_eval_local_scalar_string_value(ctx: &mut FunctionContext<'_>, value: &str) { + let (label, len) = ctx.data.add_string(value.as_bytes()); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + +/// Emits a static float into the local-scalar float result register. +fn emit_eval_local_scalar_float_value(ctx: &mut FunctionContext<'_>, value: f64) { + let label = ctx.data.add_float(value); + abi::emit_load_symbol_to_reg(ctx.emitter, abi::float_result_reg(ctx.emitter), &label, 0); +} + +/// Loads one local scalar slot into the result registers for its tracked type. +fn emit_eval_local_scalar_load_local_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, +) { + emit_eval_local_scalar_load_local_value_as(ctx, program, name, program.local_type(name)); +} + +/// Loads one local scalar slot into result registers using the type at the read point. +fn emit_eval_local_scalar_load_local_value_as( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::String => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + len_reg, + program.value_aux_offset(name), + ); + } + EvalLocalScalarType::Null => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + } + EvalLocalScalarType::Float => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + } +} + +/// Emits PHP `isset()` for definitely local scalar variables. +fn emit_eval_local_scalar_isset( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + names: &[String], +) { + let false_label = ctx.next_label("eval_local_isset_false"); + let done_label = ctx.next_label("eval_local_isset_done"); + for name in names { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + if program.local_type(name) == EvalLocalScalarType::Null { + abi::emit_jump(ctx.emitter, &false_label); + } + } + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits PHP `empty()` for a definitely local scalar variable. +fn emit_eval_local_scalar_empty_var( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, +) { + let true_label = ctx.next_label("eval_local_empty_true"); + let false_label = ctx.next_label("eval_local_empty_false"); + let done_label = ctx.next_label("eval_local_empty_done"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + match program.local_type(name) { + EvalLocalScalarType::Null => { + abi::emit_jump(ctx.emitter, &true_label); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.value_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + abi::emit_jump(ctx.emitter, &false_label); + } + EvalLocalScalarType::Float => { + emit_eval_local_scalar_load_local_value_as( + ctx, + program, + name, + EvalLocalScalarType::Float, + ); + predicates::emit_float_result_nonzero_bool(ctx); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + abi::emit_jump(ctx.emitter, &false_label); + } + EvalLocalScalarType::String => { + emit_eval_local_scalar_empty_string(ctx, program, name, &true_label, &false_label); + } + } + ctx.emitter.label(&true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits the PHP string truthiness check used by `empty()`. +fn emit_eval_local_scalar_empty_string( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + true_label: &str, + false_label: &str, +) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, program.value_aux_offset(name)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // empty strings are falsey in PHP + ctx.emitter.instruction(&format!("b.eq {}", true_label)); // branch when length is zero + ctx.emitter.instruction(&format!("cmp {}, #1", len_reg)); // check for PHP's special string "0" empty case + ctx.emitter.instruction(&format!("b.ne {}", false_label)); // non-empty non-"0" strings are truthy + ctx.emitter.instruction(&format!("ldrb w11, [{}]", ptr_reg)); // load the only byte of the candidate "0" string + ctx.emitter.instruction("cmp w11, #48"); // compare the byte with ASCII '0' + ctx.emitter.instruction(&format!("b.eq {}", true_label)); // string "0" is empty in PHP truthiness + ctx.emitter.instruction(&format!("b {}", false_label)); // any other one-byte string is truthy + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // empty strings are falsey in PHP + ctx.emitter.instruction(&format!("je {}", true_label)); // branch when length is zero + ctx.emitter.instruction(&format!("cmp {}, 1", len_reg)); // check for PHP's special string "0" empty case + ctx.emitter.instruction(&format!("jne {}", false_label)); // non-empty non-"0" strings are truthy + ctx.emitter + .instruction(&format!("movzx ecx, BYTE PTR [{}]", ptr_reg)); // load the only byte of the candidate "0" string + ctx.emitter.instruction("cmp ecx, 48"); // compare the byte with ASCII '0' + ctx.emitter.instruction(&format!("je {}", true_label)); // string "0" is empty in PHP truthiness + ctx.emitter.instruction(&format!("jmp {}", false_label)); // any other one-byte string is truthy + } + } +} + +/// Emits a local scalar ternary expression with same-typed branches. +fn emit_eval_local_scalar_ternary_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + condition: &EvalLocalScalarExpr, + then_expr: &EvalLocalScalarExpr, + else_expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let else_label = ctx.next_label("eval_local_ternary_else"); + let done_label = ctx.next_label("eval_local_ternary_done"); + emit_eval_local_scalar_expr_value(ctx, program, condition, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &else_label); + emit_eval_local_scalar_expr_value(ctx, program, then_expr, scratch_depth); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&else_label); + emit_eval_local_scalar_expr_value(ctx, program, else_expr, scratch_depth); + ctx.emitter.label(&done_label); +} + +/// Emits a validated static user-function call for local scalar AOT. +fn emit_eval_local_scalar_static_function_call( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + args: &[EvalLocalScalarExpr], + scratch_depth: usize, +) { + let arg_base_depth = scratch_depth; + let eval_depth = scratch_depth + args.len(); + for (index, arg) in args.iter().enumerate() { + emit_eval_local_scalar_expr_value(ctx, program, arg, eval_depth); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(arg_base_depth + index), + ); + } + for index in 0..args.len() { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, index), + program.scratch_offset(arg_base_depth + index), + ); + } + abi::emit_call_label(ctx.emitter, &function_symbol(name.trim_start_matches('\\'))); +} + +/// Emits a local scalar binary expression into the integer result register. +fn emit_eval_local_scalar_binary_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + op: &EvalLocalScalarBinaryOp, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match op { + EvalLocalScalarBinaryOp::And => { + emit_eval_local_scalar_and(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Or => { + emit_eval_local_scalar_or(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Div => { + emit_eval_local_scalar_div(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Mod => { + emit_eval_local_scalar_mod_expr(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Concat => unreachable!("concat is emitted as sequential echo"), + _ => { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp( + ctx.emitter, + result_reg, + program.scratch_offset(scratch_depth), + ); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, result_reg); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + result_reg, + program.scratch_offset(scratch_depth), + ); + emit_eval_local_scalar_binary_result(ctx, op, rhs_reg); + } + } +} + +/// Emits a numeric local-scalar division into the float result register. +fn emit_eval_local_scalar_div( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + emit_eval_local_scalar_store_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + emit_eval_local_scalar_numeric_result_to_float(ctx, right.ty); + let rhs_reg = abi::float_arg_reg_name(ctx.emitter.target, 1); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::float_result_reg(ctx.emitter)); + emit_eval_local_scalar_load_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_numeric_result_to_float(ctx, left.ty); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "fdiv {}, {}, {}", + abi::float_result_reg(ctx.emitter), + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the local scalar floating-point quotient + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!( + "divsd {}, {}", + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the local scalar floating-point quotient + } + } +} + +/// Emits a numeric local-scalar modulo after PHP-style int coercion. +fn emit_eval_local_scalar_mod_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + emit_eval_local_scalar_store_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + emit_eval_local_scalar_numeric_result_to_int(ctx, right.ty); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_local_scalar_load_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_numeric_result_to_int(ctx, left.ty); + emit_eval_local_scalar_mod(ctx, rhs_reg); +} + +/// Stores the current numeric result into a local-scalar scratch slot. +fn emit_eval_local_scalar_store_numeric_scratch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + depth: usize, + ty: EvalLocalScalarType, +) { + let reg = match ty { + EvalLocalScalarType::Float => abi::float_result_reg(ctx.emitter), + EvalLocalScalarType::Int => abi::int_result_reg(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric scratch only accepts int/float local scalar values") + } + }; + abi::emit_store_to_sp(ctx.emitter, reg, program.scratch_offset(depth)); +} + +/// Loads a numeric scratch slot into the matching result register. +fn emit_eval_local_scalar_load_numeric_scratch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + depth: usize, + ty: EvalLocalScalarType, +) { + let reg = match ty { + EvalLocalScalarType::Float => abi::float_result_reg(ctx.emitter), + EvalLocalScalarType::Int => abi::int_result_reg(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric scratch only accepts int/float local scalar values") + } + }; + abi::emit_load_temporary_stack_slot(ctx.emitter, reg, program.scratch_offset(depth)); +} + +/// Normalizes the current numeric result into the float result register. +fn emit_eval_local_scalar_numeric_result_to_float( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::Int => abi::emit_int_result_to_float_result(ctx.emitter), + EvalLocalScalarType::Float => {} + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric float coercion only accepts int/float local scalar values") + } + } +} + +/// Normalizes the current numeric result into the integer result register. +fn emit_eval_local_scalar_numeric_result_to_int( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::Int => {} + EvalLocalScalarType::Float => abi::emit_float_result_to_int_result(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric int coercion only accepts int/float local scalar values") + } + } +} + +/// Emits a short-circuiting local scalar `&&` expression. +fn emit_eval_local_scalar_and( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let false_label = ctx.next_label("eval_local_and_false"); + let done_label = ctx.next_label("eval_local_and_done"); + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits a short-circuiting local scalar `||` expression. +fn emit_eval_local_scalar_or( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let true_label = ctx.next_label("eval_local_or_true"); + let done_label = ctx.next_label("eval_local_or_done"); + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &true_label); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + ctx.emitter.label(&done_label); +} + +/// Emits unary numeric negation in the matching result register. +fn emit_eval_local_scalar_negate(ctx: &mut FunctionContext<'_>, ty: EvalLocalScalarType) { + if ty == EvalLocalScalarType::Float { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("fneg d0, d0"); // negate the local scalar floating-point result + } + Arch::X86_64 => { + ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for local scalar negation + ctx.emitter.instruction("subsd xmm1, xmm0"); // compute 0.0 minus the local scalar float + ctx.emitter.instruction("movsd xmm0, xmm1"); // move the negated local scalar float into the result register + } + } + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("neg {}, {}", result_reg, result_reg)); //negate the local scalar integer result + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("neg {}", result_reg)); // negate the local scalar integer result + } + } +} + +/// Emits a unary integer bitwise-not in the result register. +fn emit_eval_local_scalar_bitnot(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("mvn {}, {}", result_reg, result_reg)); // invert every bit of the local scalar integer result + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("not {}", result_reg)); // invert every bit of the local scalar integer result + } + } +} + +/// Emits PHP boolean negation for an int/bool local scalar result. +fn emit_eval_local_scalar_not(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test local scalar truthiness against false + ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // materialize logical negation as 0 or 1 + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); //test local scalar truthiness against false + ctx.emitter.instruction("sete al"); // materialize logical negation in the low byte + ctx.emitter + .instruction(&format!("movzx {}, al", result_reg)); // widen logical negation into the result register + } + } +} + +/// Emits the final arithmetic/comparison step for a local scalar binary operation. +fn emit_eval_local_scalar_binary_result( + ctx: &mut FunctionContext<'_>, + op: &EvalLocalScalarBinaryOp, + rhs_reg: &str, +) { + match op { + EvalLocalScalarBinaryOp::Add => emit_eval_local_scalar_arith(ctx, "add", "add", rhs_reg), + EvalLocalScalarBinaryOp::Sub => emit_eval_local_scalar_arith(ctx, "sub", "sub", rhs_reg), + EvalLocalScalarBinaryOp::Mul => emit_eval_local_scalar_arith(ctx, "mul", "imul", rhs_reg), + EvalLocalScalarBinaryOp::Mod => emit_eval_local_scalar_mod(ctx, rhs_reg), + EvalLocalScalarBinaryOp::BitAnd => emit_eval_local_scalar_arith(ctx, "and", "and", rhs_reg), + EvalLocalScalarBinaryOp::BitOr => emit_eval_local_scalar_arith(ctx, "orr", "or", rhs_reg), + EvalLocalScalarBinaryOp::BitXor => emit_eval_local_scalar_arith(ctx, "eor", "xor", rhs_reg), + EvalLocalScalarBinaryOp::ShiftLeft => { + emit_eval_local_scalar_shift(ctx, "lsl", "shl", rhs_reg) + } + EvalLocalScalarBinaryOp::ShiftRight => { + emit_eval_local_scalar_shift(ctx, "asr", "sar", rhs_reg) + } + EvalLocalScalarBinaryOp::Lt => emit_eval_local_scalar_cmp(ctx, rhs_reg, "lt", "l"), + EvalLocalScalarBinaryOp::Gt => emit_eval_local_scalar_cmp(ctx, rhs_reg, "gt", "g"), + EvalLocalScalarBinaryOp::LtEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "le", "le"), + EvalLocalScalarBinaryOp::GtEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "ge", "ge"), + EvalLocalScalarBinaryOp::Eq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "eq", "e"), + EvalLocalScalarBinaryOp::NotEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "ne", "ne"), + EvalLocalScalarBinaryOp::And + | EvalLocalScalarBinaryOp::Div + | EvalLocalScalarBinaryOp::Or + | EvalLocalScalarBinaryOp::Concat => unreachable!("handled before final binary step"), + } +} + +/// Emits a target-aware integer arithmetic operation for local scalar AOT. +fn emit_eval_local_scalar_arith( + ctx: &mut FunctionContext<'_>, + aarch64_mnemonic: &str, + x86_64_mnemonic: &str, + rhs_reg: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "{} {}, {}, {}", + aarch64_mnemonic, result_reg, result_reg, rhs_reg + )); //compute the local scalar arithmetic result + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("{} {}, {}", x86_64_mnemonic, result_reg, rhs_reg)); + //update the local scalar arithmetic result + } + } +} + +/// Emits a target-aware variable-count integer shift for local scalar AOT. +fn emit_eval_local_scalar_shift( + ctx: &mut FunctionContext<'_>, + aarch64_mnemonic: &str, + x86_64_mnemonic: &str, + rhs_reg: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "{} {}, {}, {}", + aarch64_mnemonic, result_reg, result_reg, rhs_reg + )); // shift the local scalar integer by the evaluated count + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov rcx, {}", rhs_reg)); // move the local scalar shift count into x86_64's cl register + ctx.emitter + .instruction(&format!("{} {}, cl", x86_64_mnemonic, result_reg)); + // shift the local scalar integer by the low count byte + } + } +} + +/// Emits a target-aware signed modulo operation for local scalar AOT. +fn emit_eval_local_scalar_mod(ctx: &mut FunctionContext<'_>, rhs_reg: &str) { + let result_reg = abi::int_result_reg(ctx.emitter); + let zero_label = ctx.next_label("eval_local_mod_zero"); + let done_label = ctx.next_label("eval_local_mod_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + let quotient_reg = abi::tertiary_scratch_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); //branch to the local scalar modulo zero guard + ctx.emitter.instruction(&format!( + "sdiv {}, {}, {}", + quotient_reg, result_reg, rhs_reg + )); //compute the local scalar signed quotient + ctx.emitter.instruction(&format!( + "msub {}, {}, {}, {}", + result_reg, quotient_reg, rhs_reg, result_reg + )); //compute the local scalar signed remainder + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&zero_label); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + ctx.emitter.label(&done_label); + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the local scalar modulo divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to the local scalar modulo zero guard + ctx.emitter.instruction("cqo"); // sign-extend the local scalar dividend before division + ctx.emitter.instruction(&format!("idiv {}", rhs_reg)); // divide local scalar integers + ctx.emitter.instruction(&format!("mov {}, rdx", result_reg)); // move the local scalar remainder into the result register + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&zero_label); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + ctx.emitter.label(&done_label); + } + } +} + +/// Emits a target-aware integer comparison for local scalar AOT. +fn emit_eval_local_scalar_cmp( + ctx: &mut FunctionContext<'_>, + rhs_reg: &str, + aarch64_condition: &str, + x86_64_condition: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, rhs_reg)); //compare local scalar operands + ctx.emitter + .instruction(&format!("cset {}, {}", result_reg, aarch64_condition)); + //materialize the local scalar comparison result + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, rhs_reg)); //compare local scalar operands + ctx.emitter + .instruction(&format!("set{} al", x86_64_condition)); //materialize the local scalar comparison byte + ctx.emitter + .instruction(&format!("movzx {}, al", result_reg)); // widen the local scalar comparison result + } + } +} + +/// Boxes the return expression, or null when eval exits without `return`. +fn emit_eval_local_scalar_return_cell( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + value: Option<&EvalLocalScalarExpr>, + boxing: EvalLocalScalarBoxing, +) { + if let Some(value) = value { + emit_eval_local_scalar_expr_value(ctx, program, value, 0); + emit_eval_local_scalar_box_current_result(ctx, value.ty, boxing); + } else { + emit_eval_local_scalar_null_cell(ctx, boxing); + } + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); +} + +/// Emits a boxed eval null cell into the result register. +fn emit_eval_local_scalar_null_cell(ctx: &mut FunctionContext<'_>, boxing: EvalLocalScalarBoxing) { + match boxing { + EvalLocalScalarBoxing::EvalRuntime => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLocalScalarBoxing::CoreRuntime => emit_eval_local_scalar_core_null_cell(ctx), + } +} + +/// Boxes the current int/bool result register as an eval Mixed cell. +fn emit_eval_local_scalar_box_current_result( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, + boxing: EvalLocalScalarBoxing, +) { + if matches!(boxing, EvalLocalScalarBoxing::CoreRuntime) { + if ty == EvalLocalScalarType::Null { + emit_eval_local_scalar_core_null_cell(ctx); + return; + } + emit_box_current_value_as_mixed(ctx.emitter, &ty.php_type()); + return; + } + if ty == EvalLocalScalarType::Null { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + return; + } + if ty == EvalLocalScalarType::String { + emit_eval_local_scalar_eval_runtime_string_cell(ctx); + return; + } + if ty == EvalLocalScalarType::Float { + emit_eval_local_scalar_eval_runtime_float_cell(ctx); + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + abi::emit_reg_move(ctx.emitter, arg_reg, result_reg); + let helper = match ty { + EvalLocalScalarType::Int => "__elephc_eval_value_int", + EvalLocalScalarType::Bool => "__elephc_eval_value_bool", + EvalLocalScalarType::Null | EvalLocalScalarType::Float | EvalLocalScalarType::String => { + unreachable!("non-integer eval cells are handled before integer helpers") + } + }; + let symbol = ctx.emitter.target.extern_symbol(helper); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes the current float result register with the eval runtime float helper. +fn emit_eval_local_scalar_eval_runtime_float_cell(ctx: &mut FunctionContext<'_>) { + let float_arg = abi::float_arg_reg_name(ctx.emitter.target, 0); + abi::emit_reg_move(ctx.emitter, float_arg, abi::float_result_reg(ctx.emitter)); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_float"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes the current string result registers with the eval runtime string helper. +fn emit_eval_local_scalar_eval_runtime_string_cell(ctx: &mut FunctionContext<'_>) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + let ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let len_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_reg_move(ctx.emitter, ptr_arg, ptr_reg); + abi::emit_reg_move(ctx.emitter, len_arg, len_reg); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_string"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes PHP null using the core runtime Mixed helper, without eval bridge symbols. +fn emit_eval_local_scalar_core_null_cell(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #8"); // materialize the core Mixed null runtime tag + ctx.emitter.instruction("mov x1, #0"); // null has no low payload word + ctx.emitter.instruction("mov x2, #0"); // null has no high payload word + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rax, 8"); // materialize the core Mixed null runtime tag + ctx.emitter.instruction("xor edi, edi"); // null has no low payload word + ctx.emitter.instruction("xor esi, esi"); // null has no high payload word + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + } + } +} + +/// Flushes defined local scalar slots into eval scope once native execution completes. +fn emit_eval_local_scalar_flush_defined_locals( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) { + for name in program.locals.keys() { + let skip_label = ctx.next_label("eval_local_flush_skip_undefined"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + emit_eval_local_scalar_load_local_value(ctx, program, name); + emit_eval_local_scalar_box_current_result( + ctx, + program.local_type(name), + EvalLocalScalarBoxing::EvalRuntime, + ); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_TEMP_CELL_OFFSET, + ); + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } else { + emit_eval_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } + ctx.emitter.label(&skip_label); + } +} + +/// Flushes defined local scalar slots directly into caller locals when no eval scope is needed. +fn emit_eval_local_scalar_flush_direct_locals( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + for name in program.locals.keys() { + let Some(target) = targets.get(name) else { + continue; + }; + let Some(slot) = target else { + continue; + }; + let skip_label = ctx.next_label("eval_local_direct_flush_skip_undefined"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + emit_eval_local_scalar_load_local_value(ctx, program, name); + emit_eval_local_scalar_store_current_result_to_direct_local( + ctx, + *slot, + program.local_type(name), + )?; + ctx.emitter.label(&skip_label); + } + Ok(()) +} + +/// Stores the current local-scalar result in one caller local slot. +fn emit_eval_local_scalar_store_current_result_to_direct_local( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + local_type: EvalLocalScalarType, +) -> Result<()> { + match ctx.local_php_type(slot)?.codegen_repr() { + target_ty @ (PhpType::Mixed | PhpType::Union(_)) => { + emit_eval_local_scalar_box_current_result( + ctx, + local_type, + EvalLocalScalarBoxing::CoreRuntime, + ); + // The caller slot may already hold a refcounted value written by + // native code before the eval; release it before overwriting. + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + ctx.store_current_result_to_local(slot) + } + PhpType::TaggedScalar => match local_type { + EvalLocalScalarType::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + ctx.store_current_result_to_local(slot) + } + EvalLocalScalarType::Int => { + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(ctx.emitter); + ctx.store_current_result_to_local(slot) + } + EvalLocalScalarType::Bool + | EvalLocalScalarType::Float + | EvalLocalScalarType::String => Err(CodegenIrError::unsupported( + "direct local scalar eval sync to tagged scalar".to_string(), + )), + }, + target_ty + if eval_local_scalar_direct_sync_type_supported(target_ty.clone(), local_type) => + { + ctx.store_current_result_to_local(slot) + } + target_ty => Err(CodegenIrError::unsupported(format!( + "direct local scalar eval sync to PHP type {:?}", + target_ty + ))), + } +} + +/// Boxes and echoes one AOT value, releasing the temporary box afterward. +fn emit_eval_literal_aot_echo( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) { + emit_eval_literal_aot_expr_cell(ctx, value, stack_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_mixed_write_stdout"); + abi::emit_pop_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); +} + +/// Stores one boxed scalar into the local or global eval scope used by bridge reloads. +fn emit_eval_literal_aot_scope_store( + ctx: &mut FunctionContext<'_>, + name: &str, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) -> Result<()> { + emit_eval_literal_aot_expr_cell(ctx, value, stack_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } else { + emit_eval_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } + Ok(()) +} + +/// Emits one AOT expression as an owned boxed Mixed value in the result register. +fn emit_eval_literal_aot_expr_cell( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) { + match value { + EvalLiteralAotExpr::Scalar(value) => emit_eval_literal_aot_scalar_cell(ctx, value), + EvalLiteralAotExpr::LoadVar(name) => { + emit_eval_literal_aot_scope_load(ctx, name, stack_depth); + } + EvalLiteralAotExpr::Binary { op, left, right } => { + emit_eval_literal_aot_binary_cell(ctx, op, left, right, stack_depth); + } + } +} + +/// Emits a boxed-Mixed binary operation and releases owned operand cells. +fn emit_eval_literal_aot_binary_cell( + ctx: &mut FunctionContext<'_>, + op: &EvalLiteralAotBinaryOp, + left: &EvalLiteralAotExpr, + right: &EvalLiteralAotExpr, + stack_depth: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + emit_eval_literal_aot_expr_cell(ctx, left, stack_depth); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_eval_literal_aot_expr_cell(ctx, right, stack_depth + 1); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + 16, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + 0, + ); + let symbol = ctx.emitter.target.extern_symbol(op.helper()); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 16); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 32); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); + abi::emit_pop_reg(ctx.emitter, result_reg); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Loads one variable from the eval scope and retains it as an owned Mixed cell. +fn emit_eval_literal_aot_scope_load(ctx: &mut FunctionContext<'_>, name: &str, stack_depth: usize) { + let out_cell_offset = stack_depth * 16; + let out_flags_offset = out_cell_offset + 8; + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_get_name(ctx, name, out_cell_offset, out_flags_offset); + } else { + emit_eval_scope_get_name(ctx, name, out_cell_offset, out_flags_offset); + } + let missing = ctx.next_label("eval_literal_aot_load_missing"); + let done = ctx.next_label("eval_literal_aot_load_done"); + emit_branch_if_scope_entry_missing_at(ctx, out_flags_offset, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, out_cell_offset); + let retain_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if retain_arg != result_reg { + abi::emit_reg_move(ctx.emitter, retain_arg, result_reg); + } + let retain = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_retain"); + abi::emit_call_label(ctx.emitter, &retain); + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + let null = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &null); + ctx.emitter.label(&done); +} + +/// Boxes one scalar AOT value into the standard eval `Mixed` return register. +fn emit_eval_literal_aot_scalar_cell(ctx: &mut FunctionContext<'_>, value: &EvalLiteralAotScalar) { + match value { + EvalLiteralAotScalar::Null => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + i64::from(*value), + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_bool"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + *value, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_int"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Float(value) => { + let label = ctx.data.add_float(*value); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::float_arg_reg_name(ctx.emitter.target, 0), + &label, + 0, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_float"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::String(value) => { + let (label, len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_string"); + abi::emit_call_label(ctx.emitter, &symbol); + } + } +} + +/// Calls `__elephc_eval_scope_get` for a direct AOT local-scope load. +fn emit_eval_scope_get_name( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_get` for a direct AOT global-scope load. +fn emit_eval_global_scope_get_name( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_set` for a direct AOT local-scope store. +fn emit_eval_scope_set_name(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_set` for a direct AOT global-scope store. +fn emit_eval_global_scope_set_name(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Emits an assembly marker for literal eval fragments that still use the bridge fallback. +fn emit_eval_literal_aot_marker(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let Some(fragment) = eval_literal_fragment(ctx, inst)? else { + return Ok(()); + }; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + &fragment, + ctx.module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_codegen(ctx, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_codegen(ctx, receiver, method, args) + }, + ); + let reason = plan + .fallback_reason() + .map(crate::eval_aot::EvalAotFallbackReason::description) + .unwrap_or("bridge fallback required"); + ctx.emitter.comment(&format!( + "eval literal AOT fallback: {} ({} bytes), using bridge fallback", + reason, + fragment.len(), + )); + Ok(()) +} + +/// Updates eval context source metadata for file, directory, and call-site line magic constants. +fn set_eval_call_site(ctx: &mut FunctionContext<'_>, inst: &Instruction) { + let Some(source_path) = ctx.module.source_path.as_deref() else { + return; + }; + load_eval_context_to_arg(ctx, 0); + let (file_label, file_len) = ctx.data.add_string(source_path.as_bytes()); + let file_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, file_arg, &file_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + file_len as i64, + ); + let dir = Path::new(source_path) + .parent() + .map(|path| path.display().to_string()) + .unwrap_or_default(); + let (dir_label, dir_len) = ctx.data.add_string(dir.as_bytes()); + let dir_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_symbol_address(ctx.emitter, dir_arg, &dir_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + dir_len as i64, + ); + let line = inst + .span + .and_then(|span| i64::try_from(span.line).ok()) + .unwrap_or(0); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + line, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_set_call_site"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Lowers a native positional call to a function declared by a prior `eval()` call. +pub(super) fn lower_eval_function_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + store_eval_function_call_args(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if inst.operands.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + inst.operands.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_call_function"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers a native call to a prior eval-declared function using an argument array/hash. +pub(super) fn lower_eval_function_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval function call array", 1)?; + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + let arg_array = expect_operand(inst, 0)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + let ty = ctx.load_value_to_result(arg_array)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, args_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_call_function_array"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers native construction of a class declared by a prior eval fragment. +pub(super) fn lower_eval_object_new( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let (name_label, name_len) = ctx.intern_class_name_data(expect_data(inst)?)?; + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + store_eval_function_call_args(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if inst.operands.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + inst.operands.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_new_object"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers fallback `new $class` construction through eval dynamic metadata. +pub(super) fn lower_eval_object_new_dynamic_fallback( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + miss_label: &str, +) -> Result<()> { + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("eval dynamic object new missing class operand") + })?; + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(constructor_args.len()); + let eval_miss_label = ctx.next_label("eval_dynamic_new_missing_class"); + let done_label = ctx.next_label("eval_dynamic_new_done"); + let name_ptr_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + let name_len_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_ptr_reg, 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_len_reg, 8); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_store_to_sp(ctx.emitter, name_ptr_reg, EVAL_CODE_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, name_len_reg, EVAL_CODE_LEN_OFFSET); + ensure_eval_context(ctx)?; + store_eval_function_call_operands(ctx, constructor_args, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let name_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_ptr_arg, EVAL_CODE_PTR_OFFSET); + let name_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_len_arg, EVAL_CODE_LEN_OFFSET); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if constructor_args.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + constructor_args.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_try_new_object"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &eval_miss_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&eval_miss_label); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_jump(ctx.emitter, miss_label); + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Lowers a method call that may dispatch to an eval-created dynamic object. +pub(super) fn lower_eval_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + method_name: &str, +) -> Result<()> { + let arg_count = inst.operands.len().saturating_sub(1); + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_method_call_stack_bytes(arg_count); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + let object_ty = ctx.load_value_to_result(object)?.codegen_repr(); + if !matches!(object_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &object_ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + store_eval_method_call_arg_pack(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); + let method_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_symbol_address(ctx.emitter, method_ptr_arg, &method_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + method_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers a native static-method call to an eval-declared dynamic class. +pub(super) fn lower_eval_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + method_name: &str, +) -> Result<()> { + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_static_method_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let target = format!("{}::{}", class_name, method_name); + let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + target_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers a late-static AOT-frame static method call through an active eval override. +pub(super) fn lower_eval_native_frame_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + method_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_static_method_call_stack_bytes(inst.operands.len()); + let miss_stack_label = ctx.next_label("eval_native_frame_static_method_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &method_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + method_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + emit_eval_result_as_type(ctx, &inst.result_php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst)?; + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + +/// Lowers a late-static AOT-frame static-property read through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let miss_stack_label = ctx.next_label("eval_native_frame_static_prop_get_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + property_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_property_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + emit_eval_result_as_type(ctx, &inst.result_php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst)?; + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + +/// Lowers a late-static AOT-frame static-property write through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_set( + ctx: &mut FunctionContext<'_>, + _inst: &Instruction, + value: ValueId, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let miss_stack_label = ctx.next_label("eval_native_frame_static_prop_set_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + property_len as i64, + ); + let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_property_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + +/// Lowers a callable-array dispatch through the eval bridge. +pub(super) fn lower_eval_callable_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, + arg_array: ValueId, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, arg_array, EVAL_CALLABLE_ARG_ARRAY_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); + let arg_array_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_array_arg, EVAL_CALLABLE_ARG_ARRAY_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_callable_call_array"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers an `is_callable()` probe through eval dynamic callable metadata. +pub(super) fn lower_eval_is_callable( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_is_callable"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers member-existence introspection through eval dynamic metadata. +pub(super) fn lower_eval_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + member: ValueId, + name: &str, +) -> Result<()> { + let lookup_kind = eval_member_lookup_kind(name)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, target, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, member, EVAL_CODE_PTR_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_TEMP_CELL_OFFSET); + let member_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, member_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + lookup_kind, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_member_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers class/interface/trait relation introspection through eval dynamic metadata. +pub(super) fn lower_eval_class_relation( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + name: &str, +) -> Result<()> { + let relation_kind = eval_class_relation_kind(name)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, target, EVAL_TEMP_CELL_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_TEMP_CELL_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + relation_kind, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_class_relation"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers object class-name introspection through the eval bridge. +pub(super) fn lower_eval_object_class_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + name: &str, +) -> Result<()> { + let lookup_kind = eval_class_lookup_kind(name)?; + let non_object_label = ctx.next_label("eval_object_class_non_object"); + let done_label = ctx.next_label("eval_object_class_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_object_operand(ctx, object)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &non_object_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + lookup_kind, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_class_name"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_eval_unboxed_string_result(ctx); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&non_object_label); + emit_eval_string_result(ctx, b""); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers object/class relation predicates through the eval bridge. +pub(super) fn lower_eval_object_is_a( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target_class: &str, + exclude_self: bool, +) -> Result<()> { + let false_label = ctx.next_label("eval_object_is_a_false"); + let done_label = ctx.next_label("eval_object_is_a_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_object_operand(ctx, object)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &false_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let (target_label, target_len) = ctx.data.add_string(target_class.as_bytes()); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + target_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + i64::from(exclude_self), + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_is_a"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers object/class relation predicates whose target is a runtime string or object cell. +pub(super) fn lower_eval_object_is_a_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target: ValueId, + exclude_self: bool, +) -> Result<()> { + let false_label = ctx.next_label("eval_object_is_a_dynamic_false"); + let invalid_label = ctx.next_label("eval_object_is_a_dynamic_invalid"); + let done_label = ctx.next_label("eval_object_is_a_dynamic_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, object, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, target, EVAL_CODE_PTR_OFFSET)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_CODE_PTR_OFFSET, + ); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_validate_eval_dynamic_instanceof_target(ctx, &invalid_label); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &false_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + i64::from(exclude_self), + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_is_a_dynamic"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &invalid_label); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&invalid_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_call_label(ctx.emitter, "__rt_instanceof_invalid_target"); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Returns true when the current function owns an eval context local. +pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { + eval_context_slot(ctx).is_ok() +} + +/// Lowers a post-eval dynamic function existence probe to the eval bridge ABI. +pub(super) fn lower_eval_function_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_function_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic class existence probe to the eval bridge ABI. +pub(super) fn lower_eval_class_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let (name_label, name_len) = ctx.intern_class_name_data(expect_data(inst)?)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_dynamic_class_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic constant existence probe to the eval bridge ABI. +pub(super) fn lower_eval_constant_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let constant_name = ctx.global_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(constant_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_constant_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic constant fetch to the eval bridge ABI. +pub(super) fn lower_eval_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let constant_name = ctx.global_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(constant_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_constant_fetch"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic class-like constant fetch to the eval bridge ABI. +pub(super) fn lower_eval_class_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + constant_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + let (constant_label, constant_len) = ctx.data.add_string(constant_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &constant_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + constant_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_class_constant_fetch"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic static-property read to the eval bridge ABI. +pub(super) fn lower_eval_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + property_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + property_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_property_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic static-property write to the eval bridge ABI. +pub(super) fn lower_eval_static_property_set( + ctx: &mut FunctionContext<'_>, + _inst: &Instruction, + value: ValueId, + class_name: &str, + property_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let target = format!("{}::{}", class_name, property_name); + let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &target_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + target_len as i64, + ); + let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_property_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + Ok(()) +} + +/// Returns the aligned scratch size for an eval-declared function call. +fn eval_function_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + arg_count * 8; + (bytes + 15) & !15 +} + +/// Returns the aligned scratch size for an eval dynamic method-call argument pack. +fn eval_method_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + 8 + arg_count * 8; + (bytes + 15) & !15 +} + +/// Returns the aligned scratch size for an eval dynamic static-method call. +fn eval_static_method_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + 8 + arg_count * 8; + (bytes + 15) & !15 +} + +/// Stores positional operands as boxed Mixed cells for the eval function-call ABI. +fn store_eval_function_call_args( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + store_eval_function_call_operands(ctx, &inst.operands, args_offset) +} + +/// Stores one operand slice as boxed Mixed cells for eval positional-call ABIs. +fn store_eval_function_call_operands( + ctx: &mut FunctionContext<'_>, + operands: &[ValueId], + args_offset: usize, +) -> Result<()> { + for (index, operand) in operands.iter().enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + index * 8); + } + Ok(()) +} + +/// Stores a count-prefixed positional argument pack for the eval method-call ABI. +fn store_eval_method_call_arg_pack( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + let arg_count = inst.operands.len().saturating_sub(1); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, arg_count as i64); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset); + for (index, operand) in inst.operands.iter().skip(1).enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + 8 + index * 8); + } + Ok(()) +} + +/// Stores all positional operands as a count-prefixed static-method argument pack. +fn store_eval_static_method_call_arg_pack( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, inst.operands.len() as i64); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset); + for (index, operand) in inst.operands.iter().enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + 8 + index * 8); + } + Ok(()) +} + +/// Stores an object operand as a boxed Mixed cell in eval scratch storage. +fn store_eval_object_operand(ctx: &mut FunctionContext<'_>, object: ValueId) -> Result<()> { + store_eval_mixed_operand_at(ctx, object, EVAL_TEMP_CELL_OFFSET) +} + +/// Stores one operand as a boxed Mixed cell at an eval scratch offset. +fn store_eval_mixed_operand_at( + ctx: &mut FunctionContext<'_>, + value: ValueId, + offset: usize, +) -> Result<()> { + let value_ty = ctx.load_value_to_result(value)?.codegen_repr(); + if !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &value_ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, offset); + Ok(()) +} + +/// Probes whether eval has a late-static called-class override for an AOT frame. +fn emit_eval_native_frame_override_probe( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + no_override_label: &str, +) { + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let out_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_temporary_stack_address(ctx.emitter, out_ptr_arg, EVAL_CALLED_CLASS_PTR_OFFSET); + let out_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_len_arg, EVAL_CALLED_CLASS_LEN_OFFSET); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_branch_if_int_result_zero(ctx.emitter, no_override_label); +} + +/// Converts an eval Mixed result cell to the concrete EIR type expected here. +fn emit_eval_result_as_type(ctx: &mut FunctionContext<'_>, result_ty: &PhpType) -> Result<()> { + match result_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => Ok(()), + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + Ok(()) + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + Ok(()) + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } + PhpType::Bool | PhpType::False => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + Ok(()) + } + PhpType::TaggedScalar => { + emit_eval_mixed_result_as_tagged_scalar(ctx); + Ok(()) + } + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 0x7fff_ffff_ffff_fffe, + ); + Ok(()) + } + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) + | PhpType::Buffer(_) + | PhpType::Callable + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) => { + emit_eval_unbox_mixed_to_owned_result(ctx, &result_ty.codegen_repr()); + Ok(()) + } + } +} + +/// Reorders an eval Mixed result cell into inline tagged-scalar result registers. +fn emit_eval_mixed_result_as_tagged_scalar(ctx: &mut FunctionContext<'_>) { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x9, x0"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov x0, x1"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov x1, x9"); // place the unboxed eval tag into the tagged-scalar tag register + } + Arch::X86_64 => { + ctx.emitter.instruction("mov r10, rax"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov rax, rdi"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov rdx, r10"); // place the unboxed eval tag into the tagged-scalar tag register + } + } +} + +/// Unboxes an eval Mixed result cell and retains concrete refcounted payloads. +fn emit_eval_unbox_mixed_to_owned_result(ctx: &mut FunctionContext<'_>, result_ty: &PhpType) { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_eval_move_unboxed_low_payload_to_result(ctx); + abi::emit_incref_if_refcounted(ctx.emitter, result_ty); +} + +/// Moves the low payload from `__rt_mixed_unbox` into the integer result register. +fn emit_eval_move_unboxed_low_payload_to_result(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, x1"); // return the unboxed eval low payload as the concrete result + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rax, rdi"); // return the unboxed eval low payload as the concrete result + } + } +} + +/// Boxes a raw eval predicate result when the enclosing IR value expects Mixed storage. +fn box_eval_bool_result_if_mixed(ctx: &mut FunctionContext<'_>, inst: &Instruction) { + if inst.result.is_some() && inst.result_php_type.codegen_repr() == PhpType::Mixed { + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } +} + +/// Returns the eval ABI discriminator for a class-name builtin. +fn eval_class_lookup_kind(name: &str) -> Result { + match name { + "get_class" => Ok(EVAL_CLASS_LOOKUP_GET_CLASS), + "get_parent_class" => Ok(EVAL_CLASS_LOOKUP_GET_PARENT_CLASS), + _ => Err(CodegenIrError::unsupported(format!( + "eval object class-name lookup {}", + name + ))), + } +} + +/// Returns the eval ABI discriminator for member-existence builtins. +fn eval_member_lookup_kind(name: &str) -> Result { + match name { + "method_exists" => Ok(EVAL_MEMBER_LOOKUP_METHOD_EXISTS), + "property_exists" => Ok(EVAL_MEMBER_LOOKUP_PROPERTY_EXISTS), + _ => Err(CodegenIrError::unsupported(format!( + "eval member-exists lookup {}", + name + ))), + } +} + +/// Returns the eval ABI discriminator for class/interface/trait relation builtins. +fn eval_class_relation_kind(name: &str) -> Result { + match name { + "class_implements" => Ok(EVAL_CLASS_RELATION_IMPLEMENTS), + "class_parents" => Ok(EVAL_CLASS_RELATION_PARENTS), + "class_uses" => Ok(EVAL_CLASS_RELATION_USES), + _ => Err(CodegenIrError::unsupported(format!( + "eval class-relation lookup {}", + name + ))), + } +} + +/// Branches when `__rt_mixed_unbox` did not expose an object payload. +fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter.instruction(&format!("b.ne {}", label)); // non-object values use the native false/empty fallback + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter.instruction(&format!("jne {}", label)); // non-object values use the native false/empty fallback + } + } +} + +/// Branches to the invalid-target fatal unless an eval dynamic target is string or object. +fn emit_validate_eval_dynamic_instanceof_target(ctx: &mut FunctionContext<'_>, label: &str) { + let ok_label = ctx.next_label("eval_object_is_a_dynamic_target_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("b.eq {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("b.eq {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter.instruction(&format!("b {}", label)); // reject every other dynamic instanceof target kind + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("je {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("je {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter.instruction(&format!("jmp {}", label)); // reject every other dynamic instanceof target kind + } + } + ctx.emitter.label(&ok_label); +} + +/// Branches when an eval C-ABI call returned a negative `int` sentinel. +fn emit_branch_if_eval_c_int_negative(ctx: &mut FunctionContext<'_>, label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + let branch = format!("tbnz w0, #31, {}", label); + ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel + } + Arch::X86_64 => { + ctx.emitter.instruction("test eax, eax"); // set flags from the C int result + ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel + } + } +} + +/// Reorders an unboxed eval string cell into the target string result registers. +fn emit_eval_unboxed_string_result(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the x86_64 string-result register + } +} + +/// Emits a borrowed string literal as the current native string result. +fn emit_eval_string_result(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + +/// Saves the loaded eval source string while scope setup calls use argument registers. +fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, EVAL_CODE_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, len_reg, EVAL_CODE_LEN_OFFSET); +} + +/// Ensures a persistent eval context exists and stores its handle in the scratch frame. +fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_context_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_context_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + register_eval_declared_symbols(ctx, offset); + register_eval_native_functions(ctx, offset)?; + register_eval_native_method_signatures(ctx, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_CONTEXT_HANDLE_OFFSET); + Ok(()) +} + +/// Returns the hidden frame slot that owns this function's persistent eval context. +fn eval_context_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalContext) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval context local")) +} + +/// Registers eligible AOT global functions with a newly allocated eval context. +fn register_eval_native_functions( + ctx: &mut FunctionContext<'_>, + context_offset: usize, +) -> Result<()> { + let registrations = eval_native_function_registrations(ctx); + for registration in registrations { + register_eval_native_function(ctx, context_offset, ®istration)?; + } + Ok(()) +} + +/// Registers eligible AOT method and constructor signatures with a newly allocated eval context. +fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context_offset: usize) { + for registration in eval_native_method_registrations(ctx) { + register_eval_native_method(ctx, context_offset, ®istration); + } + for registration in eval_native_constructor_registrations(ctx) { + register_eval_native_constructor(ctx, context_offset, ®istration); + } + for registration in eval_native_property_type_registrations(ctx) { + register_eval_native_property_type(ctx, context_offset, ®istration); + } + for registration in eval_native_abstract_property_registrations(ctx) { + register_eval_native_abstract_property(ctx, context_offset, ®istration); + } + for registration in eval_native_interface_property_registrations(ctx) { + register_eval_native_interface_property(ctx, context_offset, ®istration); + } + for registration in eval_native_property_default_registrations(ctx) { + register_eval_native_property_default(ctx, context_offset, ®istration); + } + for registration in eval_native_member_attribute_registrations(ctx) { + register_eval_native_member_attribute(ctx, context_offset, ®istration); + } + register_eval_native_class_parents(ctx, context_offset); +} + +/// Registers generated declared-name metadata with a newly allocated eval context. +fn register_eval_declared_symbols(ctx: &mut FunctionContext<'_>, context_offset: usize) { + let class_names = ctx.module.declared_class_names.clone(); + let interface_names = ctx.module.declared_interface_names.clone(); + let trait_names = ctx.module.declared_trait_names.clone(); + for name in class_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_class_name", + &name, + ); + } + for name in interface_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_interface_name", + &name, + ); + } + for name in trait_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_trait_name", + &name, + ); + } +} + +/// Emits one declared-name metadata registration call into the eval context. +fn register_eval_declared_symbol_name( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + symbol_name: &str, + name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx.emitter.target.extern_symbol(symbol_name); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Collects global PHP functions that can use the descriptor-invoker bridge. +fn eval_native_function_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + ctx.module + .functions + .iter() + .filter(|function| function_has_eval_metadata(function)) + .map(|function| EvalNativeFunctionRegistration { + name: function.name.clone(), + signature: function_signature_from_eir(function), + bridge_supported: function_signature_can_bridge_with_eval(function), + }) + .collect() +} + +/// Collects AOT method signatures whose metadata can be exposed to eval. +fn eval_native_method_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_instance_methods(class_name, class_info, &mut registrations); + collect_eval_native_static_methods(class_name, class_info, &mut registrations); + } + let mut interfaces = ctx.module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, interface_info) in interfaces { + collect_eval_native_interface_instance_methods( + interface_name, + interface_info, + &mut registrations, + ); + collect_eval_native_interface_static_methods( + interface_name, + interface_info, + &mut registrations, + ); + } + registrations +} + +/// Collects AOT constructors whose metadata can be exposed to eval. +fn eval_native_constructor_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let method_key = php_symbol_key("__construct"); + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + let Some(signature) = class_info.methods.get(&method_key) else { + continue; + }; + let bridge_supported = class_method_visibility_bridge_supported(class_info, &method_key) + && constructor_signature_can_bridge_with_eval(signature); + registrations.push(EvalNativeConstructorRegistration { + class_name: class_name.clone(), + signature: signature.clone(), + bridge_supported, + }); + } + registrations +} + +/// Collects AOT property types whose declared PHP type can be exposed to eval reflection. +fn eval_native_property_type_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_instance_property_types(class_name, class_info, &mut registrations); + collect_eval_native_static_property_types(class_name, class_info, &mut registrations); + } + registrations +} + +/// Collects AOT interface property contracts that eval can validate at declaration time. +fn eval_native_interface_property_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut interfaces = ctx.module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, interface_info) in interfaces { + let mut property_names = interface_info.property_order.iter().collect::>(); + if property_names.is_empty() { + property_names = interface_info.properties.keys().collect(); + property_names.sort(); + } + for property_name in property_names { + let Some(contract) = interface_info.properties.get(property_name) else { + continue; + }; + let Some(registration) = eval_native_interface_property_registration( + interface_name, + property_name, + contract, + ) else { + continue; + }; + registrations.push(registration); + } + } + registrations +} + +/// Collects AOT abstract class property contracts that eval can validate at declaration time. +fn eval_native_abstract_property_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + let mut property_names = class_info + .abstract_property_hooks + .keys() + .collect::>(); + property_names.sort(); + for property_name in property_names { + let Some(contract) = class_info.abstract_property_hooks.get(property_name) else { + continue; + }; + let Some(registration) = + eval_native_abstract_property_registration(class_name, property_name, contract) + else { + continue; + }; + registrations.push(registration); + } + } + registrations +} + +/// Converts one static abstract class property contract into eval-native metadata. +fn eval_native_abstract_property_registration( + class_name: &str, + property_name: &str, + contract: &PropertyHookContract, +) -> Option { + let requires_get = contract.get_type.is_some(); + let requires_set = contract.set_type.is_some(); + if !requires_get && !requires_set { + return None; + } + let type_spec = eval_native_interface_property_type_spec(contract)?; + Some(EvalNativeAbstractPropertyRegistration { + class_name: class_name.to_string(), + declaring_class_name: contract.declaring_type.clone(), + property_name: property_name.to_string(), + type_spec, + requires_get, + requires_set, + }) +} + +/// Converts one static interface property contract into eval-native metadata. +fn eval_native_interface_property_registration( + interface_name: &str, + property_name: &str, + contract: &PropertyHookContract, +) -> Option { + let requires_get = contract.get_type.is_some(); + let requires_set = contract.set_type.is_some(); + if !requires_get && !requires_set { + return None; + } + let type_spec = eval_native_interface_property_type_spec(contract)?; + Some(EvalNativeInterfacePropertyRegistration { + interface_name: interface_name.to_string(), + declaring_interface_name: contract.declaring_type.clone(), + property_name: property_name.to_string(), + type_spec, + requires_get, + requires_set, + }) +} + +/// Returns the single property type representation accepted by EvalIR metadata. +fn eval_native_interface_property_type_spec(contract: &PropertyHookContract) -> Option { + match (contract.get_type.as_ref(), contract.set_type.as_ref()) { + (Some(get_type), Some(set_type)) if get_type == set_type => { + eval_native_php_type_spec(get_type, false) + } + (Some(get_type), None) => eval_native_php_type_spec(get_type, false), + (None, Some(set_type)) => eval_native_php_type_spec(set_type, false), + _ => None, + } +} + +/// Collects AOT property defaults whose value can be exposed to eval reflection. +fn eval_native_property_default_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + let default_context = EvalNativeDefaultContext::for_class(ctx.module, class_name); + collect_eval_native_instance_property_defaults( + class_name, + class_info, + &default_context, + &mut registrations, + ); + collect_eval_native_static_property_defaults( + class_name, + class_info, + &default_context, + &mut registrations, + ); + } + registrations +} + +/// Collects AOT member attributes whose metadata can be exposed to eval reflection. +fn eval_native_member_attribute_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_class_attributes(class_name, class_info, &mut registrations); + collect_eval_native_method_attributes(class_name, class_info, &mut registrations); + collect_eval_native_property_attributes(class_name, class_info, &mut registrations); + collect_eval_native_class_constant_attributes(class_name, class_info, &mut registrations); + } + dedupe_eval_native_member_attribute_registrations(registrations) +} + +/// Removes inherited duplicate member-attribute registrations by normalized metadata key. +fn dedupe_eval_native_member_attribute_registrations( + registrations: Vec, +) -> Vec { + let mut seen = std::collections::HashSet::new(); + let mut unique = Vec::with_capacity(registrations.len()); + for registration in registrations { + let key = ( + registration.owner_kind, + php_symbol_key(®istration.class_name), + registration.member_name.clone(), + registration.attribute_name.clone(), + registration.attribute_args.clone(), + ); + if seen.insert(key) { + unique.push(registration); + } + } + unique +} + +/// Registers generated AOT class parent metadata for eval `parent::` resolution. +fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_offset: usize) { + let mut parents = ctx + .module + .class_infos + .iter() + .filter_map(|(class_name, class_info)| { + let parent_name = class_info.parent.as_deref()?; + Some(( + class_info.class_id, + class_name.clone(), + parent_name.to_string(), + )) + }) + .collect::>(); + parents.sort_by_key(|(class_id, _, _)| *class_id); + for (_, class_name, parent_name) in parents { + register_eval_native_class_parent(ctx, context_offset, &class_name, &parent_name); + } +} + +/// Adds class-level attribute metadata for one class-like symbol to eval registration. +fn collect_eval_native_class_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_CLASS, + class_name, + "", + &class_info.attribute_names, + &class_info.attribute_args, + registrations, + ); +} + +/// Adds method attribute metadata for one class to eval registration. +fn collect_eval_native_method_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.method_attribute_names.iter().collect::>(); + methods.sort_by_key(|(method_name, _)| method_name.as_str()); + for (method_name, attribute_names) in methods { + let attribute_args = class_info + .method_attribute_args + .get(method_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_METHOD, + eval_native_method_declaring_class(class_name, class_info, method_name), + method_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + +/// Adds property attribute metadata for one class to eval registration. +fn collect_eval_native_property_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut properties = class_info + .property_attribute_names + .iter() + .collect::>(); + properties.sort_by_key(|(property_name, _)| property_name.as_str()); + for (property_name, attribute_names) in properties { + let attribute_args = class_info + .property_attribute_args + .get(property_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_PROPERTY, + eval_native_property_attribute_declaring_class(class_name, class_info, property_name), + property_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + +/// Adds class-constant attribute metadata for one class to eval registration. +fn collect_eval_native_class_constant_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut constants = class_info + .constant_attribute_names + .iter() + .collect::>(); + constants.sort_by_key(|(constant_name, _)| constant_name.as_str()); + for (constant_name, attribute_names) in constants { + let attribute_args = class_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT, + class_name, + constant_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + +/// Adds aligned attribute name/argument metadata for one AOT member. +fn collect_eval_native_member_attributes( + owner_kind: u8, + class_name: &str, + member_name: &str, + attribute_names: &[String], + attribute_args: &[Option>], + registrations: &mut Vec, +) { + for (index, attribute_name) in attribute_names.iter().enumerate() { + let Some(args) = attribute_args.get(index).cloned().flatten() else { + continue; + }; + let attribute_args = if eval_native_member_attribute_args_supported(&args) { + Some(args) + } else { + None + }; + registrations.push(EvalNativeMemberAttributeRegistration { + owner_kind, + class_name: class_name.to_string(), + member_name: member_name.to_string(), + attribute_name: attribute_name.clone(), + attribute_args, + }); + } +} + +/// Adds supported instance-property default metadata for one class to eval registration. +fn collect_eval_native_instance_property_defaults( + class_name: &str, + class_info: &ClassInfo, + default_context: &EvalNativeDefaultContext<'_>, + registrations: &mut Vec, +) { + for (slot, (property_name, _)) in class_info.properties.iter().enumerate() { + let default = class_info.defaults.get(slot).and_then(Option::as_ref); + let is_declared = class_info.property_slot_is_declared(slot, property_name); + let is_abstract = class_info.abstract_properties.contains(property_name); + let Some(default) = + eval_native_property_default(default, is_declared, is_abstract, default_context) + else { + continue; + }; + registrations.push(EvalNativePropertyDefaultRegistration { + class_name: eval_native_instance_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + default, + }); + } +} + +/// Adds supported static-property default metadata for one class to eval registration. +fn collect_eval_native_static_property_defaults( + class_name: &str, + class_info: &ClassInfo, + default_context: &EvalNativeDefaultContext<'_>, + registrations: &mut Vec, +) { + for (slot, (property_name, _)) in class_info.static_properties.iter().enumerate() { + let default = class_info + .static_defaults + .get(slot) + .and_then(Option::as_ref); + let is_declared = class_info + .declared_static_properties + .contains(property_name); + let Some(default) = + eval_native_property_default(default, is_declared, false, default_context) + else { + continue; + }; + registrations.push(EvalNativePropertyDefaultRegistration { + class_name: eval_native_static_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + default, + }); + } +} + +/// Adds declared instance-property type metadata for one class to eval registration. +fn collect_eval_native_instance_property_types( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (slot, (property_name, php_type)) in class_info.properties.iter().enumerate() { + if !class_info.property_slot_is_declared(slot, property_name) { + continue; + } + let Some(type_spec) = eval_native_php_type_spec(php_type, false) else { + continue; + }; + registrations.push(EvalNativePropertyTypeRegistration { + class_name: eval_native_instance_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + type_spec, + }); + } +} + +/// Adds declared static-property type metadata for one class to eval registration. +fn collect_eval_native_static_property_types( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (property_name, php_type) in &class_info.static_properties { + if !class_info + .declared_static_properties + .contains(property_name) + { + continue; + } + let Some(type_spec) = eval_native_php_type_spec(php_type, false) else { + continue; + }; + registrations.push(EvalNativePropertyTypeRegistration { + class_name: eval_native_static_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + type_spec, + }); + } +} + +/// Returns the class name that declares one AOT instance property row. +fn eval_native_instance_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one AOT static property row. +fn eval_native_static_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .static_property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one AOT method metadata row. +fn eval_native_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .method_impl_classes + .get(method_name) + .or_else(|| class_info.static_method_impl_classes.get(method_name)) + .or_else(|| class_info.method_declaring_classes.get(method_name)) + .or_else(|| class_info.static_method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one AOT property attribute row. +fn eval_native_property_attribute_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .or_else(|| { + class_info + .static_property_declaring_classes + .get(property_name) + }) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Adds instance method metadata for one class to eval signature registration. +fn collect_eval_native_instance_methods( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + if method_name == "__construct" { + continue; + } + let bridge_supported = class_method_visibility_bridge_supported(class_info, method_name) + && method_signature_can_bridge_with_eval(signature); + registrations.push(EvalNativeMethodRegistration { + class_name: class_name.to_string(), + method_name: method_name.clone(), + is_static: false, + signature: signature.clone(), + bridge_supported, + }); + } +} + +/// Adds static method metadata for one class to eval signature registration. +fn collect_eval_native_static_methods( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + let bridge_supported = + class_static_method_visibility_bridge_supported(class_info, method_name) + && method_signature_can_bridge_with_eval(signature); + registrations.push(EvalNativeMethodRegistration { + class_name: class_name.to_string(), + method_name: method_name.clone(), + is_static: true, + signature: signature.clone(), + bridge_supported, + }); + } +} + +/// Adds interface instance-method metadata to eval signature registration. +fn collect_eval_native_interface_instance_methods( + interface_name: &str, + interface_info: &InterfaceInfo, + registrations: &mut Vec, +) { + let mut methods = interface_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + registrations.push(EvalNativeMethodRegistration { + class_name: eval_native_interface_method_declaring_interface( + interface_name, + interface_info, + method_name, + ) + .to_string(), + method_name: method_name.clone(), + is_static: false, + signature: signature.clone(), + bridge_supported: false, + }); + } +} + +/// Adds interface static-method metadata to eval signature registration. +fn collect_eval_native_interface_static_methods( + interface_name: &str, + interface_info: &InterfaceInfo, + registrations: &mut Vec, +) { + let mut methods = interface_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + registrations.push(EvalNativeMethodRegistration { + class_name: eval_native_interface_static_method_declaring_interface( + interface_name, + interface_info, + method_name, + ) + .to_string(), + method_name: method_name.clone(), + is_static: true, + signature: signature.clone(), + bridge_supported: false, + }); + } +} + +/// Returns the interface name that declares one AOT interface instance method row. +fn eval_native_interface_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns the interface name that declares one AOT interface static method row. +fn eval_native_interface_static_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .static_method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns true when a module function should expose metadata to eval fragments. +fn function_has_eval_metadata(function: &Function) -> bool { + !function.flags.is_main && !function.name.starts_with('_') +} + +/// Returns true when eval can dispatch a native function through the generated bridge. +fn function_signature_can_bridge_with_eval(function: &Function) -> bool { + function + .params + .iter() + .all(|param| !param.by_ref || eval_native_function_ref_param_supported(¶m.php_type)) +} + +/// Returns true when a native function by-reference parameter can use eval bridge staging. +fn eval_native_function_ref_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Bool + | PhpType::Float + | PhpType::Int + | PhpType::Iterable + | PhpType::Mixed + | PhpType::Object(_) + | PhpType::Str + ) +} + +/// Returns true when eval can dispatch a native method through the generated bridge. +fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { + eval_signature_ref_params_supported(signature) + && signature + .params + .iter() + .all(|(_, ty)| eval_native_method_param_supported(ty)) + && eval_native_method_return_supported(&signature.return_type) +} + +/// Returns true when eval can dispatch a native constructor through the generated bridge. +fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { + eval_signature_ref_params_supported(signature) + && signature + .params + .iter() + .all(|(_, ty)| eval_native_constructor_param_supported(ty)) +} + +/// Returns true when one native method argument type fits the eval method bridge. +fn eval_native_method_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Iterable + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + ) +} + +/// Returns true when one native constructor argument type fits the eval bridge. +fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Iterable + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + ) +} + +/// Returns true when one native method return type can be boxed back for eval. +fn eval_native_method_return_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Void + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Callable + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Iterable + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Returns true when the indexed parameter is the signature's variadic slot. +fn signature_param_is_variadic(signature: &FunctionSig, index: usize, param_name: &str) -> bool { + signature.variadic.as_deref().is_some_and(|variadic| { + variadic == param_name + || signature + .params + .get(index) + .is_some_and(|(name, _)| name == variadic) + }) +} + +/// Returns generated type specs for declared native callable parameters. +fn eval_native_callable_param_type_specs(signature: &FunctionSig) -> Vec> { + signature + .params + .iter() + .enumerate() + .map(|(index, (_, php_type))| { + if !signature + .declared_params + .get(index) + .copied() + .unwrap_or(false) + { + return None; + } + signature + .param_type_exprs + .get(index) + .and_then(Option::as_ref) + .and_then(eval_native_type_expr_spec) + .or_else(|| eval_native_php_type_spec(php_type, false)) + }) + .collect() +} + +/// Returns a generated type spec for a declared native callable return type. +fn eval_native_callable_return_type_spec(signature: &FunctionSig) -> Option { + signature + .declared_return + .then(|| eval_native_php_type_spec(&signature.return_type, true)) + .flatten() +} + +/// Formats one parsed PHP type expression for eval native metadata registration. +fn eval_native_type_expr_spec(type_expr: &TypeExpr) -> Option { + match type_expr { + TypeExpr::Int => Some("int".to_string()), + TypeExpr::Float => Some("float".to_string()), + TypeExpr::Bool => Some("bool".to_string()), + TypeExpr::False => Some("false".to_string()), + TypeExpr::Str => Some("string".to_string()), + TypeExpr::Void => Some("null".to_string()), + TypeExpr::Never => None, + TypeExpr::Iterable => Some("iterable".to_string()), + TypeExpr::Array(_) => Some("array".to_string()), + TypeExpr::Ptr(_) | TypeExpr::Buffer(_) => None, + TypeExpr::Named(name) => Some(name.as_str().to_string()), + TypeExpr::Nullable(inner) => { + let inner = eval_native_type_expr_spec(inner)?; + Some(format!("?{}", inner)) + } + TypeExpr::Union(members) => eval_native_type_expr_member_specs(members, "|"), + TypeExpr::Intersection(members) => eval_native_type_expr_member_specs(members, "&"), + } +} + +/// Formats a compound parsed type expression with the requested separator. +fn eval_native_type_expr_member_specs(members: &[TypeExpr], separator: &str) -> Option { + members + .iter() + .map(eval_native_type_expr_spec) + .collect::>>() + .map(|members| members.join(separator)) +} + +/// Formats one checked PHP type for eval native metadata registration. +fn eval_native_php_type_spec(php_type: &PhpType, allow_return_atoms: bool) -> Option { + match php_type { + PhpType::Int => Some("int".to_string()), + PhpType::Float => Some("float".to_string()), + PhpType::Str => Some("string".to_string()), + PhpType::Bool => Some("bool".to_string()), + PhpType::False => Some("false".to_string()), + PhpType::Void if allow_return_atoms => Some("void".to_string()), + PhpType::Void => Some("null".to_string()), + PhpType::Never if allow_return_atoms => Some("never".to_string()), + PhpType::Never => None, + PhpType::Iterable => Some("iterable".to_string()), + PhpType::Mixed => Some("mixed".to_string()), + PhpType::Array(_) | PhpType::AssocArray { .. } => Some("array".to_string()), + PhpType::Callable => Some("callable".to_string()), + PhpType::Object(name) if name.is_empty() => Some("object".to_string()), + PhpType::Object(name) => Some(name.clone()), + PhpType::Union(members) => eval_native_php_type_member_specs(members), + PhpType::Buffer(_) + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) + | PhpType::TaggedScalar => None, + } +} + +/// Formats union members from checked PHP types for eval native metadata registration. +fn eval_native_php_type_member_specs(members: &[PhpType]) -> Option { + members + .iter() + .map(|member| eval_native_php_type_spec(member, false)) + .collect::>>() + .map(|members| members.join("|")) +} + +/// Converts a PHP signature default into the compact eval bridge default ABI. +fn eval_native_callable_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, +) -> Option { + eval_native_callable_default_at(expr, default_context, 0) +} + +/// Converts a PHP default expression while preserving a recursion limit for constants. +fn eval_native_callable_default_at( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if depth > MAX_NATIVE_DEFAULT_CONSTANT_DEPTH { + return None; + } + eval_native_literal_default(expr) + .or_else(|| eval_native_object_default(expr, default_context, depth)) + .or_else(|| eval_native_array_default(expr, default_context, depth)) + .or_else(|| eval_native_constant_expression_default(expr, default_context, depth)) +} + +/// Converts representable pure constant expressions into native eval defaults. +fn eval_native_constant_expression_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match &expr.kind { + ExprKind::ConstRef(name) => { + eval_native_global_constant_default(default_context, name, depth + 1) + } + ExprKind::ClassConstant { receiver } => { + eval_native_static_receiver_name(default_context, receiver) + .map(EvalNativeCallableDefault::String) + } + ExprKind::ScopedConstantAccess { receiver, name } => { + eval_native_scoped_constant_default(default_context, receiver, name, depth + 1) + } + ExprKind::BinaryOp { left, op, right } => { + eval_native_binary_expression_default(left, op, right, default_context, depth + 1) + } + ExprKind::Not(inner) => eval_native_default_truthy(&eval_native_callable_default_at( + inner, + default_context, + depth + 1, + )?) + .map(|value| eval_native_bool_default(!value)), + ExprKind::BitNot(inner) => eval_native_default_int(inner, default_context, depth + 1) + .map(|value| eval_native_int_default(!value)), + ExprKind::NullCoalesce { value, default } => { + let value = eval_native_callable_default_at(value, default_context, depth + 1)?; + if eval_native_default_is_null(&value) { + eval_native_callable_default_at(default, default_context, depth + 1) + } else { + Some(value) + } + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + if eval_native_default_truthy(&eval_native_callable_default_at( + condition, + default_context, + depth + 1, + )?)? { + eval_native_callable_default_at(then_expr, default_context, depth + 1) + } else { + eval_native_callable_default_at(else_expr, default_context, depth + 1) + } + } + ExprKind::ShortTernary { value, default } => { + let value = eval_native_callable_default_at(value, default_context, depth + 1)?; + if eval_native_default_truthy(&value)? { + Some(value) + } else { + eval_native_callable_default_at(default, default_context, depth + 1) + } + } + _ => None, + } +} + +/// Converts one supported binary constant expression into a native eval default. +fn eval_native_binary_expression_default( + left: &Expr, + op: &BinOp, + right: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match op { + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow => { + eval_native_numeric_binary_default(left, op, right, default_context, depth + 1) + } + BinOp::Mod => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = eval_native_default_int(right, default_context, depth + 1)?; + (right != 0).then(|| eval_native_int_default(left % right)) + } + BinOp::Concat => { + let left = eval_native_default_string(left, default_context, depth + 1)?; + let right = eval_native_default_string(right, default_context, depth + 1)?; + Some(EvalNativeCallableDefault::String(format!("{left}{right}"))) + } + BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = eval_native_default_int(right, default_context, depth + 1)?; + let value = match op { + BinOp::BitAnd => left & right, + BinOp::BitOr => left | right, + BinOp::BitXor => left ^ right, + _ => unreachable!("bitwise default operator was prefiltered"), + }; + Some(eval_native_int_default(value)) + } + BinOp::ShiftLeft | BinOp::ShiftRight => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = + u32::try_from(eval_native_default_int(right, default_context, depth + 1)?).ok()?; + let value = match op { + BinOp::ShiftLeft => left.checked_shl(right), + BinOp::ShiftRight => left.checked_shr(right), + _ => unreachable!("shift default operator was prefiltered"), + }?; + Some(eval_native_int_default(value)) + } + BinOp::And | BinOp::Or | BinOp::Xor => { + let left = eval_native_default_truthy(&eval_native_callable_default_at( + left, + default_context, + depth + 1, + )?)?; + let right = eval_native_default_truthy(&eval_native_callable_default_at( + right, + default_context, + depth + 1, + )?)?; + let value = match op { + BinOp::And => left && right, + BinOp::Or => left || right, + BinOp::Xor => left ^ right, + _ => unreachable!("logical default operator was prefiltered"), + }; + Some(eval_native_bool_default(value)) + } + BinOp::NullCoalesce => { + let left = eval_native_callable_default_at(left, default_context, depth + 1)?; + if eval_native_default_is_null(&left) { + eval_native_callable_default_at(right, default_context, depth + 1) + } else { + Some(left) + } + } + _ => None, + } +} + +/// Converts one supported arithmetic expression into a native eval default. +fn eval_native_numeric_binary_default( + left: &Expr, + op: &BinOp, + right: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if let (Some(left), Some(right)) = ( + eval_native_default_int(left, default_context, depth + 1), + eval_native_default_int(right, default_context, depth + 1), + ) { + return match op { + BinOp::Add => left.checked_add(right).map(eval_native_int_default), + BinOp::Sub => left.checked_sub(right).map(eval_native_int_default), + BinOp::Mul => left.checked_mul(right).map(eval_native_int_default), + BinOp::Div if right != 0 => Some(eval_native_float_default(left as f64 / right as f64)), + BinOp::Pow => { + let value = (left as f64).powf(right as f64); + value.is_finite().then(|| eval_native_float_default(value)) + } + _ => None, + }; + } + + let left = eval_native_default_numeric(left, default_context, depth + 1)?; + let right = eval_native_default_numeric(right, default_context, depth + 1)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + value.is_finite().then(|| eval_native_float_default(value)) +} + +/// Builds one bool default metadata value. +fn eval_native_bool_default(value: bool) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload: i64::from(value), + } +} + +/// Builds one int default metadata value. +fn eval_native_int_default(value: i64) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload: value, + } +} + +/// Builds one float default metadata value. +fn eval_native_float_default(value: f64) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: value.to_bits() as i64, + } +} + +/// Returns true when one default metadata value is PHP `null`. +fn eval_native_default_is_null(default: &EvalNativeCallableDefault) -> bool { + matches!( + default, + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } + ) +} + +/// Returns PHP truthiness for one representable native eval default. +fn eval_native_default_truthy(default: &EvalNativeCallableDefault) -> Option { + match default { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } => Some(false), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload, + } => Some(*payload != 0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(*payload != 0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(f64::from_bits(*payload as u64) != 0.0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_EMPTY_ARRAY, + .. + } => Some(false), + EvalNativeCallableDefault::String(value) => Some(!value.is_empty() && value != "0"), + EvalNativeCallableDefault::Array(_) | EvalNativeCallableDefault::Object { .. } => None, + EvalNativeCallableDefault::Scalar { .. } => None, + } +} + +/// Extracts an int value from one representable default expression. +fn eval_native_default_int( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(payload), + _ => None, + } +} + +/// Extracts a numeric value from one representable default expression. +fn eval_native_default_numeric( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(payload as f64), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(f64::from_bits(payload as u64)), + _ => None, + } +} + +/// Extracts a string value from one representable default expression. +fn eval_native_default_string( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::String(value) => Some(value), + _ => None, + } +} + +/// Converts scalar/string/empty-array defaults into the compact eval bridge default ABI. +fn eval_native_literal_default(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + payload: 0, + }), + ExprKind::BoolLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload: i64::from(*value), + }), + ExprKind::IntLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload: *value, + }), + ExprKind::FloatLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: value.to_bits() as i64, + }), + ExprKind::StringLiteral(value) => Some(EvalNativeCallableDefault::String(value.clone())), + ExprKind::ArrayLiteral(elements) if elements.is_empty() => { + Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_EMPTY_ARRAY, + payload: 0, + }) + } + ExprKind::Negate(inner) => eval_native_callable_negated_default(inner), + _ => None, + } +} + +/// Converts supported object-valued defaults into compact eval bridge metadata. +fn eval_native_object_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + let ExprKind::NewObject { class_name, args } = &expr.kind else { + return None; + }; + if args.len() > MAX_NATIVE_OBJECT_DEFAULT_ARGS { + return None; + } + let mut default_args = Vec::with_capacity(args.len()); + for arg in args { + default_args.push(eval_native_object_default_arg( + arg, + default_context, + depth + 1, + )?); + } + Some(EvalNativeCallableDefault::Object { + class_name: class_name.as_canonical(), + args: default_args, + }) +} + +/// Converts one object-valued default constructor argument into bridge metadata. +fn eval_native_object_default_arg( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match &expr.kind { + ExprKind::NamedArg { name, value } => Some(EvalNativeCallableObjectDefaultArg { + name: Some(name.clone()), + default: eval_native_callable_default_at(value, default_context, depth + 1)?, + }), + ExprKind::Spread(_) => None, + _ => Some(EvalNativeCallableObjectDefaultArg { + name: None, + default: eval_native_callable_default_at(expr, default_context, depth + 1)?, + }), + } +} + +/// Converts supported array-valued defaults into compact eval bridge metadata. +fn eval_native_array_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match &expr.kind { + ExprKind::ArrayLiteral(elements) => { + let mut default_elements = Vec::with_capacity(elements.len()); + for element in elements { + if matches!(element.kind, ExprKind::Spread(_)) { + return None; + } + default_elements.push(EvalNativeCallableArrayDefaultElement { + key: None, + default: eval_native_callable_default_at(element, default_context, depth + 1)?, + }); + } + Some(EvalNativeCallableDefault::Array(default_elements)) + } + ExprKind::ArrayLiteralAssoc(elements) => { + let mut default_elements = Vec::with_capacity(elements.len()); + for (key, value) in elements { + default_elements.push(EvalNativeCallableArrayDefaultElement { + key: Some(eval_native_array_default_key( + key, + default_context, + depth + 1, + )?), + default: eval_native_callable_default_at(value, default_context, depth + 1)?, + }); + } + Some(EvalNativeCallableDefault::Array(default_elements)) + } + _ => None, + } +} + +/// Converts one supported static array key into bridge metadata. +fn eval_native_array_default_key( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if let Some(key) = eval_native_literal_array_default_key(expr) { + return Some(key); + } + match eval_native_callable_default_at(expr, default_context, depth + 1)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } => Some(EvalNativeCallableArrayDefaultKey::String(String::new())), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int( + (payload != 0) as i64, + )), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int(payload)), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int( + f64::from_bits(payload as u64) as i64, + )), + EvalNativeCallableDefault::String(value) => eval_native_string_array_default_key(&value), + _ => None, + } +} + +/// Resolves and materializes one global constant default expression. +fn eval_native_global_constant_default( + default_context: &EvalNativeDefaultContext<'_>, + name: &str, + depth: usize, +) -> Option { + let expr_kind = default_context + .module + .global_constants + .get(name) + .or_else(|| { + default_context + .module + .global_constants + .get(name.trim_start_matches('\\')) + }) + .map(|(expr_kind, _)| expr_kind.clone())?; + let expr = Expr::new(expr_kind, crate::span::Span::dummy()); + eval_native_callable_default_at(&expr, default_context, depth + 1) +} + +/// Resolves and materializes one class-like constant default expression. +fn eval_native_scoped_constant_default( + default_context: &EvalNativeDefaultContext<'_>, + receiver: &StaticReceiver, + constant_name: &str, + depth: usize, +) -> Option { + let class_name = eval_native_static_receiver_name(default_context, receiver)?; + if let Some((declaring_name, value)) = + eval_native_class_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + if let Some((declaring_name, value)) = + eval_native_interface_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + if let Some((declaring_name, value)) = + eval_native_trait_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + None +} + +/// Resolves `self`, `static`, `parent`, or a named receiver for default constants. +fn eval_native_static_receiver_name( + default_context: &EvalNativeDefaultContext<'_>, + receiver: &StaticReceiver, +) -> Option { + match receiver { + StaticReceiver::Named(name) => { + Some(name.as_canonical().trim_start_matches('\\').to_string()) + } + StaticReceiver::Self_ | StaticReceiver::Static => { + default_context.current_class.map(str::to_string) + } + StaticReceiver::Parent => { + let current = default_context.current_class?; + resolve_eval_native_default_class(default_context.module, current) + .and_then(|(_, class_info)| class_info.parent.clone()) + } + } +} + +/// Looks up a class constant expression, including inherited parent classes. +fn eval_native_class_constant_expr<'a>( + module: &'a Module, + class_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let (resolved_name, class_info) = resolve_eval_native_default_class(module, class_name)?; + if let Some(value) = class_info.constants.get(constant_name) { + return Some((resolved_name, value)); + } + for interface_name in &class_info.interfaces { + if let Some(value) = + eval_native_interface_constant_expr(module, interface_name, constant_name) + { + return Some(value); + } + } + if let Some(parent_name) = class_info.parent.as_deref() { + return eval_native_class_constant_expr(module, parent_name, constant_name); + } + None +} + +/// Looks up an interface constant expression, including inherited interfaces. +fn eval_native_interface_constant_expr<'a>( + module: &'a Module, + interface_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let mut visited = std::collections::HashSet::new(); + let mut queue = vec![interface_name.to_string()]; + while let Some(name) = queue.pop() { + let Some((resolved_name, interface_info)) = + resolve_eval_native_default_interface(module, &name) + else { + continue; + }; + if !visited.insert(php_symbol_key(resolved_name.trim_start_matches('\\'))) { + continue; + } + if let Some(value) = interface_info.constants.get(constant_name) { + return Some((resolved_name, value)); + } + queue.extend(interface_info.parents.iter().cloned()); + } + None +} + +/// Looks up a direct trait constant expression by PHP-style trait name. +fn eval_native_trait_constant_expr<'a>( + module: &'a Module, + trait_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let trait_key = php_symbol_key(trait_name.trim_start_matches('\\')); + let resolved_name = module + .trait_table + .names + .iter() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == trait_key)?; + let value = module + .declared_trait_constants + .get(resolved_name) + .and_then(|constants| constants.get(constant_name))?; + Some((resolved_name.as_str(), value)) +} + +/// Looks up class metadata by PHP-style case-insensitive name. +fn resolve_eval_native_default_class<'a>( + module: &'a Module, + class_name: &str, +) -> Option<(&'a str, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_eval_native_default_interface<'a>( + module: &'a Module, + interface_name: &str, +) -> Option<(&'a str, &'a InterfaceInfo)> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + module + .interface_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Converts one literal static array key into bridge metadata. +fn eval_native_literal_array_default_key(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(EvalNativeCallableArrayDefaultKey::Int(*value)), + ExprKind::BoolLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int(i64::from(*value))) + } + ExprKind::FloatLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int(*value as i64)) + } + ExprKind::StringLiteral(value) => eval_native_string_array_default_key(value), + ExprKind::Null => Some(EvalNativeCallableArrayDefaultKey::String(String::new())), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value + .checked_neg() + .map(EvalNativeCallableArrayDefaultKey::Int), + ExprKind::FloatLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int((-*value) as i64)) + } + _ => None, + }, + _ => None, + } +} + +/// Normalizes one string default-array key to PHP's integer-key rules. +fn eval_native_string_array_default_key(value: &str) -> Option { + if is_php_integer_array_key(value) { + value + .parse::() + .ok() + .map(EvalNativeCallableArrayDefaultKey::Int) + } else { + Some(EvalNativeCallableArrayDefaultKey::String(value.to_string())) + } +} + +/// Converts supported property defaults into the compact eval bridge default ABI. +fn eval_native_property_default( + default: Option<&Expr>, + is_declared: bool, + is_abstract: bool, + default_context: &EvalNativeDefaultContext<'_>, +) -> Option { + if let Some(default) = default { + return eval_native_literal_default(default) + .or_else(|| eval_native_array_default(default, default_context, 0)); + } + (!is_declared && !is_abstract).then_some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + payload: 0, + }) +} + +/// Converts a negated literal default into the compact eval bridge default ABI. +fn eval_native_callable_negated_default(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => { + value + .checked_neg() + .map(|payload| EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + }) + } + ExprKind::FloatLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: (-*value).to_bits() as i64, + }), + _ => None, + } +} + +/// Encodes an object-valued native callable default for libelephc-magician. +fn encode_eval_native_object_default(default: &EvalNativeCallableDefault) -> Vec { + let EvalNativeCallableDefault::Object { class_name, args } = default else { + return Vec::new(); + }; + let mut bytes = Vec::new(); + encode_eval_native_default_string(&mut bytes, class_name); + bytes.push(args.len() as u8); + for arg in args { + encode_eval_native_object_default_arg(&mut bytes, arg); + } + bytes +} + +/// Encodes an array-valued native callable default for libelephc-magician. +fn encode_eval_native_array_default(default: &EvalNativeCallableDefault) -> Vec { + let EvalNativeCallableDefault::Array(elements) = default else { + return Vec::new(); + }; + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + encode_eval_native_array_default_element(&mut bytes, element); + } + bytes +} + +/// Encodes one array-default element and its optional static key. +fn encode_eval_native_array_default_element( + bytes: &mut Vec, + element: &EvalNativeCallableArrayDefaultElement, +) { + match &element.key { + Some(EvalNativeCallableArrayDefaultKey::Int(value)) => { + bytes.push(NATIVE_ARRAY_DEFAULT_KEY_INT); + bytes.extend_from_slice(&value.to_le_bytes()); + } + Some(EvalNativeCallableArrayDefaultKey::String(value)) => { + bytes.push(NATIVE_ARRAY_DEFAULT_KEY_STRING); + encode_eval_native_default_string(bytes, value); + } + None => bytes.push(NATIVE_ARRAY_DEFAULT_KEY_AUTO), + } + encode_eval_native_object_default_arg_value(bytes, &element.default); +} + +/// Encodes one object-default constructor argument for libelephc-magician. +fn encode_eval_native_object_default_arg( + bytes: &mut Vec, + arg: &EvalNativeCallableObjectDefaultArg, +) { + if let Some(name) = &arg.name { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_NAMED); + encode_eval_native_default_string(bytes, name); + } + encode_eval_native_object_default_arg_value(bytes, &arg.default); +} + +/// Encodes one object-default constructor argument value for libelephc-magician. +fn encode_eval_native_object_default_arg_value( + bytes: &mut Vec, + default: &EvalNativeCallableDefault, +) { + match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_SCALAR); + bytes.extend_from_slice(&(*kind as u64).to_le_bytes()); + bytes.extend_from_slice(&(*payload as u64).to_le_bytes()); + } + EvalNativeCallableDefault::String(value) => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_STRING); + encode_eval_native_default_string(bytes, value); + } + EvalNativeCallableDefault::Object { .. } => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_OBJECT); + bytes.extend_from_slice(&encode_eval_native_object_default(default)); + } + EvalNativeCallableDefault::Array(_) => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_ARRAY); + bytes.extend_from_slice(&encode_eval_native_array_default(default)); + } + } +} + +/// Encodes one UTF-8 string with a little-endian u32 byte-length prefix. +fn encode_eval_native_default_string(bytes: &mut Vec, value: &str) { + let len = u32::try_from(value.len()).unwrap_or(u32::MAX); + bytes.extend_from_slice(&len.to_le_bytes()); + bytes.extend_from_slice(value.as_bytes()); +} + +/// Returns true when eval can enforce this instance method visibility in the bridge. +fn class_method_visibility_bridge_supported(class_info: &ClassInfo, method_name: &str) -> bool { + class_info + .method_visibilities + .get(method_name) + .is_none_or(|visibility| { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) + }) +} + +/// Returns true when eval can enforce this static method visibility in the bridge. +fn class_static_method_visibility_bridge_supported( + class_info: &ClassInfo, + method_name: &str, +) -> bool { + class_info + .static_method_visibilities + .get(method_name) + .is_none_or(|visibility| { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) + }) +} + +/// Emits one native-function registration call into the just-created eval context. +fn register_eval_native_function( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeFunctionRegistration, +) -> Result<()> { + let invoker_label = emit_eval_native_function_invoker_inline(ctx, ®istration.signature); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + ctx.data, + &function_symbol(®istration.name), + Some(®istration.name), + callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + Some(®istration.signature), + &[], + &[], + callable_descriptor::CallableDescriptorInvocation::named( + callable_descriptor::CallableDescriptorShape::Function, + registration.name.clone(), + ), + Some(&invoker_label), + ); + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (name_label, name_len) = ctx.data.add_string(registration.name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &descriptor_label, + ); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &invoker_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + registration.signature.params.len() as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function"); + abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_function_bridge_support( + ctx, + context_offset, + &name_label, + name_len, + registration.bridge_supported, + ); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_function_param( + ctx, + context_offset, + &name_label, + name_len, + index, + param_name, + ); + register_eval_native_function_param_flags( + ctx, + context_offset, + &name_label, + name_len, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_function_param_type( + ctx, + context_offset, + &name_label, + name_len, + index, + type_spec, + ); + } + } + let default_context = EvalNativeDefaultContext::global(ctx.module); + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { + continue; + }; + register_eval_native_function_param_default( + ctx, + context_offset, + &name_label, + name_len, + index, + &default, + ); + } + if let Some(type_spec) = eval_native_callable_return_type_spec(®istration.signature) { + register_eval_native_function_return_type( + ctx, + context_offset, + &name_label, + name_len, + &type_spec, + ); + } + Ok(()) +} + +/// Emits an eval-safe descriptor invoker for a registered native free function. +fn emit_eval_native_function_invoker_inline( + ctx: &mut FunctionContext<'_>, + sig: &FunctionSig, +) -> String { + let label = ctx.next_label("eval_callable_invoker"); + let done_label = ctx.next_label("eval_callable_invoker_done"); + let captures: [(String, PhpType, bool); 0] = []; + let invoker = RuntimeCallableInvoker { + label: &label, + sig, + captures: &captures, + }; + abi::emit_jump(ctx.emitter, &done_label); + crate::codegen::runtime_callable_invoker::emit_runtime_callable_invoker_with_exception_boundary( + ctx.emitter, + ctx.data, + &invoker, + ); + ctx.emitter.label(&done_label); + label +} + +/// Emits one native method signature registration call into the eval context. +fn register_eval_native_method( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeMethodRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let method_key = format!("{}::{}", registration.class_name, registration.method_name); + let (method_key_label, method_key_len) = ctx.data.add_string(method_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + registration.signature.params.len() as i64, + ); + let symbol = if registration.is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method") + }; + abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_method_bridge_support( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + registration.bridge_supported, + ); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_method_param( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + param_name, + ); + register_eval_native_method_param_flags( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_method_param_type( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + type_spec, + ); + } + } + let default_context = EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { + continue; + }; + register_eval_native_method_param_default( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + &default, + ); + } + if let Some(type_spec) = eval_native_callable_return_type_spec(®istration.signature) { + register_eval_native_method_return_type( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + &type_spec, + ); + } +} + +/// Emits one native method bridge-support registration call. +fn register_eval_native_method_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_bridge_support") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_bridge_support") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method parameter-name registration call. +fn register_eval_native_method_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method parameter-flags registration call. +fn register_eval_native_method_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param_flags") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_flags") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method parameter-type registration call. +fn register_eval_native_method_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param_type") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_type") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method return-type registration call. +fn register_eval_native_method_return_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_return_type") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_return_type") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method parameter-default registration call. +fn register_eval_native_method_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_scalar", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_scalar") + } + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_string", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_string") + } + } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_object", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_object") + } + } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_array", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_array") + } + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native constructor signature registration call into the eval context. +fn register_eval_native_constructor( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeConstructorRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (class_name_label, class_name_len) = + ctx.data.add_string(registration.class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + registration.signature.params.len() as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor"); + abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_constructor_bridge_support( + ctx, + context_offset, + &class_name_label, + class_name_len, + registration.bridge_supported, + ); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_constructor_param( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + param_name, + ); + register_eval_native_constructor_param_flags( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_constructor_param_type( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + type_spec, + ); + } + } + let default_context = EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { + continue; + }; + register_eval_native_constructor_param_default( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + &default, + ); + } +} + +/// Emits one native constructor bridge-support registration call. +fn register_eval_native_constructor_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_bridge_support"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native class-parent metadata registration call into the eval context. +fn register_eval_native_class_parent( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name: &str, + parent_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (class_name_label, class_name_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + let (parent_name_label, parent_name_len) = ctx.data.add_string(parent_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &parent_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + parent_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_class_parent"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native property-type metadata registration call into the eval context. +fn register_eval_native_property_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativePropertyTypeRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}", + registration.class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_property_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native interface-property metadata registration call into the eval context. +fn register_eval_native_interface_property( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeInterfacePropertyRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}::{}", + registration.interface_name, + registration.declaring_interface_name, + registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let mut flags = 0; + if registration.requires_get { + flags |= NATIVE_PROPERTY_REQUIRES_GET; + } + if registration.requires_set { + flags |= NATIVE_PROPERTY_REQUIRES_SET; + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + flags, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_interface_property"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native abstract-property metadata registration call into the eval context. +fn register_eval_native_abstract_property( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeAbstractPropertyRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}::{}", + registration.class_name, registration.declaring_class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let mut flags = 0; + if registration.requires_get { + flags |= NATIVE_PROPERTY_REQUIRES_GET; + } + if registration.requires_set { + flags |= NATIVE_PROPERTY_REQUIRES_SET; + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + flags, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_abstract_property"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native property-default metadata registration call into the eval context. +fn register_eval_native_property_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativePropertyDefaultRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}", + registration.class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let symbol = match ®istration.default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_string") + } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(®istration.default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_array") + } + EvalNativeCallableDefault::Object { .. } => return, + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native member-attribute metadata registration call into the eval context. +fn register_eval_native_member_attribute( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeMemberAttributeRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let record = eval_native_member_attribute_record(registration); + let (record_label, record_len) = ctx.data.add_string(&record); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &record_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + record_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_member_attribute"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Encodes one member-attribute registration record for the eval bridge ABI. +fn eval_native_member_attribute_record( + registration: &EvalNativeMemberAttributeRegistration, +) -> Vec { + let mut record = Vec::new(); + record.push(registration.owner_kind); + let member_key = if registration.owner_kind == NATIVE_MEMBER_ATTRIBUTE_CLASS { + registration.class_name.clone() + } else { + format!("{}::{}", registration.class_name, registration.member_name) + }; + eval_native_member_attribute_push_string(&mut record, &member_key); + eval_native_member_attribute_push_string(&mut record, ®istration.attribute_name); + match ®istration.attribute_args { + Some(args) => { + record.push(NATIVE_ATTRIBUTE_ARGS_SUPPORTED); + eval_native_member_attribute_push_u32(&mut record, args.len()); + for arg in args { + eval_native_member_attribute_push_entry(&mut record, arg); + } + } + None => record.push(NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED), + } + record +} + +/// Returns true when an attribute argument list can be encoded for eval registration. +fn eval_native_member_attribute_args_supported(args: &[AttrArgEntry]) -> bool { + args.iter() + .all(|entry| eval_native_member_attribute_value_supported(&entry.value)) +} + +/// Returns true when one attribute argument value can be encoded for eval registration. +fn eval_native_member_attribute_value_supported(value: &AttrArgValue) -> bool { + match value { + AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(_, _) => false, + AttrArgValue::Array(elements) => eval_native_member_attribute_args_supported(elements), + AttrArgValue::Null + | AttrArgValue::Bool(_) + | AttrArgValue::Int(_) + | AttrArgValue::Float(_) + | AttrArgValue::Str(_) => true, + } +} + +/// Encodes one keyed attribute argument entry into a member-attribute registration record. +fn eval_native_member_attribute_push_entry(record: &mut Vec, entry: &AttrArgEntry) { + match &entry.key { + Some(AttrKey::Str(name)) => { + record.push(NATIVE_ATTRIBUTE_ARG_NAMED); + eval_native_member_attribute_push_string(record, name); + eval_native_member_attribute_push_arg(record, &entry.value); + } + Some(AttrKey::Int(_)) | None => eval_native_member_attribute_push_arg(record, &entry.value), + } +} + +/// Encodes one attribute argument value into a member-attribute registration record. +fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValue) { + match arg { + AttrArgValue::Null => record.push(NATIVE_ATTRIBUTE_ARG_NULL), + AttrArgValue::Bool(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_BOOL); + record.push(u8::from(*value)); + } + AttrArgValue::Int(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_INT); + record.extend_from_slice(&value.to_le_bytes()); + } + AttrArgValue::Float(bits) => { + record.push(NATIVE_ATTRIBUTE_ARG_FLOAT); + record.extend_from_slice(&bits.to_le_bytes()); + } + AttrArgValue::Str(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_STRING); + eval_native_member_attribute_push_string(record, value); + } + AttrArgValue::Array(elements) => { + record.push(NATIVE_ATTRIBUTE_ARG_ARRAY); + eval_native_member_attribute_push_u32(record, elements.len()); + for element in elements { + eval_native_member_attribute_push_entry(record, element); + } + } + AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(_, _) => { + record.push(NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED); + } + } +} + +/// Encodes one length-prefixed UTF-8 string into a member-attribute registration record. +fn eval_native_member_attribute_push_string(record: &mut Vec, value: &str) { + eval_native_member_attribute_push_u32(record, value.len()); + record.extend_from_slice(value.as_bytes()); +} + +/// Encodes one little-endian u32 length into a member-attribute registration record. +fn eval_native_member_attribute_push_u32(record: &mut Vec, value: usize) { + let value = u32::try_from(value).unwrap_or(u32::MAX); + record.extend_from_slice(&value.to_le_bytes()); +} + +/// Emits one native constructor parameter-name registration call. +fn register_eval_native_constructor_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native constructor parameter-flags registration call. +fn register_eval_native_constructor_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_flags"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native constructor parameter-type registration call. +fn register_eval_native_constructor_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native constructor parameter-default registration call. +fn register_eval_native_constructor_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_string") + } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_object") + } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_array") + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function parameter-name registration call. +fn register_eval_native_function_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function bridge-support registration call. +fn register_eval_native_function_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_bridge_support"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function parameter-flags registration call. +fn register_eval_native_function_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_flags"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function parameter-type registration call. +fn register_eval_native_function_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function return-type registration call. +fn register_eval_native_function_return_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_return_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native function parameter-default registration call. +fn register_eval_native_function_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_string") + } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_object") + } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_array") + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Loads the persistent eval context local into the selected integer argument register. +fn load_eval_context_local_to_arg( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + arg_index: usize, +) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::load_at_offset(ctx.emitter, arg_reg, context_offset); +} + +/// Loads the current eval context handle into the selected integer argument register. +fn load_eval_context_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_CONTEXT_HANDLE_OFFSET); +} + +/// Reloads the saved eval source string into the bridge code pointer/length arguments. +fn move_saved_eval_code_to_eval_args(ctx: &mut FunctionContext<'_>) { + let code_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let code_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, code_ptr_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_temporary_stack_slot(ctx.emitter, code_len_arg, EVAL_CODE_LEN_OFFSET); +} + +/// Ensures a persistent eval scope exists and stores its handle in the scratch frame. +fn ensure_eval_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_scope_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_scope_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_SCOPE_HANDLE_OFFSET); + Ok(()) +} + +/// Ensures a persistent eval global-scope exists and stores its handle in scratch. +fn ensure_eval_global_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_global_scope_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_global_scope_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_GLOBAL_SCOPE_HANDLE_OFFSET); + Ok(()) +} + +/// Returns the hidden frame slot that owns this function's persistent eval scope. +fn eval_scope_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalScope) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval scope local")) +} + +/// Returns the hidden frame slot that owns this function's eval global scope. +fn eval_global_scope_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalGlobalScope) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval global scope local")) +} + +/// Loads the current eval scope handle into the selected integer argument register. +fn load_eval_scope_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_SCOPE_HANDLE_OFFSET); +} + +/// Loads the current eval global-scope handle into the selected integer argument register. +fn load_eval_global_scope_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_GLOBAL_SCOPE_HANDLE_OFFSET); +} + +/// Installs the current eval global-scope handle into the eval context. +fn set_eval_context_global_scope(ctx: &mut FunctionContext<'_>) { + load_eval_context_to_arg(ctx, 0); + load_eval_global_scope_to_arg(ctx, 1); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_set_global_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Enters the current AOT method's class scope in the eval context, if any. +fn push_eval_context_class_scope(ctx: &mut FunctionContext<'_>) -> Result { + let Some(class_name) = current_eval_method_class(ctx).map(str::to_string) else { + return Ok(false); + }; + emit_eval_called_class_name_result(ctx, &class_name)?; + let (called_ptr_reg, called_len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, called_ptr_reg, EVAL_CALLED_CLASS_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, called_len_reg, EVAL_CALLED_CLASS_LEN_OFFSET); + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_CALLED_CLASS_PTR_OFFSET, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + EVAL_CALLED_CLASS_LEN_OFFSET, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_push_class_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + Ok(true) +} + +/// Leaves a pushed eval class scope while preserving the original eval status. +fn pop_eval_context_class_scope(ctx: &mut FunctionContext<'_>, pushed: bool) { + if !pushed { + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_context_to_arg(ctx, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_pop_class_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); +} + +/// Returns the lexical class encoded in the current EIR method name. +fn current_eval_method_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .flags + .is_method + .then(|| { + ctx.function + .name + .rsplit_once("::") + .map(|(class_name, _)| class_name) + }) + .flatten() +} + +/// Materializes the runtime called-class name for eval `static::` resolution. +fn emit_eval_called_class_name_result( + ctx: &mut FunctionContext<'_>, + fallback_class: &str, +) -> Result<()> { + if eval_late_static_class_id_available(ctx) { + match ctx.emitter.target.arch { + Arch::AArch64 => emit_eval_called_class_name_result_aarch64(ctx), + Arch::X86_64 => emit_eval_called_class_name_result_x86_64(ctx), + } + } else { + emit_eval_static_string_result(ctx, fallback_class.as_bytes()); + Ok(()) + } +} + +/// Emits the AArch64 class-id table lookup for eval's called class. +fn emit_eval_called_class_name_result_aarch64(ctx: &mut FunctionContext<'_>) -> Result<()> { + let missing = ctx.next_label("eval_called_class_missing"); + let done = ctx.next_label("eval_called_class_done"); + emit_eval_late_static_class_id_to_reg(ctx, "x12")?; + abi::emit_load_symbol_to_reg(ctx.emitter, "x10", "_class_name_count", 0); + ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("b.hs {}", missing)); // fall back to the lexical eval class when metadata is missing + abi::emit_symbol_address(ctx.emitter, "x11", "_class_name_entries"); + ctx.emitter.instruction("lsl x12, x12, #4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add x11, x11, x12"); // select the called-class metadata row + ctx.emitter.instruction("ldr x1, [x11]"); // load the called-class name pointer + ctx.emitter.instruction("ldr x2, [x11, #8]"); // load the called-class name length + ctx.emitter.instruction(&format!("b {}", done)); // skip the missing-metadata fallback + ctx.emitter.label(&missing); + abi::emit_symbol_address(ctx.emitter, "x1", "_class_name_missing"); + ctx.emitter.instruction("mov x2, #0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.label(&done); + Ok(()) +} + +/// Emits the x86_64 class-id table lookup for eval's called class. +fn emit_eval_called_class_name_result_x86_64(ctx: &mut FunctionContext<'_>) -> Result<()> { + let missing = ctx.next_label("eval_called_class_missing"); + let done = ctx.next_label("eval_called_class_done"); + emit_eval_late_static_class_id_to_reg(ctx, "r8")?; + abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_class_name_count", 0); + ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("jae {}", missing)); // fall back to the lexical eval class when metadata is missing + abi::emit_symbol_address(ctx.emitter, "r10", "_class_name_entries"); + ctx.emitter.instruction("shl r8, 4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add r10, r8"); // select the called-class metadata row + ctx.emitter.instruction("mov rax, QWORD PTR [r10]"); // load the called-class name pointer + ctx.emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the called-class name length + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the missing-metadata fallback + ctx.emitter.label(&missing); + abi::emit_symbol_address(ctx.emitter, "rax", "_class_name_missing"); + ctx.emitter.instruction("mov rdx, 0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.label(&done); + Ok(()) +} + +/// Returns true when the current method frame can provide a late-static class id. +fn eval_late_static_class_id_available(ctx: &FunctionContext<'_>) -> bool { + ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM).is_some() + || ctx.local_slot_by_name("this").is_some() +} + +/// Loads the late-static class id from the hidden static slot or `$this`. +fn emit_eval_late_static_class_id_to_reg(ctx: &mut FunctionContext<'_>, reg: &str) -> Result<()> { + if let Some(slot) = ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM) { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset(ctx.emitter, reg, offset); + return Ok(()); + } + if let Some(slot) = ctx.local_slot_by_name("this") { + match ctx.local_php_type(slot)? { + PhpType::Mixed | PhpType::Union(_) => { + ctx.load_local_to_result(slot)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let object_reg = eval_mixed_unbox_low_payload_reg(ctx); + abi::emit_load_from_address(ctx.emitter, reg, object_reg, 0); + } + PhpType::Object(_) => { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset(ctx.emitter, reg, offset); + abi::emit_load_from_address(ctx.emitter, reg, reg, 0); + } + other => { + return Err(CodegenIrError::invalid_module(format!( + "eval class scope this local has PHP type {:?}", + other + ))) + } + } + return Ok(()); + } + Err(CodegenIrError::invalid_module(format!( + "eval class scope without called-class source in {}", + ctx.function.name + ))) +} + +/// Emits a static string result for eval class-scope setup fallback paths. +fn emit_eval_static_string_result(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + +/// Collects PHP-visible locals that the current conservative scope sync can round-trip. +fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { + ctx.function + .locals + .iter() + .filter(|local| local.kind == LocalKind::PhpLocal) + .filter(|local| !local_uses_eval_global_sync(ctx, local.name.as_deref())) + .filter_map(|local| { + let name = local.name.clone()?; + let ty = local.php_type.codegen_repr(); + eval_sync_type_supported(&ty).then_some(EvalSyncLocal { + name, + slot: local.id, + ty, + }) + }) + .collect() +} + +/// Keeps only eval-sync locals whose PHP name appears in `names`. +fn filter_eval_sync_locals_by_name( + locals: Vec, + names: &BTreeSet, +) -> Vec { + locals + .into_iter() + .filter(|local| names.contains(&local.name)) + .collect() +} + +/// Returns true when a local name is backed by program-global storage during eval. +fn local_uses_eval_global_sync(ctx: &FunctionContext<'_>, name: Option<&str>) -> bool { + name.is_some_and(|name| main_name_uses_eval_global_scope(ctx, name)) +} + +/// Returns true when a main-scope name has actual EIR global storage to synchronize. +fn main_name_uses_eval_global_scope(ctx: &FunctionContext<'_>, name: &str) -> bool { + ctx.is_main && eval_sync_global_type(ctx, name).is_some() +} + +/// Collects caller-scope `global` aliases that eval fragments inherit by name. +fn eval_global_aliases(ctx: &FunctionContext<'_>) -> Vec { + ctx.function + .locals + .iter() + .filter(|local| local.kind == LocalKind::GlobalAlias) + .filter_map(|local| { + let name = local.name.clone()?; + Some(EvalGlobalAlias { + global_name: name.clone(), + name, + }) + }) + .collect() +} + +/// Collects program globals that can be boxed into the eval global scope. +fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { + let mut globals = ctx + .module + .data + .global_names + .iter() + .filter_map(|name| { + let ty = eval_sync_global_type(ctx, name)?; + eval_sync_global_type_supported(&ty).then_some(EvalSyncGlobal { + name: name.clone(), + ty, + }) + }) + .collect::>(); + push_eval_process_superglobal(&mut globals, "argc", PhpType::Int); + push_eval_process_superglobal(&mut globals, "argv", PhpType::Array(Box::new(PhpType::Str))); + globals +} + +/// Keeps only eval-sync globals whose PHP name appears in `names`. +fn filter_eval_sync_globals_by_name( + globals: Vec, + names: &BTreeSet, +) -> Vec { + globals + .into_iter() + .filter(|global| names.contains(&global.name)) + .collect() +} + +/// Adds a process superglobal to eval global sync unless normal globals already include it. +fn push_eval_process_superglobal(globals: &mut Vec, name: &str, ty: PhpType) { + if globals.iter().any(|global| global.name == name) { + return; + } + globals.push(EvalSyncGlobal { + name: name.to_string(), + ty, + }); +} + +/// Returns one unambiguous codegen type used for a program global, if available. +fn eval_sync_global_type(ctx: &FunctionContext<'_>, name: &str) -> Option { + let is_superglobal = crate::superglobals::is_superglobal(name); + let mut inferred = None; + for function in ctx + .module + .functions + .iter() + .chain(ctx.module.closures.iter()) + { + for inst in &function.instructions { + if global_instruction_name(ctx, inst) != Some(name) { + continue; + } + // Only real global storage instructions make a name a program + // global; eval scope ops reference names through the same data + // pool without any global storage behind them. + if !matches!(inst.op, Op::LoadGlobal | Op::StoreGlobal) { + continue; + } + if !is_superglobal { + // Regular globals always hold one boxed Mixed word (see + // `lower_store_global`); store operands carry narrower source + // types, so per-instruction inference would reject globals + // written as scalars and read back as Mixed after a barrier. + return Some(PhpType::Mixed); + } + let candidate = global_instruction_value_type(function, inst)?; + let candidate = candidate.codegen_repr(); + if !eval_sync_global_type_supported(&candidate) { + return None; + } + match &inferred { + Some(existing) if existing != &candidate => return None, + Some(_) => {} + None => inferred = Some(candidate), + } + } + } + inferred +} + +/// Returns the global name referenced by a load/store-global instruction. +fn global_instruction_name<'a>( + ctx: &'a FunctionContext<'_>, + inst: &Instruction, +) -> Option<&'a str> { + let Some(Immediate::GlobalName(data)) = inst.immediate else { + return None; + }; + ctx.module + .data + .global_names + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Returns the value type carried by a global load or store instruction. +fn global_instruction_value_type(function: &Function, inst: &Instruction) -> Option { + match inst.op { + Op::LoadGlobal => { + let result = inst.result?; + function.value(result).map(|value| value.php_type.clone()) + } + Op::StoreGlobal => { + let value = *inst.operands.first()?; + function.value(value).map(|value| value.php_type.clone()) + } + _ => None, + } +} + +/// Returns true when a global type can round-trip through eval global scope sync. +fn eval_sync_global_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Returns true when a local type can be boxed to Mixed and restored from Mixed after eval. +fn eval_sync_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Flushes visible native locals into the materialized eval scope before executing eval. +fn flush_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLocal]) -> Result<()> { + for local in locals { + let ty = ctx.load_local_to_result(local.slot)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_scope_set(ctx, local, scope_set_flags_for_type(&ty)); + } + Ok(()) +} + +/// Flushes supported program globals into the eval global scope before eval. +fn flush_eval_global_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + load_global_to_result(ctx, global); + if !matches!(global.ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &global.ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_global_scope_set(ctx, global, scope_set_flags_for_type(&global.ty)); + } + Ok(()) +} + +/// Flushes global-backed variables into the local eval scope for scope-read EIR AOT. +fn flush_eval_globals_to_local_scope(ctx: &mut FunctionContext<'_>, globals: &[EvalSyncGlobal]) { + for global in globals { + load_global_to_result(ctx, global); + if !matches!(global.ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &global.ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_scope_set_name(ctx, &global.name, scope_set_flags_for_type(&global.ty)); + } +} + +/// Loads a program-global symbol into result registers using its inferred type. +fn load_global_to_result(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + abi::emit_load_symbol_to_result(ctx.emitter, &symbol, &ty); +} + +/// Returns ABI flags for a scope value produced from the given native type. +fn scope_set_flags_for_type(ty: &PhpType) -> i64 { + if matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + 0 + } else { + EVAL_SCOPE_FLAG_OWNED + } +} + +/// Calls `__elephc_eval_scope_set` for one boxed global value. +fn emit_eval_global_scope_set(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Marks caller-scope global aliases in the materialized eval scope. +fn mark_eval_scope_global_aliases(ctx: &mut FunctionContext<'_>, aliases: &[EvalGlobalAlias]) { + for alias in aliases { + let (name_label, name_len) = ctx.data.add_string(alias.name.as_bytes()); + let (global_name_label, global_name_len) = + ctx.data.add_string(alias.global_name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let global_name_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_symbol_address(ctx.emitter, global_name_arg, &global_name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + global_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_scope_mark_global_alias"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + } +} + +/// Calls `__elephc_eval_scope_set` for one boxed local value. +fn emit_eval_scope_set(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Reloads synchronized locals from the eval scope after the eval interpreter returns. +fn reload_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLocal]) -> Result<()> { + for local in locals { + emit_eval_scope_get(ctx, local); + let missing = ctx.next_label("eval_scope_reload_missing"); + let done = ctx.next_label("eval_scope_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_local(ctx, local)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_local(ctx, local)?; + ctx.emitter.label(&done); + } + Ok(()) +} + +/// Reloads synchronized program globals from the eval global scope after eval. +fn reload_eval_global_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + emit_eval_global_scope_get(ctx, global); + let missing = ctx.next_label("eval_global_reload_missing"); + let done = ctx.next_label("eval_global_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_global(ctx, global)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_global(ctx, global)?; + ctx.emitter.label(&done); + } + Ok(()) +} + +/// Reloads synchronized program globals from the local eval scope after EIR eval AOT. +fn reload_eval_globals_from_local_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + emit_eval_scope_get_name(ctx, &global.name, 0, 8); + let missing = ctx.next_label("eval_global_reload_missing"); + let done = ctx.next_label("eval_global_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_global(ctx, global)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_global(ctx, global)?; + ctx.emitter.label(&done); + } + Ok(()) +} + +/// Calls `__elephc_eval_scope_get` and stores out cell/flags at the start of eval scratch. +fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { + let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_get` for one program global. +fn emit_eval_global_scope_get(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { + let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Branches to `label` when the latest scope-get flags do not mark a visible value. +fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str) { + emit_branch_if_scope_entry_missing_at(ctx, 8, label); +} + +/// Branches to `label` when the scope-get flags at `flags_offset` do not mark a visible value. +fn emit_branch_if_scope_entry_missing_at( + ctx: &mut FunctionContext<'_>, + flags_offset: usize, + label: &str, +) { + let flags_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, flags_offset); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + } + } +} + +/// Converts a scope Mixed cell back to the local's native storage type. +fn store_mixed_scope_cell_to_local( + ctx: &mut FunctionContext<'_>, + local: &EvalSyncLocal, +) -> Result<()> { + match local.ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + emit_retain_scope_cell_if_owned(ctx); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Bool => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + // Objects, arrays, and hashes are heap pointers boxed in the + // scope cell; unbox and store the raw payload pointer. + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let payload_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + }; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, payload_reg)); // move the unboxed heap pointer into the local-store result register + ctx.store_current_result_to_local(local.slot)?; + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval scope reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Converts a scope Mixed cell back to a program-global storage symbol. +fn store_mixed_scope_cell_to_global( + ctx: &mut FunctionContext<'_>, + global: &EvalSyncGlobal, +) -> Result<()> { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + match &ty { + PhpType::Mixed | PhpType::Union(_) => { + emit_retain_scope_cell_if_owned(ctx); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Mixed, false); + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); + } + PhpType::Bool => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Bool, false); + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Float, false); + } + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); + } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let payload_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + }; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, payload_reg)); // move the unboxed array payload into the ABI result register + abi::emit_incref_if_refcounted(ctx.emitter, &ty); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &ty, false); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval global reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Retains a scope-owned Mixed cell before storing it into a native local owner. +fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { + let flags_reg = abi::secondary_scratch_reg(ctx.emitter); + let skip = ctx.next_label("eval_scope_reload_borrowed"); + abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, 8); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + } + } + abi::emit_call_label(ctx.emitter, "__rt_incref"); + ctx.emitter.label(&skip); +} + +/// Stores the local fallback used when eval unsets or removes a synchronized local. +fn store_missing_scope_entry_to_local( + ctx: &mut FunctionContext<'_>, + local: &EvalSyncLocal, +) -> Result<()> { + match local.ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Float => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_int_result_to_float_result(ctx.emitter); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + ctx.store_current_result_to_local(local.slot)?; + } + PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + // Heap-pointer locals fall back to the null pointer when eval + // removed the entry, matching the object fallback. + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.store_current_result_to_local(local.slot)?; + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval scope missing reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Stores the program-global fallback for a missing eval global entry. +fn store_missing_scope_entry_to_global( + ctx: &mut FunctionContext<'_>, + global: &EvalSyncGlobal, +) -> Result<()> { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + match &ty { + PhpType::Mixed | PhpType::Union(_) => { + let symbol_name = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol_name); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Mixed, false); + } + PhpType::Int => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); + } + PhpType::Bool => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Bool, false); + } + PhpType::Float => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_int_result_to_float_result(ctx.emitter); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Float, false); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); + } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &ty, false); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval global missing reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Emits a fatal diagnostic when the eval bridge reports any non-zero status. +fn emit_eval_status_check(ctx: &mut FunctionContext<'_>) { + let ok_label = ctx.next_label("eval_status_ok"); + let parse_error_label = ctx.next_label("eval_status_parse_error"); + let throwable_label = ctx.next_label("eval_status_throwable"); + let unsupported_label = ctx.next_label("eval_status_unsupported"); + abi::emit_branch_if_int_result_zero(ctx.emitter, &ok_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_PARSE_ERROR, &parse_error_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_UNCAUGHT_THROWABLE, &throwable_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_UNSUPPORTED, &unsupported_label); + emit_eval_fatal_message(ctx, EVAL_RUNTIME_FATAL_MESSAGE); + ctx.emitter.label(&parse_error_label); + emit_eval_fatal_message(ctx, EVAL_PARSE_ERROR_MESSAGE); + ctx.emitter.label(&throwable_label); + emit_eval_throw_current(ctx); + ctx.emitter.label(&unsupported_label); + emit_eval_fatal_message(ctx, EVAL_UNSUPPORTED_MESSAGE); + ctx.emitter.label(&ok_label); +} + +/// Branches to a label when the eval bridge returned a specific status code. +fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: &str) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + } + } +} + +/// Publishes an eval-thrown Throwable and enters the normal runtime unwinder. +fn emit_eval_throw_current(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_ERROR_OFFSET); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let object_reg = eval_mixed_unbox_low_payload_reg(ctx); + abi::emit_store_reg_to_symbol(ctx.emitter, object_reg, "_exc_value", 0); + abi::emit_call_label(ctx.emitter, "__rt_throw_current"); +} + +/// Returns the low payload register produced by `__rt_mixed_unbox` for eval status handling. +fn eval_mixed_unbox_low_payload_reg(ctx: &FunctionContext<'_>) -> &'static str { + match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + } +} + +/// Emits an eval diagnostic message and exits the process. +fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { + let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.adrp("x1", &message_label); + ctx.emitter.add_lo12("x1", "x1", &message_label); + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length + ctx.emitter.syscall(4); + abi::emit_exit(ctx.emitter, 1); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + abi::emit_exit(ctx.emitter, 1); + } + } +} diff --git a/src/codegen/lower_inst/builtins/types.rs b/src/codegen/lower_inst/builtins/types.rs index 3c00f3d4d5..ff24d0fce3 100644 --- a/src/codegen/lower_inst/builtins/types.rs +++ b/src/codegen/lower_inst/builtins/types.rs @@ -345,20 +345,20 @@ pub(crate) fn lower_class_name_lookup( ctx.load_value_to_result(value)?; emit_dynamic_object_class_name(ctx, name); } + PhpType::Mixed | PhpType::Union(_) if super::has_eval_context(ctx) => { + return super::lower_eval_object_class_name(ctx, inst, value, name); + } + PhpType::Mixed | PhpType::Union(_) => { + ctx.load_value_to_result(value)?; + emit_mixed_object_class_name(ctx, name); + } PhpType::Str if name == "get_parent_class" => { let class_name = const_string_operand(ctx, value)?; let parent = parent_of(ctx, &class_name); emit_string_result(ctx, parent.as_bytes()); } - other => { + _ => { ctx.load_value_to_result(value)?; - if matches!(other, PhpType::Mixed | PhpType::Union(_)) { - return Err(CodegenIrError::unsupported(format!( - "{} for PHP type {:?}", - name, - other - ))); - } emit_string_result(ctx, b""); } } @@ -379,6 +379,13 @@ pub(crate) fn lower_is_a_relation( let object = expect_operand(inst, 0)?; let target = expect_operand(inst, 1)?; let exclude_self = name == "is_subclass_of"; + if matches!(ctx.value_php_type(object)?, PhpType::Mixed | PhpType::Union(_)) + && super::has_eval_context(ctx) + { + if let Some(target_class) = optional_const_string_operand(ctx, target)? { + return super::lower_eval_object_is_a(ctx, inst, object, &target_class, exclude_self); + } + } let result = static_relation_holds(ctx, object, target, exclude_self)?; emit_bool_result(ctx, result); store_if_result(ctx, inst) @@ -453,6 +460,34 @@ fn emit_dynamic_object_class_name(ctx: &mut FunctionContext<'_>, name: &str) { } } +/// Emits class-name lookup for a boxed Mixed value that may contain an object. +fn emit_mixed_object_class_name(ctx: &mut FunctionContext<'_>, name: &str) { + let empty_label = ctx.next_label("get_class_mixed_empty"); + let done_label = ctx.next_label("get_class_mixed_done"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // require a boxed object payload for class-name lookup + ctx.emitter + .instruction(&format!("b.ne {}", empty_label)); // non-object Mixed payloads produce an empty class name + ctx.emitter.instruction("mov x0, x1"); // expose the unboxed object pointer to the object lookup path + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // require a boxed object payload for class-name lookup + ctx.emitter + .instruction(&format!("jne {}", empty_label)); // non-object Mixed payloads produce an empty class name + ctx.emitter.instruction("mov rax, rdi"); // expose the unboxed object pointer to the object lookup path + } + } + emit_dynamic_object_class_name(ctx, name); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&empty_label); + emit_string_result(ctx, b""); + + ctx.emitter.label(&done_label); +} + /// Emits AArch64 runtime object class-name lookup for `get_class()` and `get_parent_class()`. fn emit_dynamic_object_class_name_aarch64( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/callables.rs b/src/codegen/lower_inst/callables.rs index e62846c61e..c4e3a60877 100644 --- a/src/codegen/lower_inst/callables.rs +++ b/src/codegen/lower_inst/callables.rs @@ -351,6 +351,8 @@ fn extern_decl_signature(decl: &crate::ir::ExternDecl) -> FunctionSig { .iter() .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; decl.params.len()], + param_attributes: vec![Vec::new(); decl.params.len()], defaults: vec![None; decl.params.len()], return_type: decl.return_php_type.clone(), declared_return: true, @@ -680,7 +682,12 @@ fn lower_runtime_mixed_callable_array_descriptor_invoke( abi::emit_jump(ctx.emitter, &miss_label); ctx.emitter.label(&miss_label); - emit_runtime_callable_array_no_match_abort(ctx); + if super::builtins::has_eval_context(ctx) { + super::builtins::lower_eval_callable_call_array(ctx, inst, callable, arg_mixed)?; + abi::emit_jump(ctx.emitter, &done_label); + } else { + emit_runtime_callable_array_no_match_abort(ctx); + } ctx.emitter.label(&done_label); abi::emit_release_temporary_stack(ctx.emitter, MIXED_SELECTOR_BYTES); diff --git a/src/codegen/lower_inst/callables/instance_expr.rs b/src/codegen/lower_inst/callables/instance_expr.rs index 4203a5f07b..90b2ed11d6 100644 --- a/src/codegen/lower_inst/callables/instance_expr.rs +++ b/src/codegen/lower_inst/callables/instance_expr.rs @@ -13,7 +13,7 @@ use crate::codegen::abi; use crate::ir::{BlockId, Immediate, Instruction, Op, ValueDef, ValueId}; use crate::names::{method_symbol, php_symbol_key}; -use crate::types::{FunctionSig, PhpType}; +use crate::types::PhpType; use super::super::super::context::FunctionContext; use super::super::{ @@ -159,7 +159,6 @@ fn instance_method_expr_call_target( target )) })?; - require_instance_expr_call_signature(owner, target, sig)?; let impl_class = class_info .method_impl_classes .get(&method_key) @@ -188,22 +187,6 @@ fn instance_method_expr_call_target( }) } -/// Rejects callable method signatures whose EIR expr-call lowering cannot yet forward safely. -fn require_instance_expr_call_signature( - owner: &str, - target: &str, - sig: &FunctionSig, -) -> Result<()> { - if sig.variadic.is_some() { - return Err(CodegenIrError::unsupported(format!( - "{} receiver-bound callable '{}' with variadic method signature", - owner, - target - ))); - } - Ok(()) -} - /// Returns the first-class callable producer for an expr-call operand. fn expr_callable_source_instruction<'a>( ctx: &'a FunctionContext<'_>, diff --git a/src/codegen/lower_inst/conversions.rs b/src/codegen/lower_inst/conversions.rs index ffd2d2ab6f..c0cff434a4 100644 --- a/src/codegen/lower_inst/conversions.rs +++ b/src/codegen/lower_inst/conversions.rs @@ -11,13 +11,16 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; -use crate::ir::{Immediate, Instruction, IrType}; +use crate::ir::{Immediate, Instruction, IrType, ValueId}; +use crate::names::method_symbol; use crate::types::PhpType; use super::super::context::FunctionContext; use super::{ - expect_operand, load_value_to_first_int_arg, lower_runtime_object_method_call, predicates, - store_if_result, strings, + direct_call_stack_pad_bytes, emit_dynamic_instance_method_call, + emit_mixed_method_class_dispatch, expect_operand, load_value_to_first_int_arg, + lower_runtime_object_method_call, materialize_method_call_args_with_receiver_reg_and_refs, + mixed_method_candidates, predicates, store_if_result, strings, }; use crate::codegen::{CodegenIrError, Result}; @@ -160,7 +163,10 @@ fn lower_cast_to_float(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Res } /// Lowers an explicit cast to PHP string for concrete scalar operands. -fn lower_cast_to_string(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_cast_to_string( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; let raw_ty = ctx.raw_value_php_type(value)?; if matches!(raw_ty, PhpType::Resource(_)) { @@ -178,8 +184,7 @@ fn lower_cast_to_string(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Re strings::lower_int_like_to_string(ctx, inst) } PhpType::Mixed | PhpType::Union(_) => { - load_value_to_first_int_arg(ctx, value)?; - abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + emit_mixed_string_context_result(ctx, value)?; store_if_result(ctx, inst) } PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable => { @@ -193,6 +198,211 @@ fn lower_cast_to_string(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Re } } +/// Leaves a string result for a boxed Mixed value, dispatching objects through `__toString()`. +pub(super) fn emit_mixed_string_context_result( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + emit_mixed_string_context(ctx, value, MixedStringContextMode::Result) +} + +/// Writes a boxed Mixed value to stdout, dispatching objects through `__toString()`. +pub(super) fn emit_mixed_string_context_stdout( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + emit_mixed_string_context(ctx, value, MixedStringContextMode::Stdout) +} + +/// Describes whether a Mixed string context should leave a string result or write it. +enum MixedStringContextMode { + Result, + Stdout, +} + +/// Handles PHP string contexts for boxed Mixed values with an object-aware branch. +fn emit_mixed_string_context( + ctx: &mut FunctionContext<'_>, + value: ValueId, + mode: MixedStringContextMode, +) -> Result<()> { + let candidates = mixed_method_candidates(ctx, "__toString", 1)?; + let receiver_reg = abi::nested_call_reg(ctx.emitter); + let object_label = ctx.next_label("mixed_string_object"); + let no_match_label = ctx.next_label("mixed_string_no_match"); + let done_label = ctx.next_label("mixed_string_done"); + let match_labels = candidates + .iter() + .map(|candidate| { + ctx.next_label(&format!( + "mixed_string_{}", + super::label_fragment(&candidate.class_name) + )) + }) + .collect::>(); + + ctx.load_value_to_result(value)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_unboxed_mixed_object(ctx, &object_label); + emit_mixed_string_scalar_fallback(ctx, &mode)?; + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&object_label); + discard_preserved_mixed_pointer(ctx); + move_unboxed_mixed_object_payload(ctx, receiver_reg); + emit_mixed_method_class_dispatch( + ctx, + receiver_reg, + &candidates, + &match_labels, + &no_match_label, + ); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + ctx.emitter.label(label); + let return_ty = emit_mixed_tostring_candidate_call(ctx, value, receiver_reg, candidate)?; + coerce_tostring_return_to_string_result(ctx, &return_ty)?; + if matches!(mode, MixedStringContextMode::Stdout) { + abi::emit_write_stdout(ctx.emitter, &PhpType::Str); + } + abi::emit_jump(ctx.emitter, &done_label); + } + + ctx.emitter.label(&no_match_label); + emit_mixed_missing_tostring_fatal(ctx); + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Branches to the object path when `__rt_mixed_unbox` returned an object tag. +fn emit_branch_if_unboxed_mixed_object(ctx: &mut FunctionContext<'_>, object_label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed Mixed value contains an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // dispatch object string contexts through __toString + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed Mixed value contains an object + ctx.emitter.instruction(&format!("je {}", object_label)); // dispatch object string contexts through __toString + } + } +} + +/// Runs the existing scalar Mixed string behavior after restoring the original box. +fn emit_mixed_string_scalar_fallback( + ctx: &mut FunctionContext<'_>, + mode: &MixedStringContextMode, +) -> Result<()> { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match mode { + MixedStringContextMode::Result => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + } + MixedStringContextMode::Stdout => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_write_stdout"); + } + } + Ok(()) +} + +/// Discards the saved boxed Mixed pointer once the object branch no longer needs it. +fn discard_preserved_mixed_pointer(ctx: &mut FunctionContext<'_>) { + abi::emit_pop_reg(ctx.emitter, abi::temp_int_reg(ctx.emitter.target)); +} + +/// Moves the unboxed object payload into the callee-saved receiver dispatch register. +fn move_unboxed_mixed_object_payload(ctx: &mut FunctionContext<'_>, receiver_reg: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("mov {}, x1", receiver_reg)); // preserve the unboxed object pointer for __toString dispatch + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov {}, rdi", receiver_reg)); // preserve the unboxed object pointer for __toString dispatch + } + } +} + +/// Emits one concrete `__toString()` candidate call for a boxed Mixed object. +fn emit_mixed_tostring_candidate_call( + ctx: &mut FunctionContext<'_>, + value: ValueId, + receiver_reg: &str, + candidate: &super::MixedMethodCandidate, +) -> Result { + let receiver_ty = PhpType::Object(candidate.class_name.clone()); + let mut param_types = Vec::with_capacity(candidate.target.params.len() + 1); + param_types.push(receiver_ty.clone()); + param_types.extend(candidate.target.params.iter().map(|param| param.codegen_repr())); + let mut ref_params = Vec::with_capacity(candidate.target.ref_params.len() + 1); + ref_params.push(false); + ref_params.extend(candidate.target.ref_params.iter().copied()); + let operands = [value]; + let call_args = materialize_method_call_args_with_receiver_reg_and_refs( + ctx, + receiver_reg, + &receiver_ty, + &operands, + ¶m_types, + &ref_params, + )?; + let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, call_args.overflow_bytes); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + if let Some(slot) = candidate.target.dynamic_slot { + emit_dynamic_instance_method_call(ctx, slot); + } else { + abi::emit_call_label( + ctx.emitter, + &method_symbol(&candidate.target.impl_class, &candidate.target.method_key), + ); + } + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(ctx.emitter, call_args.overflow_bytes); + Ok(candidate.target.return_ty.clone()) +} + +/// Normalizes a `__toString()` return into a string result pair. +fn coerce_tostring_return_to_string_result( + ctx: &mut FunctionContext<'_>, + return_ty: &PhpType, +) -> Result<()> { + match return_ty.codegen_repr() { + PhpType::Str => Ok(()), + PhpType::Mixed | PhpType::Union(_) => { + super::cast_loaded_mixed_pointer_to_result(ctx, &PhpType::Str) + } + other => Err(CodegenIrError::unsupported(format!( + "__toString return value for PHP type {:?}", + other + ))), + } +} + +/// Emits a fatal when a boxed Mixed object has no matching public `__toString()`. +fn emit_mixed_missing_tostring_fatal(ctx: &mut FunctionContext<'_>) { + let (label, len) = ctx + .data + .add_string(b"Fatal error: Object could not be converted to string\n"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // write the object string-cast fatal to stderr + ctx.emitter.adrp("x1", &label); + ctx.emitter.add_lo12("x1", "x1", &label); + ctx.emitter.instruction(&format!("mov x2, #{}", len)); // pass the object string-cast fatal byte length + ctx.emitter.syscall(4); + abi::emit_exit(ctx.emitter, 1); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov edi, 2"); // write the object string-cast fatal to Linux stderr + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + ctx.emitter.instruction(&format!("mov edx, {}", len)); // pass the object string-cast fatal byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the object string-cast fatal before exiting + abi::emit_exit(ctx.emitter, 1); + } + } +} + /// Lowers an object string cast through `__toString()` or PHP's conversion fatal. fn lower_object_to_string( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/iterators.rs b/src/codegen/lower_inst/iterators.rs index 3b4473d8e0..9196150bdc 100644 --- a/src/codegen/lower_inst/iterators.rs +++ b/src/codegen/lower_inst/iterators.rs @@ -194,6 +194,7 @@ pub(super) fn lower_iter_current_value( Arch::AArch64 => load_current_array_value_aarch64(ctx, offset, &elem)?, Arch::X86_64 => load_current_array_value_x86_64(ctx, offset, &elem)?, } + retain_current_indexed_value_if_unboxed(&mut ctx.emitter, &elem, &result_ty); box_current_indexed_value_if_needed(ctx, &elem, &result_ty)?; } IteratorSourceKind::Hash => match ctx.emitter.target.arch { @@ -223,6 +224,17 @@ fn iter_current_result_type(ctx: &FunctionContext<'_>, inst: &Instruction) -> Re Ok(ctx.value_php_type(result)?.codegen_repr()) } +/// Retains concrete indexed-array foreach values that are returned without Mixed boxing. +fn retain_current_indexed_value_if_unboxed( + emitter: &mut crate::codegen::emit::Emitter, + elem: &PhpType, + result_ty: &PhpType, +) { + if elem.codegen_repr() == result_ty.codegen_repr() { + abi::emit_incref_if_refcounted(emitter, &elem.codegen_repr()); + } +} + /// Boxes an indexed iterator element only when the EIR result expects `Mixed`. fn box_current_indexed_value_if_needed( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 1fc999ca5c..80e0bba195 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -17,9 +17,11 @@ use std::collections::HashSet; -use crate::codegen::{abi, callable_descriptor, emit_box_current_value_as_mixed, runtime_value_tag}; use crate::codegen::platform::Arch; use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::codegen::{ + abi, callable_descriptor, emit_box_current_value_as_mixed, runtime_value_tag, +}; use crate::intrinsics::IntrinsicCall; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; use crate::names::{method_symbol, php_symbol_key}; @@ -27,7 +29,9 @@ use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::context::FunctionContext; use super::{ - callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, expect_data, + builtins, callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, + expect_data, + coerce_loaded_value_to_tagged_scalar, emit_loaded_assoc_array_to_mixed, emit_loaded_indexed_array_to_mixed, emit_mixed_string_for_persistent_store, emit_ref_arg_writebacks, expect_operand, iterators, load_value_to_first_int_arg, materialize_direct_call_args_with_refs, @@ -103,9 +107,6 @@ pub(super) fn lower_object_new(ctx: &mut FunctionContext<'_>, inst: &Instruction if is_fiber_class(&class_name) { return lower_fiber_new(ctx, inst); } - if class_name == "ReflectionFunction" { - return reflection::lower_reflection_function_new(ctx, inst); - } if reflection::is_reflection_owner_class(&class_name) { return reflection::lower_reflection_owner_new(ctx, inst, &class_name); } @@ -137,11 +138,10 @@ pub(super) fn lower_object_new(ctx: &mut FunctionContext<'_>, inst: &Instruction property_defaults, constructor_impl, ) = { - let class_info = ctx - .module - .class_infos - .get(&class_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; + let class_info = + ctx.module.class_infos.get(&class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; if class_interfaces_require_missing_method_symbols(ctx, &class_name, class_info) { return Err(CodegenIrError::unsupported(format!( "object allocation requiring interface method symbols not emitted by EIR for {}", @@ -227,6 +227,96 @@ pub(super) fn lower_object_new(ctx: &mut FunctionContext<'_>, inst: &Instruction Ok(()) } +/// Lowers PHP object cloning for fixed-class receivers. +pub(super) fn lower_object_clone_shallow( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let source = expect_operand(inst, 0)?; + let class_name = class_name_immediate(ctx, inst)?.to_string(); + if is_builtin_stdclass(&class_name) { + return lower_stdclass_clone(ctx, inst, source); + } + if is_runtime_managed_object_clone_class(&class_name) { + return Err(CodegenIrError::unsupported(format!( + "clone for runtime-managed class {}", + class_name + ))); + } + let ( + class_id, + property_count, + allow_dynamic_properties, + retained_offsets, + owned_reference_property_offsets, + ) = { + let class_info = + ctx.module.class_infos.get(&class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; + let retained_offsets = cloned_property_retain_offsets(class_info); + let owned_reference_property_offsets = owned_reference_property_offsets(class_info); + ( + class_info.class_id, + class_info.properties.len(), + class_info.allow_dynamic_properties, + retained_offsets, + owned_reference_property_offsets, + ) + }; + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("object_clone_shallow missing result value"))?; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.load_value_to_reg(source, result_reg)?; + abi::emit_push_reg(ctx.emitter, result_reg); + emit_object_allocation( + ctx, + class_id, + property_count, + allow_dynamic_properties, + &[], + &owned_reference_property_offsets, + )?; + ctx.store_result_value(result)?; + let source_reg = abi::secondary_scratch_reg(ctx.emitter); + let dest_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, source_reg); + ctx.load_value_to_reg(result, dest_reg)?; + emit_clone_declared_property_slots(ctx, source_reg, dest_reg, property_count, &retained_offsets); + if allow_dynamic_properties { + emit_clone_dynamic_property_hash( + ctx, + source_reg, + dest_reg, + dynamic_property_hash_offset(property_count), + ); + } + Ok(()) +} + +/// Lowers `clone` for `stdClass`, whose payload is just class id plus dynamic-property hash. +fn lower_stdclass_clone( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + source: ValueId, +) -> Result<()> { + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("stdClass clone missing result value"))?; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.load_value_to_reg(source, result_reg)?; + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_stdclass_new"); + ctx.store_result_value(result)?; + let source_reg = abi::secondary_scratch_reg(ctx.emitter); + let dest_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, source_reg); + ctx.load_value_to_reg(result, dest_reg)?; + emit_clone_dynamic_property_hash(ctx, source_reg, dest_reg, 8); + Ok(()) +} + /// Lowers `new stdClass()` through the runtime helper that seeds its dynamic-property hash. fn lower_stdclass_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if !inst.operands.is_empty() { @@ -244,6 +334,19 @@ fn is_spl_doubly_linked_list_family(class_name: &str) -> bool { matches!(class_name, "SplDoublyLinkedList" | "SplStack" | "SplQueue") } +/// Returns true for object classes whose payload is not the generic declared-property layout. +fn is_runtime_managed_object_clone_class(class_name: &str) -> bool { + let class_name = class_name.trim_start_matches('\\'); + is_fiber_class(class_name) + || class_name == "Generator" + || reflection::is_reflection_owner_class(class_name) + || class_name == "CallbackFilterIterator" + || class_name == "RecursiveCallbackFilterIterator" + || class_name == "IteratorIterator" + || is_spl_doubly_linked_list_family(class_name) + || class_name == "SplFixedArray" +} + /// Lowers `new SplDoublyLinkedList`, `new SplStack`, and `new SplQueue`. fn lower_spl_doubly_linked_list_new( ctx: &mut FunctionContext<'_>, @@ -322,12 +425,17 @@ fn lower_callback_filter_iterator_new( } let source = expect_operand(inst, 0)?; let callback = expect_operand(inst, 1)?; - let (class_id, property_count, uninitialized_marker_offsets, property_defaults, callback_env_offset) = { - let class_info = ctx - .module - .class_infos - .get(class_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; + let ( + class_id, + property_count, + uninitialized_marker_offsets, + property_defaults, + callback_env_offset, + ) = { + let class_info = + ctx.module.class_infos.get(class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; if class_info.allow_dynamic_properties { return Err(CodegenIrError::unsupported(format!( "object allocation requiring dynamic properties for {}", @@ -507,7 +615,13 @@ fn lower_iterator_iterator_new(ctx: &mut FunctionContext<'_>, inst: &Instruction .result .ok_or_else(|| CodegenIrError::invalid_module("object_new missing result value"))?; ctx.store_result_value(result)?; - emit_iterator_iterator_inner_from_traversable(ctx, source, inst.operands.get(1).copied(), result, &slot) + emit_iterator_iterator_inner_from_traversable( + ctx, + source, + inst.operands.get(1).copied(), + result, + &slot, + ) } /// Stores IteratorIterator::$inner after converting IteratorAggregate inputs through getIterator(). @@ -578,28 +692,28 @@ fn emit_push_iterator_iterator_downcast_status_from_lookup(ctx: &mut FunctionCon let done = ctx.next_label("iterator_iterator_downcast_lookup_done"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the downcast class-string resolve to metadata? - ctx.emitter.instruction(&format!("b.eq {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs - ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("b.ne {}", invalid)); // interface names are invalid downcast targets - ctx.emitter.instruction("mov x0, #1"); // status 1 means x1 carries a concrete downcast class id - ctx.emitter.instruction(&format!("b {}", done)); // preserve the resolved class id for later validation + ctx.emitter.instruction("cmp x0, #0"); // did the downcast class-string resolve to metadata? + ctx.emitter.instruction(&format!("b.eq {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs + ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("b.ne {}", invalid)); // interface names are invalid downcast targets + ctx.emitter.instruction("mov x0, #1"); // status 1 means x1 carries a concrete downcast class id + ctx.emitter.instruction(&format!("b {}", done)); // preserve the resolved class id for later validation ctx.emitter.label(&invalid); - ctx.emitter.instruction("mov x0, #2"); // status 2 means the downcast must throw for aggregates - ctx.emitter.instruction("mov x1, #0"); // invalid downcast targets have no usable class id + ctx.emitter.instruction("mov x0, #2"); // status 2 means the downcast must throw for aggregates + ctx.emitter.instruction("mov x1, #0"); // invalid downcast targets have no usable class id } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the downcast class-string resolve to metadata? - ctx.emitter.instruction(&format!("je {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs - ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("jne {}", invalid)); // interface names are invalid downcast targets - ctx.emitter.instruction("mov rax, 1"); // status 1 means rdi carries a concrete downcast class id - ctx.emitter.instruction(&format!("jmp {}", done)); // preserve the resolved class id for later validation + ctx.emitter.instruction("test rax, rax"); // did the downcast class-string resolve to metadata? + ctx.emitter.instruction(&format!("je {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs + ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("jne {}", invalid)); // interface names are invalid downcast targets + ctx.emitter.instruction("mov rax, 1"); // status 1 means rdi carries a concrete downcast class id + ctx.emitter.instruction(&format!("jmp {}", done)); // preserve the resolved class id for later validation ctx.emitter.label(&invalid); - ctx.emitter.instruction("mov rax, 2"); // status 2 means the downcast must throw for aggregates - ctx.emitter.instruction("xor edi, edi"); // invalid downcast targets have no usable class id + ctx.emitter.instruction("mov rax, 2"); // status 2 means the downcast must throw for aggregates + ctx.emitter.instruction("xor edi, edi"); // invalid downcast targets have no usable class id } } ctx.emitter.label(&done); @@ -638,41 +752,41 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< let throw = ctx.next_label("iterator_iterator_downcast_throw"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x9, [sp, #16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - ctx.emitter.instruction(&format!("cbz x9, {}", skip)); // omitted/null class arguments do not constrain aggregates - ctx.emitter.instruction("cmp x9, #1"); // only status 1 carries a valid concrete class id - ctx.emitter.instruction(&format!("b.ne {}", throw)); // invalid names and interfaces throw for aggregates - ctx.emitter.instruction("ldr x0, [sp]"); // pass the saved IteratorAggregate object to the class matcher - ctx.emitter.instruction("ldr x1, [sp, #24]"); // pass the requested downcast class id to the class matcher + ctx.emitter.instruction("ldr x9, [sp, #16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid + ctx.emitter.instruction(&format!("cbz x9, {}", skip)); // omitted/null class arguments do not constrain aggregates + ctx.emitter.instruction("cmp x9, #1"); // only status 1 carries a valid concrete class id + ctx.emitter.instruction(&format!("b.ne {}", throw)); // invalid names and interfaces throw for aggregates + ctx.emitter.instruction("ldr x0, [sp]"); // pass the saved IteratorAggregate object to the class matcher + ctx.emitter.instruction("ldr x1, [sp, #24]"); // pass the requested downcast class id to the class matcher abi::emit_load_int_immediate(ctx.emitter, "x2", 0); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("cmp x0, #0"); // did the aggregate object match the requested class? - ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-base downcast classes are rejected like PHP - ctx.emitter.instruction("ldr x0, [sp, #24]"); // pass the requested class id to the interface checker + ctx.emitter.instruction("cmp x0, #0"); // did the aggregate object match the requested class? + ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-base downcast classes are rejected like PHP + ctx.emitter.instruction("ldr x0, [sp, #24]"); // pass the requested class id to the interface checker abi::emit_load_int_immediate(ctx.emitter, "x1", aggregate_interface_id); abi::emit_call_label(ctx.emitter, "__rt_class_implements_interface"); - ctx.emitter.instruction("cmp x0, #0"); // did the downcast class implement IteratorAggregate? - ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-Traversable base classes are rejected like PHP - ctx.emitter.instruction(&format!("b {}", skip)); // the aggregate downcast class is valid + ctx.emitter.instruction("cmp x0, #0"); // did the downcast class implement IteratorAggregate? + ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-Traversable base classes are rejected like PHP + ctx.emitter.instruction(&format!("b {}", skip)); // the aggregate downcast class is valid } Arch::X86_64 => { - ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - ctx.emitter.instruction("test r10, r10"); // is there an explicit downcast class to validate? - ctx.emitter.instruction(&format!("je {}", skip)); // omitted/null class arguments do not constrain aggregates - ctx.emitter.instruction("cmp r10, 1"); // only status 1 carries a valid concrete class id - ctx.emitter.instruction(&format!("jne {}", throw)); // invalid names and interfaces throw for aggregates - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // pass the saved IteratorAggregate object to the class matcher - ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // pass the requested downcast class id to the class matcher + ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid + ctx.emitter.instruction("test r10, r10"); // is there an explicit downcast class to validate? + ctx.emitter.instruction(&format!("je {}", skip)); // omitted/null class arguments do not constrain aggregates + ctx.emitter.instruction("cmp r10, 1"); // only status 1 carries a valid concrete class id + ctx.emitter.instruction(&format!("jne {}", throw)); // invalid names and interfaces throw for aggregates + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // pass the saved IteratorAggregate object to the class matcher + ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // pass the requested downcast class id to the class matcher abi::emit_load_int_immediate(ctx.emitter, "rdx", 0); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("test rax, rax"); // did the aggregate object match the requested class? - ctx.emitter.instruction(&format!("je {}", throw)); // non-base downcast classes are rejected like PHP - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // pass the requested class id to the interface checker + ctx.emitter.instruction("test rax, rax"); // did the aggregate object match the requested class? + ctx.emitter.instruction(&format!("je {}", throw)); // non-base downcast classes are rejected like PHP + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // pass the requested class id to the interface checker abi::emit_load_int_immediate(ctx.emitter, "rsi", aggregate_interface_id); abi::emit_call_label(ctx.emitter, "__rt_class_implements_interface"); - ctx.emitter.instruction("test rax, rax"); // did the downcast class implement IteratorAggregate? - ctx.emitter.instruction(&format!("je {}", throw)); // non-Traversable base classes are rejected like PHP - ctx.emitter.instruction(&format!("jmp {}", skip)); // the aggregate downcast class is valid + ctx.emitter.instruction("test rax, rax"); // did the downcast class implement IteratorAggregate? + ctx.emitter.instruction(&format!("je {}", throw)); // non-Traversable base classes are rejected like PHP + ctx.emitter.instruction(&format!("jmp {}", skip)); // the aggregate downcast class is valid } } @@ -686,40 +800,49 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage + ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object + ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object abi::emit_symbol_address(ctx.emitter, "x9", "_spl_logic_exception_class_id"); - ctx.emitter.instruction("ldr x9, [x9]"); // load LogicException's runtime class id - ctx.emitter.instruction("str x9, [x0]"); // store the class id at object header + ctx.emitter.instruction("ldr x9, [x9]"); // load LogicException's runtime class id + ctx.emitter.instruction("str x9, [x0]"); // store the class id at object header abi::emit_symbol_address(ctx.emitter, "x9", "_iterator_iterator_downcast_msg"); - ctx.emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer - ctx.emitter.instruction(&format!("mov x9, #{}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // load static exception message length - ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length - ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer + ctx.emitter.instruction(&format!( + "mov x9, #{}", + ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() + )); // load static exception message length + ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length + ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); - ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object - ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder + ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object + ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder } Arch::X86_64 => { - ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation - ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame - ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned - ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage + ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation + ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame + ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned + ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - ctx.emitter.instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header - ctx.emitter.instruction("lea r10, [rip + _iterator_iterator_downcast_msg]"); // materialize static exception message pointer - ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer - ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // store static exception message length - ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active exception object - ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing - ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing - ctx.emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder + ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object + ctx.emitter + .instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header + ctx.emitter + .instruction("lea r10, [rip + _iterator_iterator_downcast_msg]"); // materialize static exception message pointer + ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer + ctx.emitter.instruction(&format!( + "mov QWORD PTR [rax + 16], {}", + ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() + )); // store static exception message length + ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter + .instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active exception object + ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing + ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing + ctx.emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder } } } @@ -731,7 +854,9 @@ fn emit_branch_if_saved_traversable_implements( target_label: &str, ) -> Result<()> { let interface_id = interface_info_by_name(ctx, interface_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("missing interface {}", interface_name)))? + .ok_or_else(|| { + CodegenIrError::unsupported(format!("missing interface {}", interface_name)) + })? .interface_id as i64; match ctx.emitter.target.arch { Arch::AArch64 => { @@ -739,16 +864,16 @@ fn emit_branch_if_saved_traversable_implements( abi::emit_load_int_immediate(ctx.emitter, "x1", interface_id); abi::emit_load_int_immediate(ctx.emitter, "x2", 1); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("cmp x0, #0"); // test whether the saved Traversable matches this interface - ctx.emitter.instruction(&format!("b.ne {}", target_label)); // select the matching IteratorIterator normalization path + ctx.emitter.instruction("cmp x0, #0"); // test whether the saved Traversable matches this interface + ctx.emitter.instruction(&format!("b.ne {}", target_label)); // select the matching IteratorIterator normalization path } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); abi::emit_load_int_immediate(ctx.emitter, "rsi", interface_id); abi::emit_load_int_immediate(ctx.emitter, "rdx", 1); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("test rax, rax"); // test whether the saved Traversable matches this interface - ctx.emitter.instruction(&format!("jne {}", target_label)); // select the matching IteratorIterator normalization path + ctx.emitter.instruction("test rax, rax"); // test whether the saved Traversable matches this interface + ctx.emitter.instruction(&format!("jne {}", target_label)); // select the matching IteratorIterator normalization path } } Ok(()) @@ -757,7 +882,7 @@ fn emit_branch_if_saved_traversable_implements( /// Moves the object result into the receiver ABI slot before an interface method call. fn move_result_to_receiver_arg(ctx: &mut FunctionContext<'_>) { if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // pass the normalized object result as the method receiver + ctx.emitter.instruction("mov rdi, rax"); // pass the normalized object result as the method receiver } } @@ -770,7 +895,12 @@ fn emit_iterator_inner_property_from_result( let base_reg = abi::symbol_scratch_reg(ctx.emitter); let tag_reg = abi::secondary_scratch_reg(ctx.emitter); ctx.load_value_to_reg(target, base_reg)?; - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, inner_offset); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + inner_offset, + ); abi::emit_load_int_immediate(ctx.emitter, tag_reg, 6); abi::emit_store_to_address(ctx.emitter, tag_reg, base_reg, inner_offset + 8); Ok(()) @@ -808,6 +938,7 @@ fn is_builtin_throwable_payload_class(class_name: &str) -> bool { | "ArithmeticError" | "Exception" | "RuntimeException" + | "ReflectionException" | "JsonException" | "FiberError" | "LogicException" @@ -869,20 +1000,23 @@ fn class_declares_own_constructor(class_name: &str, class_info: &ClassInfo) -> b fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request compact Throwable payload storage + ctx.emitter.instruction("mov x0, #32"); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero + ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage + ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 6)); // materialize the x86_64 Throwable heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 6 + )); // materialize the x86_64 Throwable heap kind word + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero } } } @@ -915,12 +1049,13 @@ fn emit_throwable_message_fields_aarch64( ) -> Result<()> { if let Some(message) = message { ctx.load_string_value_to_regs(message, "x1", "x2")?; + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); } else { emit_empty_string_to_regs(ctx, "x1", "x2"); } - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for message initialization - ctx.emitter.instruction("str x1, [x9, #8]"); // store Throwable message pointer - ctx.emitter.instruction("str x2, [x9, #16]"); // store Throwable message length + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for message initialization + ctx.emitter.instruction("str x1, [x9, #8]"); // store Throwable message pointer + ctx.emitter.instruction("str x2, [x9, #16]"); // store Throwable message length Ok(()) } @@ -931,12 +1066,13 @@ fn emit_throwable_message_fields_x86_64( ) -> Result<()> { if let Some(message) = message { ctx.load_string_value_to_regs(message, "rax", "rdx")?; + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); } else { emit_empty_string_to_regs(ctx, "rax", "rdx"); } - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for message initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store Throwable message pointer - ctx.emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store Throwable message length + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for message initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store Throwable message pointer + ctx.emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store Throwable message length Ok(()) } @@ -948,10 +1084,7 @@ fn emit_empty_string_to_regs(ctx: &mut FunctionContext<'_>, ptr_reg: &str, len_r } /// Writes the integer exception code into the compact Throwable payload. -fn emit_throwable_code_field( - ctx: &mut FunctionContext<'_>, - code: Option, -) -> Result<()> { +fn emit_throwable_code_field(ctx: &mut FunctionContext<'_>, code: Option) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => emit_throwable_code_field_aarch64(ctx, code), Arch::X86_64 => emit_throwable_code_field_x86_64(ctx, code), @@ -968,8 +1101,8 @@ fn emit_throwable_code_field_aarch64( } else { abi::emit_load_int_immediate(ctx.emitter, "x1", 0); } - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for code initialization - ctx.emitter.instruction("str x1, [x9, #24]"); // store Throwable code + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for code initialization + ctx.emitter.instruction("str x1, [x9, #24]"); // store Throwable code Ok(()) } @@ -983,8 +1116,8 @@ fn emit_throwable_code_field_x86_64( } else { abi::emit_load_int_immediate(ctx.emitter, "rax", 0); } - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for code initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store Throwable code + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for code initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store Throwable code Ok(()) } @@ -1051,7 +1184,8 @@ fn move_fiber_callable_result_to_arg(ctx: &mut FunctionContext<'_>, callable_arg if result_reg == callable_arg { return; } - ctx.emitter.instruction(&format!("mov {}, {}", callable_arg, result_reg)); // pass selected callable descriptor to Fiber constructor + ctx.emitter + .instruction(&format!("mov {}, {}", callable_arg, result_reg)); // pass selected callable descriptor to Fiber constructor } /// Lowers constrained runtime class-string object construction. @@ -1061,10 +1195,9 @@ pub(super) fn lower_dynamic_object_new( ) -> Result<()> { let (_fallback_class, required_parent) = dynamic_object_new_metadata(ctx, inst)?; let class_name_value = expect_operand(inst, 0)?; - let constructor_args = inst - .operands - .get(1..) - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new missing class operand"))?; + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new missing class operand") + })?; let candidates = dynamic_new_candidates(ctx, &required_parent, constructor_args.len(), inst)?; if candidates.is_empty() { return Err(CodegenIrError::unsupported(format!( @@ -1115,18 +1248,17 @@ pub(super) fn lower_dynamic_object_new_mixed( inst: &Instruction, ) -> Result<()> { let class_name_value = expect_operand(inst, 0)?; - let constructor_args = inst - .operands - .get(1..) - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new_mixed missing class operand"))?; - let result = inst - .result - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new_mixed missing result value"))?; + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new_mixed missing class operand") + })?; + let result = inst.result.ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new_mixed missing result value") + })?; let done_label = ctx.next_label("dynamic_new_mixed_done"); let non_string_label = ctx.next_label("dynamic_new_mixed_non_string"); if !emit_generic_dynamic_new_class_string(ctx, class_name_value, &non_string_label)? { - emit_boxed_null(ctx); - return store_if_result(ctx, inst); + emit_dynamic_new_invalid_class_name_fatal(ctx); + return Ok(()); } abi::emit_push_result_value(ctx.emitter, &PhpType::Str); @@ -1145,18 +1277,85 @@ pub(super) fn lower_dynamic_object_new_mixed( for (candidate, label) in candidates.iter().zip(case_labels.iter()) { ctx.emitter.label(label); abi::emit_release_temporary_stack(ctx.emitter, 16); - emit_dynamic_new_mixed_candidate(ctx, candidate, constructor_args, class_name_value, result)?; + emit_dynamic_new_mixed_candidate( + ctx, + candidate, + constructor_args, + class_name_value, + result, + )?; abi::emit_jump(ctx.emitter, &done_label); } ctx.emitter.label(&fallback_label); - emit_dynamic_new_mixed_fallback(ctx); - ctx.store_result_value(result)?; - abi::emit_jump(ctx.emitter, &done_label); + if builtins::has_eval_context(ctx) { + let eval_miss_label = ctx.next_label("dynamic_new_mixed_eval_miss"); + builtins::lower_eval_object_new_dynamic_fallback(ctx, inst, &eval_miss_label)?; + ctx.store_result_value(result)?; + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&eval_miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); + } else { + emit_dynamic_new_mixed_fallback(ctx); + ctx.store_result_value(result)?; + abi::emit_jump(ctx.emitter, &done_label); + } ctx.emitter.label(&non_string_label); - emit_boxed_null(ctx); - ctx.store_result_value(result)?; + emit_dynamic_new_invalid_class_name_fatal(ctx); + + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Lowers dynamic allocation that intentionally skips PHP constructor dispatch. +pub(super) fn lower_dynamic_object_new_without_constructor_mixed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let class_name_value = expect_operand(inst, 0)?; + if inst.operands.len() != 1 { + return Err(CodegenIrError::invalid_module( + "dynamic_object_new_without_constructor_mixed expects only a class operand", + )); + } + let result = inst.result.ok_or_else(|| { + CodegenIrError::invalid_module( + "dynamic_object_new_without_constructor_mixed missing result value", + ) + })?; + let done_label = ctx.next_label("dynamic_new_no_ctor_mixed_done"); + let non_string_label = ctx.next_label("dynamic_new_no_ctor_mixed_non_string"); + if !emit_generic_dynamic_new_class_string(ctx, class_name_value, &non_string_label)? { + emit_dynamic_new_invalid_class_name_fatal(ctx); + return Ok(()); + } + abi::emit_push_result_value(ctx.emitter, &PhpType::Str); + + let fallback_label = ctx.next_label("dynamic_new_no_ctor_mixed_fallback"); + let candidates = dynamic_new_without_constructor_mixed_candidates(ctx, inst)?; + let case_labels = candidates + .iter() + .map(|candidate| { + let label = ctx.next_label("dynamic_new_no_ctor_mixed_case"); + emit_branch_if_dynamic_new_mixed_class_name_matches(ctx, &candidate.class_name, &label); + label + }) + .collect::>(); + abi::emit_jump(ctx.emitter, &fallback_label); + + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + ctx.emitter.label(label); + abi::emit_release_temporary_stack(ctx.emitter, 16); + emit_dynamic_new_without_constructor_mixed_candidate(ctx, candidate, result)?; + abi::emit_jump(ctx.emitter, &done_label); + } + + ctx.emitter.label(&fallback_label); + emit_dynamic_new_class_not_found_fatal(ctx); + + ctx.emitter.label(&non_string_label); + emit_dynamic_new_invalid_class_name_fatal(ctx); ctx.emitter.label(&done_label); Ok(()) @@ -1180,13 +1379,15 @@ fn emit_generic_dynamic_new_class_string( abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // require a boxed string class name for dynamic construction - ctx.emitter.instruction(&format!("b.ne {}", non_string_label)); // non-string class names produce the runtime null fallback + ctx.emitter.instruction("cmp x0, #1"); // require a boxed string class name for dynamic construction + ctx.emitter + .instruction(&format!("b.ne {}", non_string_label)); // non-string class names produce the runtime null fallback } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // require a boxed string class name for dynamic construction - ctx.emitter.instruction(&format!("jne {}", non_string_label)); // non-string class names produce the runtime null fallback - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the string result register + ctx.emitter.instruction("cmp rax, 1"); // require a boxed string class name for dynamic construction + ctx.emitter + .instruction(&format!("jne {}", non_string_label)); // non-string class names produce the runtime null fallback + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the string result register } } Ok(true) @@ -1217,6 +1418,27 @@ fn dynamic_new_mixed_candidates( Ok(candidates) } +/// Returns AOT candidates that can be allocated without constructor dispatch. +fn dynamic_new_without_constructor_mixed_candidates( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let mut candidates = Vec::new(); + let mut sorted_classes = ctx.module.class_infos.iter().collect::>(); + sorted_classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in sorted_classes { + if !is_dynamic_new_mixed_aot_candidate(class_name) { + continue; + } + if let Some(candidate) = + dynamic_new_without_constructor_candidate(ctx, class_name, class_info, inst)? + { + candidates.push(candidate); + } + } + Ok(candidates) +} + /// Returns true when a class can safely use the static allocation path for `new $name`. fn is_dynamic_new_mixed_aot_candidate(class_name: &str) -> bool { if class_name.starts_with("__Elephc") { @@ -1252,9 +1474,7 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "OverflowException", "RangeException", "RecursiveCallbackFilterIterator", - "ReflectionClass", - "ReflectionMethod", - "ReflectionProperty", + "ReflectionException", "RuntimeException", "SplDoublyLinkedList", "SplFixedArray", @@ -1316,8 +1536,19 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RecursiveRegexIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", + "ReflectionException", + "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", + "ReflectionParameter", "ReflectionProperty", + "ReflectionUnionType", + "ReflectionIntersectionType", "RegexIterator", "RuntimeException", "SplDoublyLinkedList", @@ -1355,8 +1586,8 @@ fn emit_branch_if_dynamic_new_mixed_class_name_matches( abi::emit_symbol_address(ctx.emitter, "x3", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "x4", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("cmp x0, #0"); // check whether the dynamic class-string matches this AOT class - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // select this AOT allocation path on a class-name match + ctx.emitter.instruction("cmp x0, #0"); // check whether the dynamic class-string matches this AOT class + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // select this AOT allocation path on a class-name match } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); @@ -1364,8 +1595,8 @@ fn emit_branch_if_dynamic_new_mixed_class_name_matches( abi::emit_symbol_address(ctx.emitter, "rdx", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "rcx", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("test rax, rax"); // check whether the dynamic class-string matches this AOT class - ctx.emitter.instruction(&format!("je {}", matched_label)); // select this AOT allocation path on a class-name match + ctx.emitter.instruction("test rax, rax"); // check whether the dynamic class-string matches this AOT class + ctx.emitter.instruction(&format!("je {}", matched_label)); // select this AOT allocation path on a class-name match } } } @@ -1415,10 +1646,34 @@ fn emit_dynamic_new_mixed_candidate( } abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); abi::emit_release_temporary_stack(ctx.emitter, 16); - emit_box_current_value_as_mixed( - ctx.emitter, - &PhpType::Object(candidate.class_name.clone()), - ); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(candidate.class_name.clone())); + ctx.store_result_value(result) +} + +/// Allocates one constructorless dynamic-new candidate and boxes it as Mixed. +fn emit_dynamic_new_without_constructor_mixed_candidate( + ctx: &mut FunctionContext<'_>, + candidate: &DynamicNewCandidate, + result: ValueId, +) -> Result<()> { + emit_object_allocation( + ctx, + candidate.class_id, + candidate.property_count, + candidate.allow_dynamic_properties, + &candidate.uninitialized_marker_offsets, + &candidate.owned_reference_property_offsets, + )?; + let object_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, object_reg); + let object_base_reg = abi::secondary_scratch_reg(ctx.emitter); + for default in &candidate.property_defaults { + abi::emit_load_temporary_stack_slot(ctx.emitter, object_base_reg, 0); + emit_property_default(ctx, object_base_reg, default)?; + } + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_release_temporary_stack(ctx.emitter, 16); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(candidate.class_name.clone())); ctx.store_result_value(result) } @@ -1512,30 +1767,34 @@ fn emit_dynamic_new_mixed_constructor_call( emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks) } -/// Invokes the runtime class-name registry fallback and boxes object/null as Mixed. +/// Invokes the runtime class-name registry fallback and boxes a matched object as Mixed. fn emit_dynamic_new_mixed_fallback(ctx: &mut FunctionContext<'_>) { - let null_label = ctx.next_label("dynamic_new_mixed_null"); + let miss_label = ctx.next_label("dynamic_new_mixed_missing_class"); let done_label = ctx.next_label("dynamic_new_mixed_fallback_done"); match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction(&format!("cbz x0, {}", null_label)); // registry miss returns PHP null for dynamic construction + ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // registry miss is PHP's class-not-found fatal for source-level dynamic construction + abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("b {}", done_label)); // skip null boxing after a registry allocation - ctx.emitter.label(&null_label); - emit_boxed_null(ctx); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.label(&miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); } Arch::X86_64 => { - abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rax", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction("test rax, rax"); // registry miss returns PHP null for dynamic construction - ctx.emitter.instruction(&format!("jz {}", null_label)); // box PHP null when no runtime class table entry matched + ctx.emitter.instruction("test rax, rax"); // did the runtime class registry produce an object? + ctx.emitter.instruction(&format!("jz {}", miss_label)); // registry miss is PHP's class-not-found fatal + abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip null boxing after a registry allocation - ctx.emitter.label(&null_label); - emit_boxed_null(ctx); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.label(&miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); } } @@ -1667,6 +1926,38 @@ fn dynamic_new_candidate( })) } +/// Builds a dynamic-new candidate that deliberately omits constructor invocation. +fn dynamic_new_without_constructor_candidate( + ctx: &FunctionContext<'_>, + class_name: &str, + class_info: &ClassInfo, + inst: &Instruction, +) -> Result> { + if class_info.is_abstract || ctx.module.enum_infos.contains_key(class_name) { + return Ok(None); + } + if class_name != "stdClass" && known_dynamic_new_builtin_class_names().contains(&class_name) { + return Ok(None); + } + if class_name == "SplFixedArray" || is_spl_doubly_linked_list_family(class_name) { + return Ok(None); + } + if class_interfaces_require_missing_method_symbols(ctx, class_name, class_info) { + return Ok(None); + } + let property_defaults = collect_property_defaults(class_info, inst)?; + Ok(Some(DynamicNewCandidate { + class_name: class_name.to_string(), + class_id: class_info.class_id, + property_count: class_info.properties.len(), + allow_dynamic_properties: class_info.allow_dynamic_properties, + uninitialized_marker_offsets: uninitialized_property_marker_offsets(class_info), + owned_reference_property_offsets: owned_reference_property_offsets(class_info), + property_defaults, + constructor_impl: None, + })) +} + /// Builds dynamic-new metadata for SPL classes whose storage is runtime-managed. fn spl_runtime_storage_dynamic_new_candidate( class_name: &str, @@ -1721,46 +2012,40 @@ fn emit_dynamic_new_class_lookup( } /// Unboxes a mixed class-string or emits the dynamic-factory fatal. -fn emit_dynamic_new_mixed_class_string( - ctx: &mut FunctionContext<'_>, - required_parent: &str, -) { +fn emit_dynamic_new_mixed_class_string(ctx: &mut FunctionContext<'_>, required_parent: &str) { let string_label = ctx.next_label("dynamic_new_class_string"); abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic factory argument is a string - ctx.emitter.instruction(&format!("b.eq {}", string_label)); // continue only when the boxed factory argument is a class-string + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic factory argument is a string + ctx.emitter.instruction(&format!("b.eq {}", string_label)); // continue only when the boxed factory argument is a class-string emit_dynamic_new_fatal(ctx, required_parent); ctx.emitter.label(&string_label); } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic factory argument is a string - ctx.emitter.instruction(&format!("je {}", string_label)); // continue only when the boxed factory argument is a class-string + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic factory argument is a string + ctx.emitter.instruction(&format!("je {}", string_label)); // continue only when the boxed factory argument is a class-string emit_dynamic_new_fatal(ctx, required_parent); ctx.emitter.label(&string_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register } } } /// Branches when the dynamic factory lookup failed or named an interface. -fn emit_branch_if_dynamic_new_lookup_invalid( - ctx: &mut FunctionContext<'_>, - invalid_label: &str, -) { +fn emit_branch_if_dynamic_new_lookup_invalid(ctx: &mut FunctionContext<'_>, invalid_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? - ctx.emitter.instruction(&format!("b.eq {}", invalid_label)); // abort unresolved factory classes before construction - ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("b.ne {}", invalid_label)); // abort interface targets because factories instantiate objects + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? + ctx.emitter.instruction(&format!("b.eq {}", invalid_label)); // abort unresolved factory classes before construction + ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("b.ne {}", invalid_label)); // abort interface targets because factories instantiate objects } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the dynamic factory class-string resolve to metadata? - ctx.emitter.instruction(&format!("je {}", invalid_label)); // abort unresolved factory classes before construction - ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("jne {}", invalid_label)); // abort interface targets because factories instantiate objects + ctx.emitter.instruction("test rax, rax"); // did the dynamic factory class-string resolve to metadata? + ctx.emitter.instruction(&format!("je {}", invalid_label)); // abort unresolved factory classes before construction + ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("jne {}", invalid_label)); // abort interface targets because factories instantiate objects } } } @@ -1783,12 +2068,14 @@ fn emit_compare_dynamic_new_class_id( abi::emit_load_temporary_stack_slot(ctx.emitter, scratch, 0); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this candidate class id - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor + ctx.emitter + .instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this candidate class id + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this candidate class id - ctx.emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor + ctx.emitter + .instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this candidate class id + ctx.emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor } } } @@ -1834,23 +2121,77 @@ fn emit_dynamic_new_fatal(ctx: &mut FunctionContext<'_>, required_parent: &str) emit_fatal_message(ctx, message.as_bytes()); } +/// Emits PHP's fatal diagnostic for source-level `new $name` with a missing class. +fn emit_dynamic_new_class_not_found_fatal(ctx: &mut FunctionContext<'_>) { + let (prefix_label, prefix_len) = ctx + .data + .add_string(b"Fatal error: Uncaught Error: Class \""); + let (suffix_label, suffix_len) = ctx.data.add_string(b"\" not found\n"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found prefix + ctx.emitter.adrp("x1", &prefix_label); + ctx.emitter.add_lo12("x1", "x1", &prefix_label); + ctx.emitter.instruction(&format!("mov x2, #{}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.syscall(4); + ctx.emitter.instruction("mov x0, #2"); // select stderr for the missing class name + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); + ctx.emitter.syscall(4); + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found suffix + ctx.emitter.adrp("x1", &suffix_label); + ctx.emitter.add_lo12("x1", "x1", &suffix_label); + ctx.emitter.instruction(&format!("mov x2, #{}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.syscall(4); + } + Arch::X86_64 => { + abi::emit_symbol_address(ctx.emitter, "rsi", &prefix_label); + ctx.emitter.instruction(&format!("mov edx, {}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found prefix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the prefix + ctx.emitter.instruction("syscall"); // write the class-not-found prefix + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); + ctx.emitter.instruction("mov edi, 2"); // select stderr for the missing class name + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the class name + ctx.emitter.instruction("syscall"); // write the missing class name + abi::emit_symbol_address(ctx.emitter, "rsi", &suffix_label); + ctx.emitter.instruction(&format!("mov edx, {}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found suffix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the suffix + ctx.emitter.instruction("syscall"); // write the class-not-found suffix + } + } + abi::emit_exit(ctx.emitter, 1); +} + +/// Emits PHP's fatal diagnostic for `new $name` when the class expression is not a string. +fn emit_dynamic_new_invalid_class_name_fatal(ctx: &mut FunctionContext<'_>) { + emit_fatal_message( + ctx, + b"Fatal error: Uncaught Error: Class name must be a valid object or a string\n", + ); +} + /// Writes a fatal diagnostic to stderr and exits. fn emit_fatal_message(ctx: &mut FunctionContext<'_>, message: &[u8]) { let (message_label, message_len) = ctx.data.add_string(message); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // select stderr for the fatal diagnostic + ctx.emitter.instruction("mov x0, #2"); // select stderr for the fatal diagnostic ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the fatal diagnostic byte length to write() + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the fatal diagnostic byte length to write() ctx.emitter.syscall(4); } Arch::X86_64 => { abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); - ctx.emitter.instruction(&format!("mov edx, {}", message_len)); // pass the fatal diagnostic byte length to write() - ctx.emitter.instruction("mov edi, 2"); // select stderr for the fatal diagnostic - ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall - ctx.emitter.instruction("syscall"); // write the fatal diagnostic bytes + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the fatal diagnostic byte length to write() + ctx.emitter.instruction("mov edi, 2"); // select stderr for the fatal diagnostic + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall + ctx.emitter.instruction("syscall"); // write the fatal diagnostic bytes } } abi::emit_exit(ctx.emitter, 1); @@ -1866,11 +2207,7 @@ fn collect_property_defaults( let Some(default_expr) = class_info.defaults.get(index).and_then(Option::as_ref) else { continue; }; - let offset = class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16); + let offset = 8 + index * 16; defaults.push(PropertyDefault { offset, value: literal_default_value( @@ -1937,10 +2274,14 @@ fn emit_property_default( abi::emit_symbol_address(ctx.emitter, scratch, &label); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("ldr {}, [{}]", float_reg, scratch)); // load the property default float literal through the symbol scratch register + ctx.emitter + .instruction(&format!("ldr {}, [{}]", float_reg, scratch)); + // load the property default float literal through the symbol scratch register } Arch::X86_64 => { - ctx.emitter.instruction(&format!("movsd {}, QWORD PTR [{}]", float_reg, scratch)); // load the property default float literal through the symbol scratch register + ctx.emitter + .instruction(&format!("movsd {}, QWORD PTR [{}]", float_reg, scratch)); + // load the property default float literal through the symbol scratch register } } abi::emit_store_to_address(ctx.emitter, float_reg, object_reg, default.offset); @@ -2090,7 +2431,10 @@ pub(super) fn lower_prop_get(ctx: &mut FunctionContext<'_>, inst: &Instruction) if let Some(class_name) = union_object_member_class(ctx, object)? { return lower_union_object_prop_get(ctx, inst, object, &class_name, &property); } - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_mixed_prop_get(ctx, inst, object, &property); } if object_is_builtin_stdclass(ctx, object)? { @@ -2206,6 +2550,24 @@ fn lower_mixed_load_prop_ref_cell( store_ref_cell_pointer_result(ctx, inst) } +/// Lowers a declared object-property initialization probe. +pub(super) fn lower_prop_initialized( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let object = expect_operand(inst, 0)?; + let property = property_name_immediate(ctx, inst)?.to_string(); + let slot = resolve_property_slot(ctx, object, &property, inst)?; + if !slot.is_declared { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + return store_if_result(ctx, inst); + } + let base_reg = abi::symbol_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(object, base_reg)?; + emit_typed_property_initialized_bool(ctx, &slot, base_reg); + store_if_result(ctx, inst) +} + /// Returns the receiver class when an undeclared property should route through `__get`. fn magic_get_receiver_class( ctx: &FunctionContext<'_>, @@ -2219,7 +2581,11 @@ fn magic_get_receiver_class( let Some(class_info) = ctx.module.class_infos.get(normalized) else { return Ok(None); }; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return Ok(None); } if class_info.methods.contains_key(&php_symbol_key("__get")) { @@ -2247,7 +2613,10 @@ fn lower_magic_get_prop( if let Some(slot) = target.dynamic_slot { super::emit_dynamic_instance_method_call(ctx, slot); } else { - abi::emit_call_label(ctx.emitter, &method_symbol(&target.impl_class, &target.method_key)); + abi::emit_call_label( + ctx.emitter, + &method_symbol(&target.impl_class, &target.method_key), + ); } store_method_call_result(ctx, inst, &target) } @@ -2324,23 +2693,27 @@ fn lower_allow_dynamic_prop_get( ctx.load_value_to_reg(object, object_reg)?; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter + .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); - ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // missing dynamic properties read as PHP null - ctx.emitter.instruction("mov x0, x1"); // return the boxed Mixed cell stored in the hash entry - ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit + ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // missing dynamic properties read as PHP null + ctx.emitter.instruction("mov x0, x1"); // return the boxed Mixed cell stored in the hash entry + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rdi, QWORD PTR [{} + {}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter.instruction(&format!( + "mov rdi, QWORD PTR [{} + {}]", + object_reg, hash_offset + )); // load the dynamic-property hash pointer from the receiver abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); - ctx.emitter.instruction("test rax, rax"); // check whether the dynamic-property key was present - ctx.emitter.instruction(&format!("je {}", miss_label)); // missing dynamic properties read as PHP null - ctx.emitter.instruction("mov rax, rdi"); // return the boxed Mixed cell stored in the hash entry - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the null fallback after a successful dynamic-property hit + ctx.emitter.instruction("test rax, rax"); // check whether the dynamic-property key was present + ctx.emitter.instruction(&format!("je {}", miss_label)); // missing dynamic properties read as PHP null + ctx.emitter.instruction("mov rax, rdi"); // return the boxed Mixed cell stored in the hash entry + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the null fallback after a successful dynamic-property hit } } ctx.emitter.label(&miss_label); @@ -2499,14 +2872,14 @@ fn declared_mixed_property_candidates( fn emit_mixed_object_payload_or_null(ctx: &mut FunctionContext<'_>, null_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // check whether the Mixed receiver holds an object payload - ctx.emitter.instruction(&format!("b.ne {}", null_label)); // non-object Mixed receivers produce a null property result - ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object payload for class-id dispatch + ctx.emitter.instruction("cmp x0, #6"); // check whether the Mixed receiver holds an object payload + ctx.emitter.instruction(&format!("b.ne {}", null_label)); // non-object Mixed receivers produce a null property result + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object payload for class-id dispatch } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // check whether the Mixed receiver holds an object payload - ctx.emitter.instruction(&format!("jne {}", null_label)); // non-object Mixed receivers produce a null property result - ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object payload for class-id dispatch + ctx.emitter.instruction("cmp rax, 6"); // check whether the Mixed receiver holds an object payload + ctx.emitter.instruction(&format!("jne {}", null_label)); // non-object Mixed receivers produce a null property result + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object payload for class-id dispatch } } } @@ -2520,20 +2893,20 @@ fn emit_mixed_property_class_dispatch( ) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x9, [x0]"); // load the receiver class id for Mixed property dispatch + ctx.emitter.instruction("ldr x9, [x0]"); // load the receiver class id for Mixed property dispatch for (candidate, label) in candidates.iter().zip(match_labels.iter()) { abi::emit_load_int_immediate(ctx.emitter, "x10", candidate.class_id as i64); - ctx.emitter.instruction("cmp x9, x10"); // compare the receiver class id against this declared-property owner - ctx.emitter.instruction(&format!("b.eq {}", label)); // read the declared property when the class id matches + ctx.emitter.instruction("cmp x9, x10"); // compare the receiver class id against this declared-property owner + ctx.emitter.instruction(&format!("b.eq {}", label)); // read the declared property when the class id matches } emit_branch_to_stdclass_candidate(ctx, "x9", "x10", stdclass_label); } Arch::X86_64 => { - ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for Mixed property dispatch + ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for Mixed property dispatch for (candidate, label) in candidates.iter().zip(match_labels.iter()) { abi::emit_load_int_immediate(ctx.emitter, "r10", candidate.class_id as i64); - ctx.emitter.instruction("cmp r11, r10"); // compare the receiver class id against this declared-property owner - ctx.emitter.instruction(&format!("je {}", label)); // read the declared property when the class id matches + ctx.emitter.instruction("cmp r11, r10"); // compare the receiver class id against this declared-property owner + ctx.emitter.instruction(&format!("je {}", label)); // read the declared property when the class id matches } emit_branch_to_stdclass_candidate(ctx, "r11", "r10", stdclass_label); } @@ -2553,12 +2926,14 @@ fn emit_branch_to_stdclass_candidate( abi::emit_load_int_immediate(ctx.emitter, scratch_reg, stdclass_id as i64); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage - ctx.emitter.instruction(&format!("b.eq {}", stdclass_label)); // route stdClass reads through the hash-backed helper + ctx.emitter + .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage + ctx.emitter.instruction(&format!("b.eq {}", stdclass_label)); // route stdClass reads through the hash-backed helper } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage - ctx.emitter.instruction(&format!("je {}", stdclass_label)); // route stdClass reads through the hash-backed helper + ctx.emitter + .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage + ctx.emitter.instruction(&format!("je {}", stdclass_label)); // route stdClass reads through the hash-backed helper } } } @@ -2592,7 +2967,7 @@ fn emit_stdclass_get_from_loaded_object(ctx: &mut FunctionContext<'_>, property: abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, rax"); // pass the unboxed stdClass object pointer to the dynamic getter + ctx.emitter.instruction("mov rdi, rax"); // pass the unboxed stdClass object pointer to the dynamic getter abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); @@ -2604,12 +2979,12 @@ fn emit_stdclass_get_from_loaded_object(ctx: &mut FunctionContext<'_>, property: fn emit_branch_if_mixed_unboxed_object(ctx: &mut FunctionContext<'_>, object_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed union holds an object payload - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // read the declared property only for object payloads + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed union holds an object payload + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // read the declared property only for object payloads } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed union holds an object payload - ctx.emitter.instruction(&format!("je {}", object_label)); // read the declared property only for object payloads + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed union holds an object payload + ctx.emitter.instruction(&format!("je {}", object_label)); // read the declared property only for object payloads } } } @@ -2618,10 +2993,10 @@ fn emit_branch_if_mixed_unboxed_object(ctx: &mut FunctionContext<'_>, object_lab fn move_mixed_unboxed_object_payload(ctx: &mut FunctionContext<'_>, base_reg: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, x1", base_reg)); // use the unboxed object pointer as the declared-property base + ctx.emitter.instruction(&format!("mov {}, x1", base_reg)); // use the unboxed object pointer as the declared-property base } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, rdi", base_reg)); // use the unboxed object pointer as the declared-property base + ctx.emitter.instruction(&format!("mov {}, rdi", base_reg)); // use the unboxed object pointer as the declared-property base } } } @@ -2656,17 +3031,22 @@ fn lower_nullable_prop_get_with_warning( /// Emits PHP's warning for reading a property from null. fn emit_property_on_null_warning(ctx: &mut FunctionContext<'_>, property: &str) { - let message = format!("Warning: Attempt to read property \"{}\" on null\n", property); + let message = format!( + "Warning: Attempt to read property \"{}\" on null\n", + property + ); let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the property-on-null warning byte length + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the property-on-null warning byte length } Arch::X86_64 => { abi::emit_symbol_address(ctx.emitter, "rdi", &message_label); - ctx.emitter.instruction(&format!("mov esi, {}", message_len)); // pass the property-on-null warning byte length + ctx.emitter + .instruction(&format!("mov esi, {}", message_len)); // pass the property-on-null warning byte length } } abi::emit_call_label(ctx.emitter, "__rt_diag_warning"); @@ -2716,7 +3096,10 @@ pub(super) fn lower_dynamic_prop_get( if let Some(property) = const_string_operand(ctx, property_value)? { return lower_const_dynamic_prop_get(ctx, object, property, inst); } - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_runtime_dynamic_mixed_prop_get(ctx, inst, object, property_value); } if object_is_builtin_stdclass(ctx, object)? { @@ -2732,7 +3115,10 @@ fn lower_const_dynamic_prop_get( property: &str, inst: &Instruction, ) -> Result<()> { - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_mixed_prop_get(ctx, inst, object, property); } if object_is_builtin_stdclass(ctx, object)? { @@ -2763,18 +3149,63 @@ fn lower_runtime_dynamic_mixed_prop_get( property_value: ValueId, ) -> Result<()> { ensure_runtime_dynamic_property_name(ctx, property_value, inst)?; - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.load_value_to_reg(object, "x0")?; - ctx.load_string_value_to_regs(property_value, "x1", "x2")?; - } - Arch::X86_64 => { - ctx.load_value_to_reg(object, "rdi")?; - ctx.load_string_value_to_regs(property_value, "rsi", "rdx")?; + ensure_dynamic_property_miss_supported(inst)?; + let candidates = declared_mixed_property_get_candidates(ctx, inst)?; + let done_label = ctx.next_label("mixed_dyn_prop_get_done"); + let miss_label = ctx.next_label("mixed_dyn_prop_get_miss"); + let miss_no_stack_label = ctx.next_label("mixed_dyn_prop_get_miss_no_stack"); + let stdclass_label = ctx.next_label("mixed_dyn_prop_get_stdclass"); + let match_labels = candidates + .iter() + .map(|candidate| { + ctx.next_label(&format!( + "mixed_dyn_prop_get_{}", + label_fragment(&candidate.slot.property) + )) + }) + .collect::>(); + + ctx.load_value_to_reg(object, abi::int_result_reg(ctx.emitter))?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_mixed_unboxed_not_object(ctx, &miss_no_stack_label); + push_mixed_unboxed_object_payload(ctx); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + ctx.load_string_value_to_regs(property_value, ptr_reg, len_reg)?; + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + emit_branch_if_mixed_dynamic_property_candidate_matches(ctx, candidate, label); + } + emit_branch_if_stacked_object_is_stdclass(ctx, 16, &stdclass_label); + abi::emit_jump(ctx.emitter, &miss_label); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + ctx.emitter.label(label); + let base_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, base_reg, 16); + if candidate.slot.is_declared { + emit_uninitialized_typed_property_guard(ctx, &candidate.slot, base_reg); } + emit_property_load(ctx, &candidate.slot, base_reg)?; + materialize_loaded_property_result(ctx, inst, &candidate.slot.php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_jump(ctx.emitter, &done_label); } - abi::emit_call_label(ctx.emitter, "__rt_mixed_property_get"); - cast_loaded_mixed_pointer_to_result(ctx, &inst.result_php_type.codegen_repr())?; + + ctx.emitter.label(&stdclass_label); + emit_runtime_stdclass_get_for_stacked_name(ctx, inst, 16, 0)?; + abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&miss_label); + abi::emit_release_temporary_stack(ctx.emitter, 32); + emit_dynamic_property_miss_result(ctx, inst); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&miss_no_stack_label); + emit_dynamic_property_miss_result(ctx, inst); + + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -2894,11 +3325,10 @@ fn declared_dynamic_property_slots( ) -> Result> { let normalized = class_name.trim_start_matches('\\'); let property_names = { - let class_info = ctx - .module - .class_infos - .get(normalized) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; + let class_info = + ctx.module.class_infos.get(normalized).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", normalized)) + })?; class_info .properties .iter() @@ -2911,6 +3341,36 @@ fn declared_dynamic_property_slots( .collect() } +/// Collects declared-property candidates readable from a boxed Mixed receiver. +fn declared_mixed_property_get_candidates( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let mut candidates = Vec::new(); + let mut sorted_classes = ctx.module.class_infos.iter().collect::>(); + sorted_classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in sorted_classes { + if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { + continue; + } + for (property, _) in &class_info.properties { + let Ok(slot) = resolve_property_slot_for_class(ctx, class_name, property, inst) else { + continue; + }; + candidates.push(MixedPropertyCandidate { + class_id: class_info.class_id, + slot, + }); + } + } + candidates.sort_by(|left, right| { + left.class_id + .cmp(&right.class_id) + .then_with(|| left.slot.property.cmp(&right.slot.property)) + }); + Ok(candidates) +} + /// Verifies that the EIR result type can receive every declared property candidate. fn ensure_dynamic_property_slot_results_supported( slots: &[PropertySlot], @@ -2997,17 +3457,18 @@ fn emit_branch_if_dynamic_name_matches( abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); abi::emit_symbol_address(ctx.emitter, "x3", &label); abi::emit_load_int_immediate(ctx.emitter, "x4", len as i64); - ctx.emitter.instruction("bl __rt_str_eq"); // compare the runtime property name against this declared property - ctx.emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property slot when the names match + ctx.emitter.instruction("bl __rt_str_eq"); // compare the runtime property name against this declared property + ctx.emitter + .instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property slot when the names match } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 8); abi::emit_symbol_address(ctx.emitter, "rdx", &label); abi::emit_load_int_immediate(ctx.emitter, "rcx", len as i64); - ctx.emitter.instruction("call __rt_str_eq"); // compare the runtime property name against this declared property - ctx.emitter.instruction("test rax, rax"); // check whether the runtime string comparison matched - ctx.emitter.instruction(&format!("jne {}", target_label)); // dispatch to the declared property slot when the names match + ctx.emitter.instruction("call __rt_str_eq"); // compare the runtime property name against this declared property + ctx.emitter.instruction("test rax, rax"); // check whether the runtime string comparison matched + ctx.emitter.instruction(&format!("jne {}", target_label)); // dispatch to the declared property slot when the names match } } } @@ -3213,7 +3674,7 @@ fn lower_runtime_mixed_prop_set( abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); for (candidate, label) in candidates.iter().zip(match_labels.iter()) { - emit_branch_if_mixed_dynamic_set_candidate_matches(ctx, candidate, label); + emit_branch_if_mixed_dynamic_property_candidate_matches(ctx, candidate, label); } emit_branch_if_stacked_object_is_stdclass(ctx, 16, &stdclass_label); abi::emit_jump(ctx.emitter, &miss_label); @@ -3307,12 +3768,12 @@ fn declared_mixed_property_set_candidates( fn emit_branch_if_mixed_unboxed_not_object(ctx: &mut FunctionContext<'_>, target_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed receiver holds an object payload - ctx.emitter.instruction(&format!("b.ne {}", target_label)); // non-object dynamic property writes are ignored + ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed receiver holds an object payload + ctx.emitter.instruction(&format!("b.ne {}", target_label)); // non-object dynamic property writes are ignored } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed receiver holds an object payload - ctx.emitter.instruction(&format!("jne {}", target_label)); // non-object dynamic property writes are ignored + ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed receiver holds an object payload + ctx.emitter.instruction(&format!("jne {}", target_label)); // non-object dynamic property writes are ignored } } } @@ -3326,7 +3787,7 @@ fn push_mixed_unboxed_object_payload(ctx: &mut FunctionContext<'_>) { } /// Branches when both the stacked object class id and runtime property name match. -fn emit_branch_if_mixed_dynamic_set_candidate_matches( +fn emit_branch_if_mixed_dynamic_property_candidate_matches( ctx: &mut FunctionContext<'_>, candidate: &MixedPropertyCandidate, matched_label: &str, @@ -3335,17 +3796,17 @@ fn emit_branch_if_mixed_dynamic_set_candidate_matches( match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 16); - ctx.emitter.instruction("ldr x10, [x9]"); // load the candidate receiver class id + ctx.emitter.instruction("ldr x10, [x9]"); // load the candidate receiver class id abi::emit_load_int_immediate(ctx.emitter, "x11", candidate.class_id as i64); - ctx.emitter.instruction("cmp x10, x11"); // compare receiver class id before checking the property name - ctx.emitter.instruction(&format!("b.ne {}", next_label)); // skip name comparison for unrelated classes + ctx.emitter.instruction("cmp x10, x11"); // compare receiver class id before checking the property name + ctx.emitter.instruction(&format!("b.ne {}", next_label)); // skip name comparison for unrelated classes } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "r11", 16); - ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the candidate receiver class id + ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the candidate receiver class id abi::emit_load_int_immediate(ctx.emitter, "r12", candidate.class_id as i64); - ctx.emitter.instruction("cmp r10, r12"); // compare receiver class id before checking the property name - ctx.emitter.instruction(&format!("jne {}", next_label)); // skip name comparison for unrelated classes + ctx.emitter.instruction("cmp r10, r12"); // compare receiver class id before checking the property name + ctx.emitter.instruction(&format!("jne {}", next_label)); // skip name comparison for unrelated classes } } emit_branch_if_dynamic_name_matches(ctx, &candidate.slot.property, matched_label); @@ -3364,21 +3825,44 @@ fn emit_branch_if_stacked_object_is_stdclass( match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", object_stack_offset); - ctx.emitter.instruction("ldr x10, [x9]"); // load the stacked object's class id + ctx.emitter.instruction("ldr x10, [x9]"); // load the stacked object's class id abi::emit_load_int_immediate(ctx.emitter, "x11", stdclass_id as i64); - ctx.emitter.instruction("cmp x10, x11"); // check whether the runtime receiver is stdClass - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // route stdClass writes through the dynamic-property helper + ctx.emitter.instruction("cmp x10, x11"); // check whether the runtime receiver is stdClass + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // route stdClass writes through the dynamic-property helper } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "r11", object_stack_offset); - ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the stacked object's class id + ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the stacked object's class id abi::emit_load_int_immediate(ctx.emitter, "r12", stdclass_id as i64); - ctx.emitter.instruction("cmp r10, r12"); // check whether the runtime receiver is stdClass - ctx.emitter.instruction(&format!("je {}", matched_label)); // route stdClass writes through the dynamic-property helper + ctx.emitter.instruction("cmp r10, r12"); // check whether the runtime receiver is stdClass + ctx.emitter.instruction(&format!("je {}", matched_label)); // route stdClass writes through the dynamic-property helper } } } +/// Calls `__rt_stdclass_get` using a stacked object pointer and runtime name pair. +fn emit_runtime_stdclass_get_for_stacked_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object_stack_offset: usize, + name_stack_offset: usize, +) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "x0", object_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", name_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", name_stack_offset + 8); + } + Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", object_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", name_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", name_stack_offset + 8); + } + } + abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); + cast_loaded_mixed_pointer_to_result(ctx, &inst.result_php_type.codegen_repr()) +} + /// Calls `__rt_stdclass_set` using a stacked object pointer and runtime name pair. fn emit_runtime_stdclass_set_for_stacked_name( ctx: &mut FunctionContext<'_>, @@ -3482,29 +3966,41 @@ fn lower_allow_dynamic_prop_set( materialize_dynamic_property_mixed_value(ctx, value, &value_ty)?; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, x0", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore + ctx.emitter.instruction(&format!("mov {}, x0", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); - ctx.emitter.instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter + .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); - ctx.emitter.instruction(&format!("mov x3, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload - ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use the high payload word - abi::emit_load_int_immediate(ctx.emitter, "x5", runtime_value_tag(&PhpType::Mixed) as i64); + ctx.emitter.instruction(&format!("mov x3, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use the high payload word + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_set"); abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, "x0", object_reg, hash_offset); } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, rax", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore + ctx.emitter.instruction(&format!("mov {}, rax", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); - ctx.emitter.instruction(&format!("mov rdi, QWORD PTR [{} + {}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter.instruction(&format!( + "mov rdi, QWORD PTR [{} + {}]", + object_reg, hash_offset + )); // load the dynamic-property hash pointer from the receiver abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); - ctx.emitter.instruction(&format!("mov rcx, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload - ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash entries do not use the high payload word - abi::emit_load_int_immediate(ctx.emitter, "r9", runtime_value_tag(&PhpType::Mixed) as i64); + ctx.emitter.instruction(&format!("mov rcx, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash entries do not use the high payload word + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_set"); abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, "rax", object_reg, hash_offset); @@ -3565,19 +4061,21 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the property-assign-on-null fatal to stderr + ctx.emitter.instruction("mov x0, #2"); // write the property-assign-on-null fatal to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the property-assign-on-null fatal byte length + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the property-assign-on-null fatal byte length ctx.emitter.syscall(4); abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the property-assign-on-null fatal to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the property-assign-on-null fatal to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); - ctx.emitter.instruction(&format!("mov edx, {}", message_len)); // pass the property-assign-on-null fatal byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the property-assign-on-null fatal before exiting + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the property-assign-on-null fatal byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the property-assign-on-null fatal before exiting abi::emit_exit(ctx.emitter, 1); } } @@ -3587,12 +4085,18 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let value = expect_operand(inst, 0)?; let value_ty = ctx.value_php_type(value)?; - if !matches!(value_ty, PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_)) { + if !matches!( + value_ty, + PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_) + ) { emit_false(ctx); return store_if_result(ctx, inst); } - let class_name = class_name_immediate(ctx, inst)?; - let Some((target_id, target_kind)) = classify_named_target(ctx, class_name) else { + let class_name = class_name_immediate(ctx, inst)?.to_string(); + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_object_is_a(ctx, inst, value, &class_name, false); + } + let Some((target_id, target_kind)) = classify_named_target(ctx, &class_name) else { emit_false(ctx); return store_if_result(ctx, inst); }; @@ -3611,9 +4115,15 @@ pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction } /// Lowers dynamic `instanceof` where the target is resolved from a runtime string or object. -pub(super) fn lower_instanceof_dynamic(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_instanceof_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; let target = expect_operand(inst, 1)?; + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_object_is_a_dynamic(ctx, inst, value, target, false); + } let value_ty = ctx.value_php_type(value)?; let target_ty = ctx.value_php_type(target)?; let target_false = ctx.next_label("instanceof_dynamic_target_false"); @@ -3644,20 +4154,25 @@ fn emit_object_allocation( let payload_size = dynamic_properties_offset + dynamic_properties_bytes; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov x0, #{}", payload_size)); // request object payload storage for the class id and property slots + ctx.emitter + .instruction(&format!("mov x0, #{}", payload_size)); // request object payload storage for the class id and property slots abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the object payload - ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the compile-time class id - ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the object payload + ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the compile-time class id + ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rax, {}", payload_size)); // request object payload storage for the class id and property slots + ctx.emitter + .instruction(&format!("mov rax, {}", payload_size)); // request object payload storage for the class id and property slots abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the object payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the compile-time class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the object payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the compile-time class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero } } let object_reg = abi::int_result_reg(ctx.emitter); @@ -3668,7 +4183,11 @@ fn emit_object_allocation( } if !uninitialized_marker_offsets.is_empty() { let marker_reg = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_int_immediate(ctx.emitter, marker_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + marker_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); for offset in uninitialized_marker_offsets { abi::emit_store_to_address(ctx.emitter, marker_reg, object_reg, *offset); } @@ -3708,26 +4227,154 @@ fn dynamic_property_hash_offset(property_count: usize) -> usize { 8 + property_count * 16 } -/// Allocates the per-object dynamic-property hash and stores it in the object payload. -fn emit_dynamic_property_hash_init( +/// Returns property slot offsets whose copied low word must be retained for the cloned owner. +fn cloned_property_retain_offsets(class_info: &ClassInfo) -> Vec { + class_info + .properties + .iter() + .enumerate() + .filter_map(|(index, (property, php_type))| { + if class_info.property_slot_is_reference(index, property) { + return None; + } + property_clone_needs_retain(php_type).then_some(8 + index * 16) + }) + .collect() +} + +/// Returns true when a property slot's low word owns heap storage after a shallow copy. +fn property_clone_needs_retain(php_type: &PhpType) -> bool { + let php_type = php_type.codegen_repr(); + matches!(php_type, PhpType::Str) || php_type.is_refcounted() +} + +/// Copies declared 16-byte property slots and retains heap-backed child payloads. +fn emit_clone_declared_property_slots( ctx: &mut FunctionContext<'_>, - object_reg: &str, + source_reg: &str, + dest_reg: &str, + property_count: usize, + retained_offsets: &[usize], +) { + for index in 0..property_count { + let offset = 8 + index * 16; + emit_copy_property_slot(ctx, source_reg, dest_reg, offset); + if retained_offsets.contains(&offset) { + emit_retain_cloned_property_pointer(ctx, source_reg, dest_reg, offset); + } + } +} + +/// Copies one 16-byte declared-property slot from the source object to the clone. +fn emit_copy_property_slot( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let low_reg = abi::int_result_reg(ctx.emitter); + let high_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, low_reg, source_reg, offset); + abi::emit_load_from_address(ctx.emitter, high_reg, source_reg, offset + 8); + abi::emit_store_to_address(ctx.emitter, low_reg, dest_reg, offset); + abi::emit_store_to_address(ctx.emitter, high_reg, dest_reg, offset + 8); +} + +/// Retains the copied low-word pointer for string, array, hash, object, or Mixed slots. +fn emit_retain_cloned_property_pointer( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + abi::emit_load_from_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_call_label(ctx.emitter, "__rt_incref"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); +} + +/// Replaces the constructor-seeded dynamic-property hash with a shallow clone of the source hash. +fn emit_clone_dynamic_property_hash( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, offset: usize, ) { + emit_release_existing_dynamic_property_hash(ctx, source_reg, dest_reg, offset); + let null_label = ctx.next_label("object_clone_dyn_props_null"); + let done_label = ctx.next_label("object_clone_dyn_props_done"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, result_reg, source_reg, offset); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cbz {}, {}", result_reg, null_label)); // missing dynamic-property hash clones as a null hash pointer + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); // check whether the source dynamic-property hash exists + ctx.emitter.instruction(&format!("jz {}", null_label)); // missing dynamic-property hash clones as a null hash pointer + } + } + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + let hash_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if hash_arg != result_reg { + abi::emit_reg_move(ctx.emitter, hash_arg, result_reg); + } + abi::emit_call_label(ctx.emitter, "__rt_hash_clone_shallow"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&null_label); + abi::emit_store_zero_to_address(ctx.emitter, dest_reg, offset); + ctx.emitter.label(&done_label); +} + +/// Releases the empty hash allocated while constructing the clone shell. +fn emit_release_existing_dynamic_property_hash( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + abi::emit_load_from_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_any"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); +} + +/// Allocates the per-object dynamic-property hash and stores it in the object payload. +fn emit_dynamic_property_hash_init(ctx: &mut FunctionContext<'_>, object_reg: &str, offset: usize) { let hash_reg = abi::secondary_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, object_reg); match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_int_immediate(ctx.emitter, "x0", 4); - abi::emit_load_int_immediate(ctx.emitter, "x1", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x1", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); - ctx.emitter.instruction(&format!("mov {}, x0", hash_reg)); // preserve the dynamic-property hash across object restore + ctx.emitter.instruction(&format!("mov {}, x0", hash_reg)); // preserve the dynamic-property hash across object restore } Arch::X86_64 => { abi::emit_load_int_immediate(ctx.emitter, "rdi", 4); - abi::emit_load_int_immediate(ctx.emitter, "rsi", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "rsi", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); - ctx.emitter.instruction(&format!("mov {}, rax", hash_reg)); // preserve the dynamic-property hash across object restore + ctx.emitter.instruction(&format!("mov {}, rax", hash_reg)); // preserve the dynamic-property hash across object restore } } abi::emit_pop_reg(ctx.emitter, object_reg); @@ -3742,7 +4389,11 @@ fn class_interfaces_require_missing_method_symbols( ) -> bool { let emitted_methods = emitted_instance_method_keys(ctx); let mut seen = HashSet::new(); - let mut stack = class_info.interfaces.iter().map(String::as_str).collect::>(); + let mut stack = class_info + .interfaces + .iter() + .map(String::as_str) + .collect::>(); while let Some(interface_name) = stack.pop() { if !seen.insert(interface_name.to_string()) { continue; @@ -3824,8 +4475,7 @@ fn class_method_already_emitted( .name .rsplit_once("::") .is_some_and(|(candidate_class, candidate_method)| { - candidate_class == class_name - && php_symbol_key(candidate_method) == method_key + candidate_class == class_name && php_symbol_key(candidate_method) == method_key }) }) } @@ -3837,18 +4487,13 @@ fn uninitialized_property_marker_offsets(class_info: &ClassInfo) -> Vec { .iter() .enumerate() .filter_map(|(index, (property, _))| { - let starts_uninitialized = class_info.declared_properties.contains(property) + let is_owned_reference = class_info.owned_reference_properties.contains(property) + && class_info.property_slot_is_reference(index, property); + let starts_uninitialized = class_info.property_slot_is_declared(index, property) && class_info.defaults.get(index).is_some_and(|default| default.is_none()) - && !class_info.owned_reference_properties.contains(property); + && !is_owned_reference; if starts_uninitialized { - Some( - class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16) - + 8, - ) + Some(8 + index * 16 + 8) } else { None } @@ -3864,14 +4509,10 @@ fn owned_reference_property_offsets(class_info: &ClassInfo) -> Vec { .iter() .enumerate() .filter_map(|(index, (property, _))| { - if class_info.owned_reference_properties.contains(property) { - Some( - class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16), - ) + if class_info.owned_reference_properties.contains(property) + && class_info.property_slot_is_reference(index, property) + { + Some(8 + index * 16) } else { None } @@ -3928,11 +4569,17 @@ fn dynamic_property_hash_offset_for_class( .class_infos .get(normalized) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return Ok(None); } if class_info.allow_dynamic_properties { - return Ok(Some(dynamic_property_hash_offset(class_info.properties.len()))); + return Ok(Some(dynamic_property_hash_offset( + class_info.properties.len(), + ))); } Ok(None) } @@ -3963,13 +4610,7 @@ fn resolve_property_slot_for_class( .class_infos .get(normalized) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; - let is_reference = class_info.reference_properties.contains(property); - let Some((index, (_, php_type))) = class_info - .properties - .iter() - .enumerate() - .find(|(_, (name, _))| name == property) - else { + let Some((index, (_, php_type))) = class_info.visible_property(property) else { return Err(CodegenIrError::unsupported(format!( "{} for dynamic or missing property {}::${}", inst.op.name(), @@ -3977,20 +4618,17 @@ fn resolve_property_slot_for_class( property ))); }; + let is_reference = class_info.property_slot_is_reference(index, property); let php_type = runtime_property_type_override(ctx, normalized, property) .unwrap_or_else(|| php_type.clone()); ensure_property_type_supported(&php_type, inst)?; - let offset = class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16); + let offset = 8 + index * 16; Ok(PropertySlot { class_name: normalized.to_string(), property: property.to_string(), php_type, offset, - is_declared: class_info.declared_properties.contains(property), + is_declared: class_info.property_slot_is_declared(index, property), is_packed: false, is_reference, }) @@ -4037,7 +4675,9 @@ fn const_string_operand<'a>(ctx: &FunctionContext<'a>, value: ValueId) -> Result return Ok(None); } let Some(Immediate::Data(data)) = instruction.immediate else { - return Err(CodegenIrError::invalid_module("const_str missing data immediate")); + return Err(CodegenIrError::invalid_module( + "const_str missing data immediate", + )); }; ctx.module .data @@ -4080,10 +4720,7 @@ pub(super) fn nullable_object_receiver_class( } /// Returns the unique object class carried by a boxed union, ignoring null and scalar arms. -fn union_object_member_class( - ctx: &FunctionContext<'_>, - object: ValueId, -) -> Result> { +fn union_object_member_class(ctx: &FunctionContext<'_>, object: ValueId) -> Result> { let PhpType::Union(members) = raw_value_php_type(ctx, object)? else { return Ok(None); }; @@ -4120,14 +4757,14 @@ pub(super) fn emit_nullable_receiver_object_payload( abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #8"); // check whether the nullable receiver holds PHP null - ctx.emitter.instruction(&format!("b.eq {}", null_label)); // short-circuit property access for nullsafe null receivers - ctx.emitter.instruction(&format!("mov {}, x1", object_reg)); // promote the unboxed object payload into the property base register + ctx.emitter.instruction("cmp x0, #8"); // check whether the nullable receiver holds PHP null + ctx.emitter.instruction(&format!("b.eq {}", null_label)); // short-circuit property access for nullsafe null receivers + ctx.emitter.instruction(&format!("mov {}, x1", object_reg)); // promote the unboxed object payload into the property base register } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 8"); // check whether the nullable receiver holds PHP null - ctx.emitter.instruction(&format!("je {}", null_label)); // short-circuit property access for nullsafe null receivers - ctx.emitter.instruction(&format!("mov {}, rdi", object_reg)); // promote the unboxed object payload into the property base register + ctx.emitter.instruction("cmp rax, 8"); // check whether the nullable receiver holds PHP null + ctx.emitter.instruction(&format!("je {}", null_label)); // short-circuit property access for nullsafe null receivers + ctx.emitter.instruction(&format!("mov {}, rdi", object_reg)); // promote the unboxed object payload into the property base register } } Ok(()) @@ -4135,7 +4772,11 @@ pub(super) fn emit_nullable_receiver_object_payload( /// Boxes a PHP null sentinel as a runtime Mixed cell. pub(super) fn emit_boxed_null(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), RUNTIME_NULL_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + RUNTIME_NULL_SENTINEL, + ); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void); } @@ -4151,8 +4792,14 @@ fn resolve_packed_field_slot( .module .packed_class_infos .get(normalized) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown packed class {}", normalized)))?; - let Some(field) = class_info.fields.iter().find(|field| field.name == property) else { + .ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown packed class {}", normalized)) + })?; + let Some(field) = class_info + .fields + .iter() + .find(|field| field.name == property) + else { return Err(CodegenIrError::unsupported(format!( "{} for missing packed field {}::${}", inst.op.name(), @@ -4174,15 +4821,16 @@ fn resolve_packed_field_slot( /// Verifies that this slice knows how to represent the property type in an object slot. fn ensure_property_type_supported(php_type: &PhpType, inst: &Instruction) -> Result<()> { - match php_type { + match php_type.codegen_repr() { PhpType::Bool | PhpType::False | PhpType::Int | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Void | PhpType::Never => Ok(()), - ty if is_pointer_sized_property_type(ty) => Ok(()), + ref ty if is_pointer_sized_property_type(ty) => Ok(()), _ => Err(CodegenIrError::unsupported(format!( "{} for property PHP type {:?}", inst.op.name(), @@ -4216,6 +4864,12 @@ fn ensure_property_value_supported( if can_convert_indexed_array_to_mixed_property(value_ty, &slot.php_type) { return Ok(()); } + if can_store_assoc_array_as_mixed_property(value_ty, &slot.php_type) { + return Ok(()); + } + if can_store_value_as_tagged_scalar_property(value_ty, &slot.php_type) { + return Ok(()); + } if can_coerce_tagged_scalar_to_int_property(value_ty, &slot.php_type) { return Ok(()); } @@ -4231,6 +4885,9 @@ fn ensure_property_value_supported( if can_coerce_mixed_to_scalar_property(value_ty, &slot.php_type) { return Ok(()); } + if can_unbox_mixed_to_object_property(value_ty, &slot.php_type) { + return Ok(()); + } Err(CodegenIrError::unsupported(format!( "{} assigning PHP type {:?} to {}::${} with PHP type {:?}", inst.op.name(), @@ -4241,6 +4898,14 @@ fn ensure_property_value_supported( ))) } +/// Returns true when a boxed Mixed value can be unboxed into an object-typed +/// property slot. Untyped parameters widen to the boxed Mixed ABI while the +/// checker still infers the slot as a concrete object type. +fn can_unbox_mixed_to_object_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + && matches!(slot_ty.codegen_repr(), PhpType::Object(_)) +} + /// Returns true when a concrete object value is assignable to an object-typed property. fn can_store_object_for_object_property( ctx: &FunctionContext<'_>, @@ -4280,11 +4945,9 @@ fn object_type_implements_interface( let Some(class_info) = class_info_by_name(ctx, &class_name) else { return false; }; - if class_info - .interfaces - .iter() - .any(|interface_name| interface_extends_interface(ctx, interface_name, target_interface)) - { + if class_info.interfaces.iter().any(|interface_name| { + interface_extends_interface(ctx, interface_name, target_interface) + }) { return true; } current = class_info.parent.clone(); @@ -4311,11 +4974,7 @@ fn interface_extends_interface( } /// Returns true when a class is or extends the target class. -fn class_extends_class( - ctx: &FunctionContext<'_>, - class_name: &str, - target_class: &str, -) -> bool { +fn class_extends_class(ctx: &FunctionContext<'_>, class_name: &str, target_class: &str) -> bool { let mut current = Some(class_name.to_string()); while let Some(name) = current { if same_php_type_name(&name, target_class) { @@ -4327,10 +4986,7 @@ fn class_extends_class( } /// Finds class metadata by PHP-case-insensitive name. -fn class_info_by_name<'a>( - ctx: &'a FunctionContext<'_>, - class_name: &str, -) -> Option<&'a ClassInfo> { +fn class_info_by_name<'a>(ctx: &'a FunctionContext<'_>, class_name: &str) -> Option<&'a ClassInfo> { let wanted = php_symbol_key(class_name.trim_start_matches('\\')); ctx.module .class_infos @@ -4377,6 +5033,24 @@ fn can_coerce_mixed_to_scalar_property(value_ty: &PhpType, slot_ty: &PhpType) -> ) } +/// Returns true when a value can materialize nullable-int tagged-scalar property storage. +fn can_store_value_as_tagged_scalar_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + if slot_ty.codegen_repr() != PhpType::TaggedScalar { + return false; + } + matches!( + value_ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Callable + | PhpType::Void + | PhpType::Never + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + ) +} + /// Returns true when a nullable inline scalar can be narrowed into int property storage. fn can_coerce_tagged_scalar_to_int_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { value_ty.codegen_repr() == PhpType::TaggedScalar && slot_ty.codegen_repr() == PhpType::Int @@ -4413,8 +5087,19 @@ fn can_convert_indexed_array_to_mixed_property(value_ty: &PhpType, slot_ty: &Php else { return false; }; - slot_elem.codegen_repr() == PhpType::Mixed - && value_elem.codegen_repr() != PhpType::Mixed + slot_elem.codegen_repr() == PhpType::Mixed && value_elem.codegen_repr() != PhpType::Mixed +} + +/// Returns true when associative-array storage can satisfy a generic `array` property. +fn can_store_assoc_array_as_mixed_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + let PhpType::AssocArray { .. } = value_ty.codegen_repr() else { + return false; + }; + match slot_ty.codegen_repr() { + PhpType::Array(slot_elem) => slot_elem.codegen_repr() == PhpType::Mixed, + PhpType::AssocArray { value, .. } => value.codegen_repr() == PhpType::Mixed, + _ => false, + } } /// Returns true when a value can initialize a pointer-sized slot as null. @@ -4455,7 +5140,7 @@ fn emit_property_load( if slot.is_reference { return emit_reference_property_load(ctx, slot, base_reg); } - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); if base_reg == ptr_reg { @@ -4474,14 +5159,22 @@ fn emit_property_load( let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); } - ty if is_pointer_sized_property_type(ty) => { + PhpType::TaggedScalar => { let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); + abi::emit_load_from_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + } + ty if is_pointer_sized_property_type(&ty) => { + let int_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); + } + _ => { + return Err(CodegenIrError::unsupported(format!( + "property load for PHP type {:?}", + slot.php_type + ))) } - _ => return Err(CodegenIrError::unsupported(format!( - "property load for PHP type {:?}", - slot.php_type - ))), } Ok(()) } @@ -4504,16 +5197,27 @@ fn emit_reference_property_load( let float_reg = abi::float_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, float_reg, pointer_reg, 0); } + PhpType::TaggedScalar => { + let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, int_reg, pointer_reg, 0); + abi::emit_load_from_address(ctx.emitter, tag_reg, pointer_reg, 8); + } ty if is_pointer_sized_property_type(&ty) - || matches!(ty, PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never) => + || matches!( + ty, + PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never + ) => { let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, pointer_reg, 0); } - ty => return Err(CodegenIrError::unsupported(format!( - "reference property load for PHP type {:?}", - ty - ))), + ty => { + return Err(CodegenIrError::unsupported(format!( + "reference property load for PHP type {:?}", + ty + ))) + } } Ok(()) } @@ -4524,7 +5228,7 @@ fn emit_packed_field_load( slot: &PropertySlot, base_reg: &str, ) -> Result<()> { - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Float => { let float_reg = abi::float_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, float_reg, base_reg, slot.offset); @@ -4540,22 +5244,31 @@ fn emit_packed_field_load( PhpType::Packed(_) => { let int_reg = abi::int_result_reg(ctx.emitter); if slot.offset == 0 { - ctx.emitter.instruction(&format!("mov {}, {}", int_reg, base_reg)); // return the nested packed field address directly + ctx.emitter + .instruction(&format!("mov {}, {}", int_reg, base_reg)); // return the nested packed field address directly } else { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("add {}, {}, #{}", int_reg, base_reg, slot.offset)); // compute the nested packed field address + ctx.emitter.instruction(&format!( + "add {}, {}, #{}", + int_reg, base_reg, slot.offset + )); // compute the nested packed field address } Arch::X86_64 => { - ctx.emitter.instruction(&format!("lea {}, [{} + {}]", int_reg, base_reg, slot.offset)); // compute the nested packed field address + ctx.emitter.instruction(&format!( + "lea {}, [{} + {}]", + int_reg, base_reg, slot.offset + )); // compute the nested packed field address } } } } - _ => return Err(CodegenIrError::unsupported(format!( - "packed field load for PHP type {:?}", - slot.php_type - ))), + _ => { + return Err(CodegenIrError::unsupported(format!( + "packed field load for PHP type {:?}", + slot.php_type + ))) + } } Ok(()) } @@ -4582,7 +5295,7 @@ fn emit_property_store( abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); return Ok(()); } - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_push_reg(ctx.emitter, base_reg); @@ -4614,7 +5327,16 @@ fn emit_property_store( abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); } - ty if is_pointer_sized_property_type(ty) => { + PhpType::TaggedScalar => { + let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, base_reg); + load_property_store_value_to_result(ctx, value, &slot.php_type)?; + abi::emit_pop_reg(ctx.emitter, base_reg); + abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); + abi::emit_store_to_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + } + ty if is_pointer_sized_property_type(&ty) => { let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, base_reg); load_property_store_value_to_result(ctx, value, &slot.php_type)?; @@ -4629,10 +5351,12 @@ fn emit_property_store( abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); } - _ => return Err(CodegenIrError::unsupported(format!( - "property store for PHP type {:?}", - slot.php_type - ))), + _ => { + return Err(CodegenIrError::unsupported(format!( + "property store for PHP type {:?}", + slot.php_type + ))) + } } Ok(()) } @@ -4645,7 +5369,12 @@ fn emit_reference_property_bind( base_reg: &str, ) -> Result<()> { super::materialize_local_ref_arg_address(ctx, value)?; - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, slot.offset); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + slot.offset, + ); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); Ok(()) } @@ -4759,7 +5488,12 @@ fn release_previous_referenced_value( abi::emit_push_result_value(ctx.emitter, &result_ty.codegen_repr()); } abi::emit_push_reg(ctx.emitter, pointer_reg); - abi::emit_load_from_address(ctx.emitter, abi::int_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_load_from_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); match prop_ty { PhpType::Str => abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"), PhpType::Callable => callable_descriptor::emit_release_current_descriptor(ctx.emitter), @@ -4784,17 +5518,46 @@ fn store_current_result_to_reference_cell( abi::emit_store_to_address(ctx.emitter, len_reg, pointer_reg, 8); } PhpType::Float => { - abi::emit_store_to_address(ctx.emitter, abi::float_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_store_to_address( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + pointer_reg, + 0, + ); + } + PhpType::TaggedScalar => { + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); + abi::emit_store_to_address( + ctx.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), + pointer_reg, + 8, + ); } ty if is_pointer_sized_property_type(&ty) - || matches!(ty, PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never) => + || matches!( + ty, + PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never + ) => { - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); + } + ty => { + return Err(CodegenIrError::unsupported(format!( + "reference property store for PHP type {:?}", + ty + ))) } - ty => return Err(CodegenIrError::unsupported(format!( - "reference property store for PHP type {:?}", - ty - ))), } Ok(()) } @@ -4827,7 +5590,12 @@ fn release_previous_property_value( abi::emit_push_result_value(ctx.emitter, &result_ty.codegen_repr()); } abi::emit_push_reg(ctx.emitter, base_reg); - abi::emit_load_from_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, offset); + abi::emit_load_from_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + offset, + ); match prop_ty { PhpType::Str => abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"), PhpType::Callable => callable_descriptor::emit_release_current_descriptor(ctx.emitter), @@ -4888,6 +5656,41 @@ fn load_property_store_value_to_result( abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Array(Box::new(PhpType::Mixed))); return Ok(()); } + if can_store_assoc_array_as_mixed_property(&value_ty, slot_ty) { + let PhpType::AssocArray { + key: source_key, + value: source_value, + } = ctx.load_value_to_result(value)?.codegen_repr() + else { + return Err(CodegenIrError::unsupported(format!( + "property associative-array widening from PHP type {:?}", + value_ty + ))); + }; + if source_value.codegen_repr() != PhpType::Mixed { + emit_loaded_assoc_array_to_mixed(ctx); + } + abi::emit_incref_if_refcounted( + ctx.emitter, + &PhpType::AssocArray { + key: source_key, + value: Box::new(PhpType::Mixed), + }, + ); + return Ok(()); + } + if can_store_value_as_tagged_scalar_property(&value_ty, slot_ty) { + match value_ty.codegen_repr() { + PhpType::Void | PhpType::Never => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + } + _ => { + ctx.load_value_to_result(value)?; + coerce_loaded_value_to_tagged_scalar(ctx, &value_ty)?; + } + } + return Ok(()); + } if can_coerce_tagged_scalar_to_int_property(&value_ty, slot_ty) { ctx.load_value_to_result(value)?; crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); @@ -4900,6 +5703,7 @@ fn load_property_store_value_to_result( PhpType::Int => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"), PhpType::Bool => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"), PhpType::Float => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"), + PhpType::Object(_) => emit_mixed_object_for_property_store(ctx), _ => {} } return Ok(()); @@ -4943,10 +5747,12 @@ fn emit_packed_field_store( abi::emit_pop_reg(ctx.emitter, base_reg); abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); } - _ => return Err(CodegenIrError::unsupported(format!( - "packed field store for PHP type {:?}", - slot.php_type - ))), + _ => { + return Err(CodegenIrError::unsupported(format!( + "packed field store for PHP type {:?}", + slot.php_type + ))) + } } Ok(()) } @@ -4979,21 +5785,56 @@ fn emit_uninitialized_typed_property_guard( let marker_reg = abi::secondary_scratch_reg(ctx.emitter); let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, marker_reg, object_reg, slot.offset + 8); - abi::emit_load_int_immediate(ctx.emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel - ctx.emitter.instruction(&format!("b.ne {}", initialized_label)); // continue the property read once the slot has been initialized + ctx.emitter + .instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter + .instruction(&format!("b.ne {}", initialized_label)); // continue the property read once the slot has been initialized } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel - ctx.emitter.instruction(&format!("jne {}", initialized_label)); // continue the property read once the slot has been initialized + ctx.emitter + .instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter + .instruction(&format!("jne {}", initialized_label)); // continue the property read once the slot has been initialized } } emit_uninitialized_typed_property_fatal(ctx, slot); ctx.emitter.label(&initialized_label); } +/// Compares a typed instance-property marker with the uninitialized sentinel. +fn emit_typed_property_initialized_bool( + ctx: &mut FunctionContext<'_>, + slot: &PropertySlot, + object_reg: &str, +) { + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, marker_reg, object_reg, slot.offset + 8); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter.instruction("setne al"); // materialize true when the instance property is initialized + ctx.emitter.instruction("movzx rax, al"); // widen the initialization flag into the integer result register + } + } +} + /// Emits the runtime throw for an uninitialized typed-property read. /// /// Constructs an `Error` object with the diagnostic message, publishes it to @@ -5110,26 +5951,60 @@ fn emit_normalized_dynamic_instanceof_value( } /// Unboxes a Mixed/Union tested value and leaves only object payloads as matchable. +/// Unboxes a Mixed store value into the object pointer expected by an +/// object-typed property slot. Non-object payloads store the null sentinel +/// (matching the other lossy Mixed property coercions rather than raising a +/// TypeError). The property store retains the object, so the unboxed pointer +/// is increfed here. +fn emit_mixed_object_for_property_store(ctx: &mut FunctionContext<'_>) { + let object_label = ctx.next_label("prop_store_mixed_value_object"); + let done = ctx.next_label("prop_store_mixed_value_done"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed payload is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads store their unboxed pointer + ctx.emitter.instruction("mov x0, #0"); // non-object payloads fall back to the null sentinel + ctx.emitter.instruction(&format!("b {}", done)); // skip pointer promotion for non-object payloads + ctx.emitter.label(&object_label); + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the result register + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed payload is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads store their unboxed pointer + ctx.emitter.instruction("xor eax, eax"); // non-object payloads fall back to the null sentinel + ctx.emitter.instruction(&format!("jmp {}", done)); // skip pointer promotion for non-object payloads + ctx.emitter.label(&object_label); + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the result register + } + } + ctx.emitter.label(&done); + abi::emit_incref_if_refcounted( + ctx.emitter, + &PhpType::Object(String::new()), // property stores retain the transferred object + ); +} + fn emit_mixed_instanceof_value_normalization(ctx: &mut FunctionContext<'_>) { let object_label = ctx.next_label("instanceof_dynamic_value_object"); let done = ctx.next_label("instanceof_dynamic_value_done"); abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the tested mixed payload is an object - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads can be matched after dynamic target resolution - ctx.emitter.instruction("mov x0, #0"); // scalar mixed payloads become null so the matcher returns false - ctx.emitter.instruction(&format!("b {}", done)); // skip object-payload promotion for scalar payloads + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the tested mixed payload is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads can be matched after dynamic target resolution + ctx.emitter.instruction("mov x0, #0"); // scalar mixed payloads become null so the matcher returns false + ctx.emitter.instruction(&format!("b {}", done)); // skip object-payload promotion for scalar payloads ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the tested mixed payload is an object - ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads can be matched after dynamic target resolution - ctx.emitter.instruction("xor eax, eax"); // scalar mixed payloads become null so the matcher returns false - ctx.emitter.instruction(&format!("jmp {}", done)); // skip object-payload promotion for scalar payloads + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the tested mixed payload is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads can be matched after dynamic target resolution + ctx.emitter.instruction("xor eax, eax"); // scalar mixed payloads become null so the matcher returns false + ctx.emitter.instruction(&format!("jmp {}", done)); // skip object-payload promotion for scalar payloads ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register } } ctx.emitter.label(&done); @@ -5166,15 +6041,15 @@ fn emit_lookup_string_target(ctx: &mut FunctionContext<'_>, false_label: &str) { abi::emit_call_label(ctx.emitter, "__rt_instanceof_lookup"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the dynamic string resolve to a known class or interface? - ctx.emitter.instruction(&format!("b.eq {}", false_label)); // unresolved class-string targets make instanceof false - ctx.emitter.instruction("mov x0, x1"); // move the resolved target id into the matcher target-id register - ctx.emitter.instruction("mov x1, x2"); // move the resolved target kind into the matcher target-kind register + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic string resolve to a known class or interface? + ctx.emitter.instruction(&format!("b.eq {}", false_label)); // unresolved class-string targets make instanceof false + ctx.emitter.instruction("mov x0, x1"); // move the resolved target id into the matcher target-id register + ctx.emitter.instruction("mov x1, x2"); // move the resolved target kind into the matcher target-kind register } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the dynamic string resolve to a known class or interface? - ctx.emitter.instruction(&format!("je {}", false_label)); // unresolved class-string targets make instanceof false - ctx.emitter.instruction("mov rax, rdi"); // move the resolved target id into the matcher target-id register + ctx.emitter.instruction("test rax, rax"); // did the dynamic string resolve to a known class or interface? + ctx.emitter.instruction(&format!("je {}", false_label)); // unresolved class-string targets make instanceof false + ctx.emitter.instruction("mov rax, rdi"); // move the resolved target id into the matcher target-id register } } } @@ -5184,19 +6059,19 @@ fn emit_object_target_metadata(ctx: &mut FunctionContext<'_>) { let ok_label = ctx.next_label("instanceof_dynamic_object_target_ok"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cbnz x0, {}", ok_label)); // non-null object targets can provide runtime class metadata + ctx.emitter.instruction(&format!("cbnz x0, {}", ok_label)); // non-null object targets can provide runtime class metadata emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&ok_label); - ctx.emitter.instruction("ldr x0, [x0]"); // load the runtime class id from the target object header - ctx.emitter.instruction("mov x1, #0"); // object targets always resolve to class target kind + ctx.emitter.instruction("ldr x0, [x0]"); // load the runtime class id from the target object header + ctx.emitter.instruction("mov x1, #0"); // object targets always resolve to class target kind } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // null object targets are not valid dynamic instanceof targets - ctx.emitter.instruction(&format!("jne {}", ok_label)); // non-null object targets can provide runtime class metadata + ctx.emitter.instruction("test rax, rax"); // null object targets are not valid dynamic instanceof targets + ctx.emitter.instruction(&format!("jne {}", ok_label)); // non-null object targets can provide runtime class metadata emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&ok_label); - ctx.emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime class id from the target object header - ctx.emitter.instruction("xor edx, edx"); // object targets always resolve to class target kind + ctx.emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime class id from the target object header + ctx.emitter.instruction("xor edx, edx"); // object targets always resolve to class target kind } } } @@ -5209,30 +6084,30 @@ fn emit_mixed_target_metadata(ctx: &mut FunctionContext<'_>, false_label: &str) abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter.instruction(&format!("b.eq {}", string_label)); // resolve boxed string targets through class-string lookup - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // resolve boxed object targets through their runtime class id + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("b.eq {}", string_label)); // resolve boxed string targets through class-string lookup + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // resolve boxed object targets through their runtime class id emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&string_label); emit_lookup_string_target(ctx, false_label); abi::emit_jump(ctx.emitter, &done); ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov x0, x1"); // move the unboxed target object pointer into the result register + ctx.emitter.instruction("mov x0, x1"); // move the unboxed target object pointer into the result register emit_object_target_metadata(ctx); } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter.instruction(&format!("je {}", string_label)); // resolve boxed string targets through class-string lookup - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter.instruction(&format!("je {}", object_label)); // resolve boxed object targets through their runtime class id + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("je {}", string_label)); // resolve boxed string targets through class-string lookup + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // resolve boxed object targets through their runtime class id emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&string_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target string pointer into the lookup input register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target string pointer into the lookup input register emit_lookup_string_target(ctx, false_label); abi::emit_jump(ctx.emitter, &done); ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target object pointer into the result register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target object pointer into the result register emit_object_target_metadata(ctx); } } @@ -5263,12 +6138,7 @@ fn emit_invalid_dynamic_target_fatal(ctx: &mut FunctionContext<'_>) { } /// Emits the metadata matcher call with object-or-mixed input already in argument 0. -fn emit_match_call( - ctx: &mut FunctionContext<'_>, - target_id: u64, - target_kind: i64, - helper: &str, -) { +fn emit_match_call(ctx: &mut FunctionContext<'_>, target_id: u64, target_kind: i64, helper: &str) { abi::emit_load_int_immediate( ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), @@ -5283,10 +6153,7 @@ fn emit_match_call( } /// Classifies a named target as a class `(kind 0)` or interface `(kind 1)`. -fn classify_named_target( - ctx: &FunctionContext<'_>, - class_name: &str, -) -> Option<(u64, i64)> { +fn classify_named_target(ctx: &FunctionContext<'_>, class_name: &str) -> Option<(u64, i64)> { let normalized = class_name.trim_start_matches('\\'); if let Some(class_info) = ctx.module.class_infos.get(normalized) { return Some((class_info.class_id, 0)); @@ -5317,10 +6184,7 @@ fn property_name_immediate<'a>( } /// Resolves an instruction class-name immediate into the module data pool. -fn class_name_immediate<'a>( - ctx: &'a FunctionContext<'_>, - inst: &Instruction, -) -> Result<&'a str> { +fn class_name_immediate<'a>(ctx: &'a FunctionContext<'_>, inst: &Instruction) -> Result<&'a str> { let data = expect_data(inst)?; ctx.module .data diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 080c1ae940..a43ccb36a7 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -6,16 +6,33 @@ //! - `crate::codegen::lower_inst::objects::lower_object_new()`. //! //! Key details: -//! - `ReflectionClass`, `ReflectionMethod`, and `ReflectionProperty` +//! - `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, +//! `ReflectionProperty`, `ReflectionClassConstant`, and `ReflectionEnum*` //! constructors are compile-time metadata lookups that populate private -//! `__name`/`__attrs` slots instead of running their public empty bodies. +//! metadata slots instead of running their public empty bodies. -use crate::codegen::abi; use crate::codegen::platform::Arch; -use crate::codegen::{CodegenIrError, Result}; -use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; -use crate::names::php_symbol_key; -use crate::types::AttrArgEntry; +use crate::codegen::literal_defaults::{ + emit_boxed_bool_literal_to_result, emit_boxed_float_literal_to_result, + emit_boxed_int_literal_to_result, emit_boxed_null_literal_to_result, + emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, + emit_string_literal_default_to_result, +}; +use crate::codegen::{ + abi, emit_array_value_type_stamp, emit_box_current_owned_value_as_mixed, + emit_box_current_value_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, + runtime_value_tag, CodegenIrError, Result, UNINITIALIZED_TYPED_PROPERTY_SENTINEL, +}; +use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; +use crate::names::{ + enum_case_symbol, php_symbol_key, property_hook_get_method, property_hook_set_method, + static_property_symbol, +}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; +use crate::types::{ + is_php_integer_array_key, AttrArgEntry, EnumCaseInfo, EnumCaseValue, FunctionSig, + InterfaceInfo, PhpType, +}; use super::super::super::context::FunctionContext; @@ -24,13 +41,261 @@ struct ReflectionOwnerMetadata { reflected_name: Option, attr_names: Vec, attr_args: Vec>>, + interface_names: Vec, + trait_names: Vec, + trait_aliases: Vec<(String, String)>, + parent_names: Vec, + method_names: Vec, + property_names: Vec, + constant_names: Vec, + constant_members: Vec, + default_property_members: Vec, + static_property_members: Vec, + constant_reflection_members: Vec, + enum_case_members: Vec, + method_members: Vec, + property_members: Vec, + property_hook_members: Vec<(String, ReflectionListedMember)>, + constructor_member: Option, + parent_class_name: Option, + constant_value: Option, + backing_value: Option, + is_enum_case: bool, + parameter_members: Vec, + type_metadata: Option, + property_default_value: Option, + required_parameter_count: i64, + is_deprecated: bool, + is_generator: bool, + prototype_member: Option>, + is_final: bool, + is_abstract: bool, + is_interface: bool, + is_trait: bool, + is_enum: bool, + is_readonly: bool, + is_anonymous: bool, + is_instantiable: bool, + is_cloneable: bool, + is_iterable: bool, + modifiers: i64, + member_flags: ReflectionMemberFlags, +} + +/// Compile-time metadata for one class/interface/trait/enum constant reflector. +struct ReflectionClassConstantMetadata { + declaring_class_name: String, + attr_names: Vec, + attr_args: Vec>>, + value: ReflectionConstantValue, + visibility: Visibility, + is_final: bool, +} + +/// Metadata for one member object returned by `ReflectionClass::getMethods()` or `getProperties()`. +#[derive(Clone)] +struct ReflectionListedMember { + name: String, + declaring_class_name: Option, + attr_names: Vec, + attr_args: Vec>>, + constant_value: Option, + backing_value: Option, + is_enum_case: bool, + flags: ReflectionMemberFlags, + modifiers: i64, + type_metadata: Option, + default_value: Option, + property_hook_members: Vec<(String, ReflectionListedMember)>, + required_parameter_count: i64, + is_deprecated: bool, + is_generator: bool, + prototype_member: Option>, + parameters: Vec, +} + +/// Metadata for one object returned by `ReflectionMethod::getParameters()`. +#[derive(Clone)] +struct ReflectionParameterMember { + name: String, + declaring_class_name: Option, + declaring_function: Option, + attr_names: Vec, + attr_args: Vec>>, + position: i64, + is_optional: bool, + is_variadic: bool, + is_passed_by_reference: bool, + is_promoted: bool, + has_type: bool, + allows_null: bool, + is_array_type: bool, + is_callable_type: bool, + type_metadata: Option, + default_value: Option, + default_value_constant_name: Option, +} + +/// Metadata needed for `ReflectionParameter::getDeclaringFunction()`. +#[derive(Clone)] +enum ReflectionDeclaringFunctionMember { + Function { + name: String, + attr_names: Vec, + attr_args: Vec>>, + required_parameter_count: i64, + type_metadata: Option, + is_deprecated: bool, + is_generator: bool, + }, + Method { + name: String, + declaring_class_name: Option, + attr_names: Vec, + attr_args: Vec>>, + flags: ReflectionMemberFlags, + required_parameter_count: i64, + type_metadata: Option, + is_deprecated: bool, + is_generator: bool, + }, +} + +/// Metadata for one `ReflectionType` object returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +enum ReflectionParameterTypeMetadata { + Named(ReflectionNamedTypeMetadata), + Union(ReflectionUnionTypeMetadata), + Intersection(ReflectionIntersectionTypeMetadata), +} + +/// Metadata for one `ReflectionNamedType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionNamedTypeMetadata { + name: String, + allows_null: bool, + is_builtin: bool, +} + +/// Metadata for one `ReflectionUnionType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionUnionTypeMetadata { + types: Vec, + allows_null: bool, +} + +/// Metadata for one `ReflectionIntersectionType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionIntersectionTypeMetadata { + types: Vec, +} + +/// Compile-time default forms returned by `ReflectionParameter::getDefaultValue()`. +#[derive(Clone)] +enum ReflectionParameterDefaultValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, + Object { + class_name: String, + args: Vec, + }, + Array(Vec), + AssocArray(Vec), +} + +/// Metadata for one key/value pair in an associative Reflection default array. +#[derive(Clone)] +struct ReflectionDefaultAssocEntry { + key: ReflectionDefaultArrayKey, + value: ReflectionParameterDefaultValue, +} + +/// Normalized PHP key forms for associative Reflection default arrays. +#[derive(Clone)] +enum ReflectionDefaultArrayKey { + Int(i64), + Str(String), +} + +/// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. +#[derive(Clone)] +struct ReflectionConstantMember { + name: String, + value: ReflectionConstantValue, +} + +/// Metadata for one property entry returned by `ReflectionClass::getDefaultProperties()`. +#[derive(Clone)] +struct ReflectionDefaultPropertyMember { + name: String, + value: ReflectionParameterDefaultValue, +} + +/// Metadata for one live static-property value exposed by ReflectionClass. +struct ReflectionStaticPropertyMember { + name: String, + declaring_class_name: String, + php_type: PhpType, + is_declared: bool, +} + +/// Compile-time value forms supported by Reflection constant metadata emission. +#[derive(Clone)] +enum ReflectionConstantValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, + EnumCase { + enum_name: String, + case_name: String, + }, +} + +/// Compile-time parameter selector from `ReflectionParameter::__construct()`. +enum ReflectionParameterSelector { + Name(String), + Position(i64), +} + +/// Boolean metadata exposed by ReflectionMethod and ReflectionProperty predicates. +#[derive(Clone, Copy, Default)] +struct ReflectionMemberFlags { + is_static: bool, + is_public: bool, + is_protected: bool, + is_private: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_promoted: bool, + is_virtual: bool, +} + +/// Runtime class candidate used when object reflection must dispatch by object class id. +struct ReflectionRuntimeClassCandidate { + class_name: String, + class_id: u64, } /// Returns true for reflection owner classes that need metadata-aware construction. pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, - "ReflectionClass" | "ReflectionMethod" | "ReflectionProperty" + "ReflectionClass" + | "ReflectionObject" + | "ReflectionFunction" + | "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionParameter" + | "ReflectionClassConstant" + | "ReflectionEnum" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" ) } @@ -40,443 +305,1156 @@ pub(super) fn lower_reflection_owner_new( inst: &Instruction, class_name: &str, ) -> Result<()> { - let metadata = reflection_owner_metadata(ctx, class_name, inst)?; - let (class_id, property_count, uninitialized_marker_offsets) = { - let class_info = ctx - .module - .class_infos - .get(class_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; - ( - class_info.class_id, - class_info.properties.len(), - super::uninitialized_property_marker_offsets(class_info), - ) - }; - super::emit_object_allocation( - ctx, - class_id, - property_count, - false, - &uninitialized_marker_offsets, - &[], - )?; - if let Some(reflected_name) = metadata.reflected_name.as_deref() { - emit_reflection_string_property(ctx, reflected_name, 8, 16); + if let Some(object_operand) = reflection_object_operand(ctx, class_name, inst)? { + emit_reflection_owner_from_runtime_object(ctx, class_name, object_operand)?; + } else { + let metadata = reflection_owner_metadata(ctx, class_name, inst)?; + emit_reflection_owner_object(ctx, class_name, &metadata)?; } - emit_reflection_attrs_property( - ctx, - class_name, - &metadata.attr_names, - &metadata.attr_args, - )?; - let result = inst - .result - .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; - ctx.store_result_value(result) -} - -/// Lowers `new ReflectionFunction("name")` by populating its name and -/// parameter-count slots from the reflected function's signature. The slot -/// layout is `__name` (8/16), `__short` (24/32), `__num_params` (40/48), -/// `__num_required` (56/64). -pub(super) fn lower_reflection_function_new( - ctx: &mut FunctionContext<'_>, - inst: &Instruction, -) -> Result<()> { - let (full_name, short_name, num_params, num_required) = reflection_function_metadata(ctx, inst)?; - let (class_id, property_count, uninitialized_marker_offsets, name_off, short_off, np_off, nr_off) = { - let class_info = ctx - .module - .class_infos - .get("ReflectionFunction") - .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionFunction"))?; - let slot = |name: &str| -> Result { - class_info - .property_offsets - .get(name) - .copied() - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0)) - }; - ( - class_info.class_id, - class_info.properties.len(), - super::uninitialized_property_marker_offsets(class_info), - slot("__name")?, - slot("__short")?, - slot("__num_params")?, - slot("__num_required")?, - ) - }; - super::emit_object_allocation( - ctx, - class_id, - property_count, - false, - &uninitialized_marker_offsets, - &[], - )?; - emit_reflection_string_property(ctx, &full_name, name_off, name_off + 8); - emit_reflection_string_property(ctx, &short_name, short_off, short_off + 8); - emit_reflection_int_property(ctx, num_params, np_off, np_off + 8); - emit_reflection_int_property(ctx, num_required, nr_off, nr_off + 8); - - // Build the `ReflectionParameter[]` array and store it into `__params`. - let params_off = ctx - .module - .class_infos - .get("ReflectionFunction") - .and_then(|ci| ci.property_offsets.get("__params").copied()) - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0))?; - let param_infos = reflection_function_param_infos(ctx, &full_name); - let (rp_class_id, rp_prop_count, rp_markers, rp_name, rp_pos, rp_opt, rp_var, rp_type, rp_has_type) = { - let ci = ctx - .module - .class_infos - .get("ReflectionParameter") - .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionParameter"))?; - let slot = |n: &str| -> Result { - ci.property_offsets - .get(n) - .copied() - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0)) - }; - ( - ci.class_id, - ci.properties.len(), - super::uninitialized_property_marker_offsets(ci), - slot("__name")?, - slot("__position")?, - slot("__optional")?, - slot("__variadic")?, - slot("__type")?, - slot("__has_type")?, - ) - }; - let result_reg = abi::int_result_reg(ctx.emitter); - let object_reg = abi::symbol_scratch_reg(ctx.emitter); - abi::emit_push_reg(ctx.emitter, result_reg); - abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); - abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, params_off); - abi::emit_call_label(ctx.emitter, "__rt_decref_array"); - emit_reflection_parameter_array( - ctx, - ¶m_infos, - rp_class_id, - rp_prop_count, - &rp_markers, - rp_name, - rp_pos, - rp_opt, - rp_var, - rp_type, - rp_has_type, - )?; - abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, params_off); - abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); - abi::emit_store_to_address( - ctx.emitter, - abi::secondary_scratch_reg(ctx.emitter), - object_reg, - params_off + 8, - ); - abi::emit_push_reg(ctx.emitter, object_reg); - abi::emit_pop_reg(ctx.emitter, result_reg); - let result = inst .result .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; ctx.store_result_value(result) } -/// Resolves `ReflectionFunction(name)` to its full name, short name, and -/// parameter counts from the reflected function's lowered signature. -fn reflection_function_metadata( +/// Returns the constructor object operand for ReflectionClass/Object object reflection. +fn reflection_object_operand( ctx: &FunctionContext<'_>, + class_name: &str, inst: &Instruction, -) -> Result<(String, String, i64, i64)> { - let Some(name_operand) = inst.operands.first().copied() else { - return Ok((String::new(), String::new(), 0, 0)); +) -> Result> { + if !matches!(class_name, "ReflectionClass" | "ReflectionObject") { + return Ok(None); + } + let Some(object_operand) = inst.operands.first().copied() else { + return Ok(None); }; - let function_name = const_required_string_operand(ctx, name_operand, "ReflectionFunction")?; - let key = php_symbol_key(function_name.trim_start_matches('\\')); - let signature = ctx - .module - .functions - .iter() - .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) - .and_then(|function| function.signature.as_ref()); - let (num_params, num_required) = signature - .map(|sig| { - let total = sig.params.len() as i64; - let required = sig - .params - .iter() - .zip(sig.defaults.iter().chain(std::iter::repeat(&None))) - .filter(|((name, _), default)| { - default.is_none() && sig.variadic.as_deref() != Some(name.as_str()) - }) - .count() as i64; - (total, required) - }) - .unwrap_or((0, 0)); - let short_name = function_name - .trim_start_matches('\\') - .rsplit('\\') - .next() - .unwrap_or(&function_name) - .to_string(); - Ok((function_name.clone(), short_name, num_params, num_required)) + if matches!(ctx.value_php_type(object_operand)?, PhpType::Object(_)) { + Ok(Some(object_operand)) + } else { + Ok(None) + } } -/// Stores an integer immediate into a Reflection object's property slot. -fn emit_reflection_int_property( +/// Materializes ReflectionClass/Object metadata by dispatching on the object's runtime class id. +fn emit_reflection_owner_from_runtime_object( ctx: &mut FunctionContext<'_>, - value: i64, - low_offset: usize, - high_offset: usize, -) { - let object_reg = abi::int_result_reg(ctx.emitter); - let scratch = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_int_immediate(ctx.emitter, scratch, value); - abi::emit_store_to_address(ctx.emitter, scratch, object_reg, low_offset); - abi::emit_load_int_immediate(ctx.emitter, scratch, 0); - abi::emit_store_to_address(ctx.emitter, scratch, object_reg, high_offset); -} + class_name: &str, + object_operand: ValueId, +) -> Result<()> { + let candidates = reflection_runtime_class_candidates(ctx, object_operand)?; + if candidates.is_empty() { + return Err(CodegenIrError::unsupported(format!( + "{} constructor for object with no known runtime class candidates", + class_name + ))); + } -/// Per-parameter reflection metadata for one function parameter. -struct ReflectionParamInfo { - name: String, - optional: bool, - variadic: bool, - /// `Some((type_name, is_builtin, allows_null))` when the parameter declares a - /// single named type; `None` for an untyped parameter (`getType()` is null). - type_info: Option<(String, bool, bool)>, -} + let fallback_label = ctx.next_label("reflection_object_fallback"); + let done_label = ctx.next_label("reflection_object_done"); + let case_labels = candidates + .iter() + .map(|_| ctx.next_label("reflection_object_case")) + .collect::>(); -/// Maps a declared parameter type to `ReflectionNamedType` metadata -/// `(name, is_builtin, allows_null)`, or `None` for an unsupported/union shape. -fn reflection_named_type_info(ty: &crate::types::PhpType) -> Option<(String, bool, bool)> { - use crate::types::PhpType; - match ty { - PhpType::Int => Some(("int".to_string(), true, false)), - PhpType::Str => Some(("string".to_string(), true, false)), - PhpType::Float => Some(("float".to_string(), true, false)), - PhpType::Bool => Some(("bool".to_string(), true, false)), - PhpType::Array(_) | PhpType::AssocArray { .. } => Some(("array".to_string(), true, false)), - PhpType::Callable => Some(("callable".to_string(), true, false)), - PhpType::Iterable => Some(("iterable".to_string(), true, false)), - // Bare `Mixed` is how an *untyped* parameter is represented in the EIR - // signature (and `declared_params` is unreliable here — it is also set - // for boxed-ABI params). PHP reports untyped parameters as having no - // type, so map `Mixed` to no named type. An explicit `mixed` hint is - // the only case this under-reports, which is an accepted edge case. - PhpType::Object(class) => Some((class.trim_start_matches('\\').to_string(), false, false)), - PhpType::Union(members) => { - let has_null = members.iter().any(|m| matches!(m, PhpType::Void)); - let mut non_null = members.iter().filter(|m| !matches!(m, PhpType::Void)); - let single = non_null.next(); - // Only `T|null` (a single non-null member) maps to a named type. - match (single, non_null.next()) { - (Some(member), None) => reflection_named_type_info(member) - .map(|(name, builtin, _)| (name, builtin, has_null)), - _ => None, - } - } - _ => None, + emit_runtime_object_class_dispatch(ctx, object_operand, &candidates, &case_labels, &fallback_label)?; + + let fallback_metadata = reflection_class_metadata_for_name(ctx, &candidates[0].class_name)?; + emit_reflection_owner_object(ctx, class_name, &fallback_metadata)?; + emit_reflection_dispatch_jump(ctx, &done_label); // skip runtime reflection candidates after fallback allocation + + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + ctx.emitter.label(label); + let metadata = reflection_class_metadata_for_name(ctx, &candidate.class_name)?; + emit_reflection_owner_object(ctx, class_name, &metadata)?; + emit_reflection_dispatch_jump(ctx, &done_label); // finish after materializing the matched runtime class } -} -/// Extracts per-parameter reflection metadata from a function's lowered -/// signature. A parameter is optional once a default or the variadic is seen -/// (matching PHP's `isOptional`). -fn reflection_function_param_infos( - ctx: &FunctionContext<'_>, - function_name: &str, -) -> Vec { - let key = php_symbol_key(function_name.trim_start_matches('\\')); - let Some(signature) = ctx - .module - .functions - .iter() - .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) - .and_then(|function| function.signature.as_ref()) - else { - return Vec::new(); - }; - let mut seen_optional = false; - signature - .params - .iter() - .enumerate() - .map(|(idx, (name, ty))| { - let variadic = signature.variadic.as_deref() == Some(name.as_str()); - let has_default = signature.defaults.get(idx).map_or(false, Option::is_some); - if has_default || variadic { - seen_optional = true; - } - let declared = signature.declared_params.get(idx).copied().unwrap_or(false); - let type_info = if declared { - reflection_named_type_info(ty) - } else { - None - }; - ReflectionParamInfo { - name: name.clone(), - optional: seen_optional, - variadic, - type_info, - } - }) - .collect() + ctx.emitter.label(&done_label); + Ok(()) } -/// Allocates a fresh indexed array sized for `count` object handles (8-byte stride). -fn emit_alloc_object_array(ctx: &mut FunctionContext<'_>, count: usize) { +/// Emits an unconditional jump for the reflection runtime-class dispatch. +fn emit_reflection_dispatch_jump(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", count.max(1) as i64); - abi::emit_load_int_immediate(ctx.emitter, "x1", 8); + ctx.emitter.instruction(&format!("b {}", label)); // continue after the selected reflection object is ready } Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rdi", count.max(1) as i64); - abi::emit_load_int_immediate(ctx.emitter, "rsi", 8); + ctx.emitter.instruction(&format!("jmp {}", label)); // continue after the selected reflection object is ready } } - abi::emit_call_label(ctx.emitter, "__rt_array_new"); } -/// Pops a freshly built object and the result array off the stack and appends -/// the object handle to the array (leaving the array pointer in the result reg). -fn emit_append_object_to_array(ctx: &mut FunctionContext<'_>) { +/// Emits target-specific class-id comparisons for runtime object reflection. +fn emit_runtime_object_class_dispatch( + ctx: &mut FunctionContext<'_>, + object_operand: ValueId, + candidates: &[ReflectionRuntimeClassCandidate], + case_labels: &[String], + fallback_label: &str, +) -> Result<()> { + ctx.load_value_to_result(object_operand)?; match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_pop_reg(ctx.emitter, "x1"); - abi::emit_pop_reg(ctx.emitter, "x0"); + ctx.emitter.instruction(&format!("cbz x0, {}", fallback_label)); // use fallback metadata for null object pointers + ctx.emitter.instruction("ldr x9, [x0]"); // load the object's concrete runtime class id + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + abi::emit_load_int_immediate(ctx.emitter, "x10", candidate.class_id as i64); + ctx.emitter.instruction("cmp x9, x10"); // compare the object class id with this reflection candidate + ctx.emitter.instruction(&format!("b.eq {}", label)); // select metadata for the matched runtime class + } + ctx.emitter.instruction(&format!("b {}", fallback_label)); // fall back when no generated candidate matches } Arch::X86_64 => { - abi::emit_pop_reg(ctx.emitter, "rsi"); - abi::emit_pop_reg(ctx.emitter, "rdi"); + ctx.emitter.instruction("test rax, rax"); // use fallback metadata for null object pointers + ctx.emitter.instruction(&format!("je {}", fallback_label)); // skip class-id loading when the object pointer is null + ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the object's concrete runtime class id + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + abi::emit_load_int_immediate(ctx.emitter, "r10", candidate.class_id as i64); + ctx.emitter.instruction("cmp r11, r10"); // compare the object class id with this reflection candidate + ctx.emitter.instruction(&format!("je {}", label)); // select metadata for the matched runtime class + } + ctx.emitter.instruction(&format!("jmp {}", fallback_label)); // fall back when no generated candidate matches } } - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); + ctx.emitter.label(fallback_label); + Ok(()) } -/// Builds an indexed array of `ReflectionParameter` objects (one per function -/// parameter), leaving the array pointer in the result register. Stack-balanced. -#[allow(clippy::too_many_arguments)] -fn emit_reflection_parameter_array( - ctx: &mut FunctionContext<'_>, - params: &[ReflectionParamInfo], - class_id: u64, - property_count: usize, - markers: &[usize], - name_off: usize, - pos_off: usize, - opt_off: usize, - var_off: usize, - type_off: usize, - has_type_off: usize, -) -> Result<()> { - // ReflectionNamedType layout for building per-parameter type objects. - let named_type = ctx.module.class_infos.get("ReflectionNamedType").map(|ci| { - let off = |n: &str| ci.property_offsets.get(n).copied().unwrap_or(0); - ( - ci.class_id, - ci.properties.len(), - super::uninitialized_property_marker_offsets(ci), - off("__name"), - off("__allows_null"), - off("__builtin"), - ) - }); - emit_alloc_object_array(ctx, params.len()); - crate::codegen::emit_array_value_type_stamp( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - &crate::types::PhpType::Object("ReflectionParameter".to_string()), - ); - for (position, param) in params.iter().enumerate() { - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - super::emit_object_allocation(ctx, class_id, property_count, false, markers, &[])?; - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - emit_reflection_string_property(ctx, ¶m.name, name_off, name_off + 8); - emit_reflection_int_property(ctx, position as i64, pos_off, pos_off + 8); - emit_reflection_int_property(ctx, param.optional as i64, opt_off, opt_off + 8); - emit_reflection_int_property(ctx, param.variadic as i64, var_off, var_off + 8); - if let (Some((type_name, builtin, allows_null)), Some((nt_id, nt_count, nt_markers, nt_name, nt_anull, nt_builtin))) = - (¶m.type_info, &named_type) - { - // Build a ReflectionNamedType (result reg); the parameter object is - // safe on the stack at slot 0 across this balanced construction. - super::emit_object_allocation(ctx, *nt_id, *nt_count, false, nt_markers, &[])?; - emit_reflection_string_property(ctx, type_name, *nt_name, *nt_name + 8); - emit_reflection_int_property(ctx, *builtin as i64, *nt_builtin, *nt_builtin + 8); - emit_reflection_int_property(ctx, *allows_null as i64, *nt_anull, *nt_anull + 8); - // `__type` is a `mixed` property, so its value must be a *boxed* - // Mixed cell (the receiver later dispatches `getType()->...` through - // the Mixed unbox path). Box the freshly built object pointer (still - // in the result reg) into a cell, then store it as a Mixed slot: - // boxed-cell pointer in the low word, 0 in the high word. The slot - // was zero-initialized at allocation, so no decref of an old value - // is required. - crate::codegen::emit_box_current_value_as_mixed( - ctx.emitter, - &crate::types::PhpType::Object("ReflectionNamedType".to_string()), - ); - let cell_reg = abi::int_result_reg(ctx.emitter); - let param_reg = abi::symbol_scratch_reg(ctx.emitter); - let flag_reg = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_temporary_stack_slot(ctx.emitter, param_reg, 0); - abi::emit_store_to_address(ctx.emitter, cell_reg, param_reg, type_off); - abi::emit_store_zero_to_address(ctx.emitter, param_reg, type_off + 8); - abi::emit_load_int_immediate(ctx.emitter, flag_reg, 1); - abi::emit_store_to_address(ctx.emitter, flag_reg, param_reg, has_type_off); - abi::emit_store_zero_to_address(ctx.emitter, param_reg, has_type_off + 8); - } - emit_append_object_to_array(ctx); +/// Returns runtime class candidates compatible with the object's static type metadata. +fn reflection_runtime_class_candidates( + ctx: &FunctionContext<'_>, + object_operand: ValueId, +) -> Result> { + let static_type = reflection_object_static_type_name(ctx, object_operand)?; + let mut candidates = ctx + .module + .class_infos + .iter() + .filter(|(class_name, _)| reflection_class_matches_object_type(ctx, class_name, &static_type)) + .map(|(class_name, class_info)| ReflectionRuntimeClassCandidate { + class_name: class_name.clone(), + class_id: class_info.class_id, + }) + .collect::>(); + candidates.sort_by_key(|candidate| candidate.class_id); + candidates.dedup_by_key(|candidate| candidate.class_id); + Ok(candidates) +} + +/// Resolves the static object type name used to bound runtime ReflectionObject dispatch. +fn reflection_object_static_type_name( + ctx: &FunctionContext<'_>, + object_operand: ValueId, +) -> Result { + match ctx.value_php_type(object_operand)? { + PhpType::Object(class_name) if class_name.is_empty() => reflection_current_method_class(ctx) + .map(str::to_string) + .ok_or_else(|| { + CodegenIrError::unsupported( + "ReflectionObject constructor for object with unknown static class", + ) + }), + PhpType::Object(class_name) => Ok(class_name), + other => Err(CodegenIrError::unsupported(format!( + "ReflectionObject constructor for PHP type {:?}", + other + ))), } - Ok(()) } -/// Resolves Reflection constructor operands to captured class/member metadata. -fn reflection_owner_metadata( +/// Returns the lexical class name encoded in the current EIR method name, if any. +fn reflection_current_method_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .name + .rsplit_once("::") + .map(|(class_name, _)| class_name) +} + +/// Returns true when a runtime candidate class can inhabit the operand's static object type. +fn reflection_class_matches_object_type( ctx: &FunctionContext<'_>, class_name: &str, - inst: &Instruction, -) -> Result { - match class_name { - "ReflectionClass" => reflection_class_metadata(ctx, inst), - "ReflectionMethod" => reflection_method_metadata(ctx, inst), - "ReflectionProperty" => reflection_property_metadata(ctx, inst), - _ => Ok(empty_reflection_metadata()), + static_type: &str, +) -> bool { + if reflection_same_php_type_name(class_name, static_type) { + return true; } + if resolve_reflection_interface(ctx, static_type).is_some() { + return reflection_class_implements_interface(ctx, class_name, static_type); + } + reflection_class_extends_class(ctx, class_name, static_type) } -/// Resolves `ReflectionClass(class)` metadata. -fn reflection_class_metadata( +/// Returns true when two PHP type names compare case-insensitively after namespace trimming. +fn reflection_same_php_type_name(left: &str, right: &str) -> bool { + php_symbol_key(left.trim_start_matches('\\')) == php_symbol_key(right.trim_start_matches('\\')) +} + +/// Returns true when a runtime class candidate is or extends `target_class`. +fn reflection_class_extends_class( ctx: &FunctionContext<'_>, - inst: &Instruction, -) -> Result { - let Some(class_operand) = inst.operands.first().copied() else { - return Ok(empty_reflection_metadata()); - }; - let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionClass")?; - Ok(resolve_reflection_class(ctx, &reflected_class) - .map(|(class_name, info)| ReflectionOwnerMetadata { - reflected_name: Some(class_name.to_string()), - attr_names: info.attribute_names.clone(), - attr_args: info.attribute_args.clone(), - }) - .unwrap_or_else(empty_reflection_metadata)) + class_name: &str, + target_class: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + while let Some(name) = current { + if reflection_same_php_type_name(&name, target_class) { + return true; + } + current = resolve_reflection_class(ctx, &name) + .and_then(|(_, class_info)| class_info.parent.clone()); + } + false } -/// Resolves `ReflectionMethod(class, method)` metadata. -fn reflection_method_metadata( +/// Returns true when a runtime class candidate implements the requested interface. +fn reflection_class_implements_interface( ctx: &FunctionContext<'_>, - inst: &Instruction, -) -> Result { + class_name: &str, + target_interface: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + while let Some(name) = current { + let Some((_, class_info)) = resolve_reflection_class(ctx, &name) else { + return false; + }; + if class_info.interfaces.iter().any(|interface_name| { + reflection_interface_extends_interface(ctx, interface_name, target_interface) + }) { + return true; + } + current = class_info.parent.clone(); + } + false +} + +/// Returns true when an interface is or extends the requested interface target. +fn reflection_interface_extends_interface( + ctx: &FunctionContext<'_>, + interface_name: &str, + target_interface: &str, +) -> bool { + if reflection_same_php_type_name(interface_name, target_interface) { + return true; + } + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return false; + }; + let Some(interface) = ctx.module.interface_infos.get(interface_name) else { + return false; + }; + interface + .parents + .iter() + .any(|parent| reflection_interface_extends_interface(ctx, parent, target_interface)) +} + +/// Allocates and populates one builtin Reflection owner object from metadata. +fn emit_reflection_owner_object( + ctx: &mut FunctionContext<'_>, + class_name: &str, + metadata: &ReflectionOwnerMetadata, +) -> Result<()> { + let is_reflection_class_owner = matches!(class_name, "ReflectionClass" | "ReflectionObject"); + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + if let Some(reflected_name) = metadata.reflected_name.as_deref() { + emit_reflection_owner_string_property_by_name(ctx, class_name, "__name", reflected_name)?; + if is_reflection_class_owner || class_name == "ReflectionEnum" { + emit_reflection_class_name_parts(ctx, class_name, reflected_name)?; + } + if is_reflection_class_owner { + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__interface_names", + &metadata.interface_names, + )?; + emit_reflection_class_array_property_by_name( + ctx, + class_name, + "__interfaces", + &metadata.interface_names, + )?; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__trait_names", + &metadata.trait_names, + )?; + emit_reflection_class_array_property_by_name( + ctx, + class_name, + "__traits", + &metadata.trait_names, + )?; + emit_reflection_string_assoc_property_by_name( + ctx, + class_name, + "__trait_aliases", + &metadata.trait_aliases, + )?; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__parent_names", + &metadata.parent_names, + )?; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__method_names", + &metadata.method_names, + )?; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__property_names", + &metadata.property_names, + )?; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__constant_names", + &metadata.constant_names, + )?; + emit_reflection_constant_array_property_by_name( + ctx, + class_name, + "__constants", + &metadata.constant_members, + )?; + emit_reflection_default_property_array_property_by_name( + ctx, + class_name, + "__default_properties", + &metadata.default_property_members, + )?; + emit_reflection_static_property_array_property_by_name( + ctx, + class_name, + "__static_properties", + &metadata.static_property_members, + )?; + emit_reflection_member_array_property_by_name( + ctx, + class_name, + "__reflection_constants", + "ReflectionClassConstant", + &metadata.constant_reflection_members, + )?; + emit_reflection_member_array_property_by_name( + ctx, + class_name, + "__methods", + "ReflectionMethod", + &metadata.method_members, + )?; + emit_reflection_constructor_property( + ctx, + class_name, + metadata.constructor_member.as_ref(), + )?; + emit_reflection_parent_class_property( + ctx, + class_name, + metadata.parent_class_name.as_deref(), + )?; + emit_reflection_member_array_property_by_name( + ctx, + class_name, + "__properties", + "ReflectionProperty", + &metadata.property_members, + )?; + } else if class_name == "ReflectionFunction" { + let (_, short_name) = reflection_name_parts(reflected_name); + emit_reflection_owner_string_property_by_name(ctx, class_name, "__short_name", short_name)?; + } + if class_name == "ReflectionEnum" { + let case_names = metadata + .enum_case_members + .iter() + .map(|member| member.name.clone()) + .collect::>(); + let case_class = if metadata.type_metadata.is_some() { + "ReflectionEnumBackedCase" + } else { + "ReflectionEnumUnitCase" + }; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__case_names", + &case_names, + )?; + emit_reflection_member_array_property_by_name( + ctx, + "ReflectionEnum", + "__cases", + case_class, + &metadata.enum_case_members, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_backed", + metadata.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property_by_name( + ctx, + class_name, + "__backing_type", + metadata.type_metadata.as_ref(), + )?; + } + if class_name == "ReflectionFunction" { + emit_reflection_function_name_parts(ctx, reflected_name)?; + } + if class_name == "ReflectionMethod" { + emit_reflection_method_name_parts(ctx, reflected_name)?; + } + } + emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; + if is_reflection_class_owner || class_name == "ReflectionEnum" { + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", metadata.is_final)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_abstract", metadata.is_abstract)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_interface", metadata.is_interface)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_trait", metadata.is_trait)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_enum", metadata.is_enum)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_readonly", metadata.is_readonly)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_anonymous", metadata.is_anonymous)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_instantiable", metadata.is_instantiable)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_cloneable", metadata.is_cloneable)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_iterable", metadata.is_iterable)?; + let is_internal = metadata + .reflected_name + .as_deref() + .is_some_and(reflection_class_like_is_internal); + emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", is_internal)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_user_defined", + metadata.reflected_name.is_some() && !is_internal, + )?; + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + } + if matches!( + class_name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + emit_reflection_declaring_class_property( + ctx, + class_name, + metadata.parent_class_name.as_deref(), + )?; + if matches!(class_name, "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase") { + emit_reflection_enum_property(ctx, class_name, metadata.parent_class_name.as_deref())?; + } + } + if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + let is_internal = reflection_function_or_method_is_internal(class_name, &metadata); + emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", is_internal)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_user_defined", + metadata.reflected_name.is_some() && !is_internal, + )?; + emit_reflection_parameter_array_property_by_name( + ctx, + class_name, + "__parameters", + &metadata.parameter_members, + )?; + emit_reflection_owner_int_property( + ctx, + class_name, + "__required_parameter_count", + metadata.required_parameter_count, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_return_type", + metadata.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_deprecated", + metadata.is_deprecated, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_generator", + metadata.is_generator, + )?; + } + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + if let Some(value) = &metadata.constant_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__value")?; + } + } + if class_name == "ReflectionEnumBackedCase" { + if let Some(value) = &metadata.backing_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__backing_value")?; + } + } + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_enum_case", + metadata.is_enum_case, + )?; + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + } + if class_name == "ReflectionMethod" { + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_prototype", + metadata.prototype_member.is_some(), + )?; + emit_reflection_method_prototype_property(ctx, metadata.prototype_member.as_deref())?; + } + if class_name == "ReflectionProperty" { + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; + emit_reflection_owner_type_property_by_name( + ctx, + class_name, + "__settable_type", + metadata.type_metadata.as_ref(), + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_default_value", + metadata.property_default_value.is_some(), + )?; + emit_reflection_owner_default_value_property( + ctx, + class_name, + metadata.property_default_value.as_ref(), + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_hooks", + !metadata.property_hook_members.is_empty(), + )?; + emit_reflection_property_hook_array_property_by_name( + ctx, + class_name, + "__hooks", + &metadata.property_hook_members, + )?; + let property_string = reflection_property_to_string( + metadata.reflected_name.as_deref().unwrap_or(""), + metadata.member_flags, + metadata.type_metadata.as_ref(), + metadata.property_default_value.as_ref(), + ); + emit_reflection_owner_string_property_by_name( + ctx, + class_name, + "__string", + &property_string, + )?; + } + if class_name == "ReflectionParameter" { + if let Some(parameter) = metadata.parameter_members.first() { + emit_reflection_parameter_properties(ctx, parameter)?; + } + } + emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; + Ok(()) +} + +/// Stores an integer immediate into a Reflection object's property slot. +fn emit_reflection_int_property( + ctx: &mut FunctionContext<'_>, + value: i64, + low_offset: usize, + high_offset: usize, +) { + let object_reg = abi::int_result_reg(ctx.emitter); + let scratch = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, scratch, value); + abi::emit_store_to_address(ctx.emitter, scratch, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, scratch, 0); + abi::emit_store_to_address(ctx.emitter, scratch, object_reg, high_offset); +} + +/// Stores namespace-aware name parts for a statically materialized class-like reflector. +fn emit_reflection_class_name_parts( + ctx: &mut FunctionContext<'_>, + class_name: &str, + reflected_name: &str, +) -> Result<()> { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + emit_reflection_owner_string_property_by_name(ctx, class_name, "__short_name", short_name)?; + emit_reflection_owner_string_property_by_name( + ctx, + class_name, + "__namespace_name", + namespace_name, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__in_namespace", + !namespace_name.is_empty(), + )?; + Ok(()) +} + +/// Stores namespace-aware name parts for a statically materialized ReflectionFunction. +fn emit_reflection_function_name_parts( + ctx: &mut FunctionContext<'_>, + reflected_name: &str, +) -> Result<()> { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionFunction", + "__short_name", + short_name, + )?; + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionFunction", + "__namespace_name", + namespace_name, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionFunction", + "__in_namespace", + !namespace_name.is_empty(), + )?; + Ok(()) +} + +/// Stores PHP's method reflection name parts for a statically materialized method. +fn emit_reflection_method_name_parts( + ctx: &mut FunctionContext<'_>, + reflected_name: &str, +) -> Result<()> { + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionMethod", + "__short_name", + reflected_name, + )?; + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionMethod", + "__namespace_name", + "", + )?; + emit_reflection_owner_bool_property(ctx, "ReflectionMethod", "__in_namespace", false)?; + Ok(()) +} + +/// Splits a canonical PHP class-like name into namespace and short-name parts. +fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { + match reflected_name.rfind('\\') { + Some(separator) => ( + &reflected_name[..separator], + &reflected_name[separator + 1..], + ), + None => ("", reflected_name), + } +} + +/// Resolves Reflection constructor operands to captured class/member metadata. +fn reflection_owner_metadata( + ctx: &FunctionContext<'_>, + class_name: &str, + inst: &Instruction, +) -> Result { + match class_name { + "ReflectionClass" => reflection_class_metadata(ctx, inst), + "ReflectionEnum" => reflection_enum_metadata(ctx, inst), + "ReflectionFunction" => reflection_function_metadata(ctx, inst), + "ReflectionMethod" => reflection_method_metadata(ctx, inst), + "ReflectionProperty" => reflection_property_metadata(ctx, inst), + "ReflectionParameter" => reflection_parameter_metadata(ctx, inst), + "ReflectionClassConstant" => reflection_class_constant_metadata(ctx, inst), + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { + reflection_enum_case_metadata(ctx, class_name, inst) + } + _ => Ok(empty_reflection_metadata()), + } +} + +/// Resolves `ReflectionClass(class)` metadata. +fn reflection_class_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(class_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionClass")?; + reflection_class_metadata_for_name(ctx, &reflected_class) +} + +/// Resolves `ReflectionEnum(enum)` metadata for a known enum name. +fn reflection_enum_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(enum_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_enum = const_string_or_class_operand(ctx, enum_operand, "ReflectionEnum")?; + let mut metadata = reflection_class_metadata_for_name(ctx, &reflected_enum)?; + let Some(enum_name) = metadata.reflected_name.as_deref() else { + return Ok(empty_reflection_metadata()); + }; + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Ok(empty_reflection_metadata()); + }; + metadata.type_metadata = enum_info + .backing_type + .as_ref() + .and_then(reflection_named_type_metadata) + .map(ReflectionParameterTypeMetadata::Named); + Ok(metadata) +} + +/// Resolves `ReflectionClass(name)` metadata for a known class-like name. +fn reflection_class_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_class: &str, +) -> Result { + if let Some((class_name, info)) = resolve_reflection_class(ctx, &reflected_class) { + let is_enum = is_reflection_enum(ctx, class_name); + let method_names = reflection_class_method_names(ctx, class_name); + let property_names = reflection_class_property_names(ctx, class_name, info); + let constant_names = reflection_class_constant_names(ctx, class_name, info); + let constant_members = reflection_class_constant_members(ctx, class_name, info)?; + let default_property_members = + reflection_class_default_property_members(info, &property_names); + let static_property_members = reflection_class_static_property_members(class_name, info); + let constant_reflection_members = + reflection_class_constant_reflection_members(ctx, class_name, info)?; + let enum_case_members = if is_enum { + reflection_enum_case_members(ctx, class_name) + } else { + Vec::new() + }; + let method_members = reflection_class_method_members(ctx, class_name, info, &method_names)?; + let property_members = + reflection_class_property_members(ctx, class_name, info, &property_names); + let constructor_member = reflection_constructor_member(&method_members); + let is_instantiable = + reflection_class_is_instantiable(info, is_enum, constructor_member.as_ref()); + let is_cloneable = reflection_class_is_cloneable(class_name, info, is_enum); + let is_iterable = reflection_class_is_iterable(info, is_enum); + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(class_name.to_string()), + attr_names: info.attribute_names.clone(), + attr_args: info.attribute_args.clone(), + interface_names: info.interfaces.clone(), + trait_names: info.used_traits.clone(), + trait_aliases: info.trait_aliases.clone(), + parent_names: reflection_parent_class_names(ctx, info), + method_names, + property_names, + constant_names, + constant_members, + default_property_members, + static_property_members, + constant_reflection_members, + enum_case_members, + method_members, + property_members, + property_hook_members: Vec::new(), + constructor_member, + parent_class_name: reflection_parent_class_name(ctx, info), + constant_value: None, + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: info.is_final, + is_abstract: info.is_abstract, + is_interface: false, + is_trait: false, + is_enum, + is_readonly: info.is_readonly_class && !is_enum, + is_anonymous: is_reflection_anonymous_class_name(class_name), + is_instantiable, + is_cloneable, + is_iterable, + modifiers: reflection_class_modifiers( + info.is_final, + info.is_abstract, + info.is_readonly_class, + is_enum, + ), + member_flags: ReflectionMemberFlags::default(), + }); + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { + let method_names = reflection_interface_method_names(ctx, interface_name); + let property_names = reflection_interface_property_names(ctx, interface_name); + let constant_names = reflection_interface_constant_names(ctx, interface_name); + let constant_members = reflection_interface_constant_members(ctx, interface_name)?; + let constant_reflection_members = + reflection_interface_constant_reflection_members(ctx, interface_name)?; + let method_members = ctx + .module + .interface_infos + .get(interface_name) + .map(|info| { + reflection_interface_method_members(ctx, info, interface_name, &method_names) + }) + .transpose()? + .unwrap_or_else(|| default_method_members(&method_names, true, interface_name)); + let property_members = default_property_members(&property_names, true, interface_name); + let constructor_member = reflection_constructor_member(&method_members); + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(interface_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + interface_names: reflection_interface_parent_names(ctx, interface_name), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names, + property_names, + constant_names, + constant_members, + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members, + enum_case_members: Vec::new(), + method_members, + property_members, + property_hook_members: Vec::new(), + constructor_member, + parent_class_name: None, + constant_value: None, + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: true, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: 0, + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + }); + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { + let trait_names = ctx + .module + .declared_trait_uses + .get(trait_name) + .cloned() + .unwrap_or_default(); + let method_names = reflection_trait_method_names(ctx, trait_name); + let property_names = reflection_trait_property_names(ctx, trait_name); + let constant_names = reflection_trait_constant_names(ctx, trait_name); + let constant_members = reflection_trait_constant_members(ctx, trait_name)?; + let constant_reflection_members = + reflection_trait_constant_reflection_members(ctx, trait_name)?; + let method_members = ctx + .module + .declared_trait_methods + .get(trait_name) + .map(|methods| reflection_trait_method_members(ctx, methods, trait_name, &method_names)) + .transpose()? + .unwrap_or_else(|| default_method_members(&method_names, false, trait_name)); + let property_members = default_property_members(&property_names, false, trait_name); + let constructor_member = reflection_constructor_member(&method_members); + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(trait_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + interface_names: Vec::new(), + trait_names, + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names, + property_names, + constant_names, + constant_members, + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members, + enum_case_members: Vec::new(), + method_members, + property_members, + property_hook_members: Vec::new(), + constructor_member, + parent_class_name: None, + constant_value: None, + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: true, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: 0, + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + }); + } + Ok(empty_reflection_metadata()) +} + +/// Resolves class metadata for nested declaring-class slots without recursive member objects. +fn reflection_shallow_class_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_class: &str, +) -> Result { + let mut metadata = reflection_class_metadata_for_name(ctx, reflected_class)?; + metadata.method_names.clear(); + metadata.property_names.clear(); + metadata.constant_names.clear(); + metadata.constant_members.clear(); + metadata.constant_reflection_members.clear(); + metadata.enum_case_members.clear(); + metadata.method_members.clear(); + metadata.property_members.clear(); + metadata.constructor_member = None; + metadata.parent_class_name = None; + Ok(metadata) +} + +/// Resolves `ReflectionEnum` metadata for nested enum-case slots. +fn reflection_enum_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_enum: &str, +) -> Result { + let mut metadata = reflection_class_metadata_for_name(ctx, reflected_enum)?; + let Some(enum_name) = metadata.reflected_name.as_deref() else { + return Ok(empty_reflection_metadata()); + }; + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Ok(empty_reflection_metadata()); + }; + metadata.type_metadata = enum_info + .backing_type + .as_ref() + .and_then(reflection_named_type_metadata) + .map(ReflectionParameterTypeMetadata::Named); + metadata.method_names.clear(); + metadata.property_names.clear(); + metadata.constant_names.clear(); + metadata.constant_members.clear(); + metadata.constant_reflection_members.clear(); + metadata.enum_case_members.clear(); + metadata.method_members.clear(); + metadata.property_members.clear(); + metadata.constructor_member = None; + metadata.parent_class_name = None; + Ok(metadata) +} + +/// Resolves `ReflectionFunction(function)` metadata. +fn reflection_function_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(function_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let function_name = const_required_string_operand(ctx, function_operand, "ReflectionFunction")?; + let Some(function) = ctx.function_by_name(&function_name) else { + if let Some((builtin_name, signature)) = + reflection_builtin_function_signature(&function_name) + { + return reflection_builtin_function_metadata(ctx, &builtin_name, &signature); + } + return Ok(empty_reflection_metadata()); + }; + let Some(signature) = function.signature.as_ref() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_name = function.name.trim_start_matches('\\').to_string(); + let required_parameter_count = reflection_required_parameter_count(signature); + let type_metadata = reflection_return_type_metadata(signature); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: reflected_name.clone(), + attr_names: function.attribute_names.clone(), + attr_args: function.attribute_args.clone(), + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: signature.deprecation.is_some(), + is_generator: function.flags.is_generator, + }; + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(reflected_name); + metadata.attr_names = function.attribute_names.clone(); + metadata.attr_args = function.attribute_args.clone(); + metadata.parameter_members = reflection_parameter_members_with_declaring_function( + ctx, + signature, + "", + None, + None, + Some(declaring_function), + &[], + )?; + metadata.required_parameter_count = required_parameter_count; + metadata.type_metadata = type_metadata; + metadata.is_deprecated = signature.deprecation.is_some(); + metadata.is_generator = function.flags.is_generator; + Ok(metadata) +} + +/// Builds metadata for a supported builtin `ReflectionFunction`. +fn reflection_builtin_function_metadata( + ctx: &FunctionContext<'_>, + function_name: &str, + signature: &FunctionSig, +) -> Result { + let required_parameter_count = reflection_required_parameter_count(signature); + let type_metadata = reflection_return_type_metadata(signature); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: function_name.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: false, + is_generator: false, + }; + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(function_name.to_string()); + metadata.parameter_members = reflection_parameter_members_with_declaring_function( + ctx, + signature, + "", + None, + None, + Some(declaring_function), + &[], + )?; + metadata.required_parameter_count = required_parameter_count; + metadata.type_metadata = type_metadata; + Ok(metadata) +} + +/// Returns the canonical callable-builtin name and signature for ReflectionFunction. +fn reflection_builtin_function_signature(function_name: &str) -> Option<(String, FunctionSig)> { + let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); + crate::types::first_class_callable_builtin_sig(&builtin_key) + .map(|signature| (builtin_key, signature)) +} + +/// Returns whether a reflected function or method represents compiler builtin metadata. +fn reflection_function_or_method_is_internal( + class_name: &str, + metadata: &ReflectionOwnerMetadata, +) -> bool { + if class_name == "ReflectionFunction" { + return metadata + .reflected_name + .as_deref() + .and_then(reflection_builtin_function_signature) + .is_some(); + } + metadata + .parent_class_name + .as_deref() + .is_some_and(reflection_class_like_is_internal) +} + +/// Resolves `ReflectionMethod(class, method)` metadata. +fn reflection_method_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { let Some(class_operand) = inst.operands.first().copied() else { return Ok(empty_reflection_metadata()); }; @@ -486,15 +1464,83 @@ fn reflection_method_metadata( let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionMethod")?; let method_name = const_required_string_operand(ctx, method_operand, "ReflectionMethod")?; let method_key = php_symbol_key(&method_name); - Ok(resolve_reflection_class(ctx, &reflected_class) - .and_then(|(_, info)| { - Some(ReflectionOwnerMetadata { - reflected_name: None, - attr_names: info.method_attribute_names.get(&method_key)?.clone(), - attr_args: info.method_attribute_args.get(&method_key)?.clone(), - }) - }) - .unwrap_or_else(empty_reflection_metadata)) + if let Some((_, info)) = resolve_reflection_class(ctx, &reflected_class) { + if let Some(member) = + reflection_class_method_member(ctx, &reflected_class, info, &method_key)? + { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { + if let Some(info) = ctx.module.interface_infos.get(interface_name) { + if let Some(member) = + reflection_interface_method_member(ctx, info, interface_name, &method_key)? + { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { + if let Some(methods) = ctx.module.declared_trait_methods.get(trait_name) { + if let Some(member) = + reflection_trait_method_member(ctx, methods, trait_name, &method_key)? + { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + } + Ok(empty_reflection_metadata()) +} + +/// Builds direct ReflectionMethod constructor metadata from one reflected method member. +fn reflection_method_owner_metadata( + method_name: &str, + member: ReflectionListedMember, +) -> ReflectionOwnerMetadata { + ReflectionOwnerMetadata { + reflected_name: Some(method_name.to_string()), + attr_names: member.attr_names, + attr_args: member.attr_args, + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members: Vec::new(), + constructor_member: None, + parent_class_name: member.declaring_class_name, + constant_value: member.constant_value, + backing_value: member.backing_value, + is_enum_case: member.is_enum_case, + parameter_members: member.parameters, + type_metadata: member.type_metadata, + property_default_value: None, + required_parameter_count: member.required_parameter_count, + is_deprecated: member.is_deprecated, + is_generator: member.is_generator, + prototype_member: member.prototype_member, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: reflection_method_modifiers_from_flags(member.flags), + member_flags: member.flags, + } } /// Resolves `ReflectionProperty(class, property)` metadata. @@ -512,173 +1558,5891 @@ fn reflection_property_metadata( let property_name = const_required_string_operand(ctx, property_operand, "ReflectionProperty")?; Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { + let declaring_class_name = + reflection_property_declaring_class_name(info, &property_name); + let type_metadata = reflection_property_type_metadata(info, &property_name); + let member_flags = reflection_property_member_flags(info, &property_name)?; + let property_hook_members = reflection_property_hook_members( + info, + &property_name, + declaring_class_name.as_deref(), + member_flags, + type_metadata.as_ref(), + ); Some(ReflectionOwnerMetadata { - reflected_name: None, - attr_names: info.property_attribute_names.get(&property_name)?.clone(), - attr_args: info.property_attribute_args.get(&property_name)?.clone(), + reflected_name: Some(property_name.clone()), + attr_names: info + .property_attribute_names + .get(&property_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .property_attribute_args + .get(&property_name) + .cloned() + .unwrap_or_default(), + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members, + constructor_member: None, + parent_class_name: declaring_class_name, + constant_value: None, + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata, + property_default_value: reflection_property_default_value(info, &property_name), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: reflection_property_modifiers_for_info(info, &property_name)?, + member_flags, }) }) .unwrap_or_else(empty_reflection_metadata)) } -/// Looks up class metadata by PHP-style case-insensitive name. -fn resolve_reflection_class<'a>( - ctx: &'a FunctionContext<'_>, - class_name: &str, -) -> Option<(&'a str, &'a crate::types::ClassInfo)> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.module - .class_infos - .iter() - .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) - .map(|(name, info)| (name.as_str(), info)) -} - -/// Returns empty Reflection metadata for unsupported dynamic constructor operands. -fn empty_reflection_metadata() -> ReflectionOwnerMetadata { - ReflectionOwnerMetadata { - reflected_name: None, +/// Resolves `ReflectionParameter(target, parameter)` metadata. +fn reflection_parameter_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + if inst.operands.len() == 2 { + return reflection_function_parameter_metadata(ctx, inst); + } + let Some(class_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(method_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(parameter_operand) = inst.operands.get(2).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionParameter")?; + let method_name = const_required_string_operand(ctx, method_operand, "ReflectionParameter")?; + let selector = const_parameter_selector_operand(ctx, parameter_operand)?; + let method_key = php_symbol_key(&method_name); + let method = reflection_method_member_for_class_like(ctx, &reflected_class, &method_key)?; + let Some(parameter) = method + .as_ref() + .and_then(|method| reflection_parameter_member_for_selector(&method.parameters, selector)) + else { + return Ok(empty_reflection_metadata()); + }; + Ok(reflection_parameter_owner_metadata(parameter)) +} + +/// Resolves `ReflectionParameter(function, parameter)` metadata. +fn reflection_function_parameter_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(function_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(parameter_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let function_name = + const_required_string_operand(ctx, function_operand, "ReflectionParameter")?; + let selector = const_parameter_selector_operand(ctx, parameter_operand)?; + let Some(function) = ctx.function_by_name(&function_name) else { + if let Some((builtin_name, signature)) = + reflection_builtin_function_signature(&function_name) + { + let metadata = reflection_builtin_function_metadata(ctx, &builtin_name, &signature)?; + let Some(parameter) = + reflection_parameter_member_for_selector(&metadata.parameter_members, selector) + else { + return Ok(empty_reflection_metadata()); + }; + return Ok(reflection_parameter_owner_metadata(parameter)); + } + return Ok(empty_reflection_metadata()); + }; + let Some(signature) = function.signature.as_ref() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_name = function.name.trim_start_matches('\\').to_string(); + let type_metadata = reflection_return_type_metadata(signature); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: reflected_name, + attr_names: function.attribute_names.clone(), + attr_args: function.attribute_args.clone(), + required_parameter_count: reflection_required_parameter_count(signature), + type_metadata, + is_deprecated: signature.deprecation.is_some(), + is_generator: function.flags.is_generator, + }; + let parameters = reflection_parameter_members_with_declaring_function( + ctx, + signature, + "", + None, + None, + Some(declaring_function), + &[], + )?; + let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { + return Ok(empty_reflection_metadata()); + }; + Ok(reflection_parameter_owner_metadata(parameter)) +} + +/// Builds direct ReflectionParameter constructor metadata from one parameter member. +fn reflection_parameter_owner_metadata( + parameter: ReflectionParameterMember, +) -> ReflectionOwnerMetadata { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(parameter.name.clone()); + metadata.parameter_members.push(parameter); + metadata +} + +/// Resolves a reflected method member on a class, interface, or trait. +fn reflection_method_member_for_class_like( + ctx: &FunctionContext<'_>, + reflected_class: &str, + method_key: &str, +) -> Result> { + if let Some((_, info)) = resolve_reflection_class(ctx, reflected_class) { + return reflection_class_method_member(ctx, reflected_class, info, method_key); + } + if let Some(interface_name) = resolve_reflection_interface(ctx, reflected_class) { + return ctx + .module + .interface_infos + .get(interface_name) + .map(|info| reflection_interface_method_member(ctx, info, interface_name, method_key)) + .transpose() + .map(Option::flatten); + } + resolve_reflection_trait(ctx, reflected_class) + .and_then(|trait_name| { + ctx.module + .declared_trait_methods + .get(trait_name) + .map(|methods| (trait_name, methods)) + }) + .map(|(trait_name, methods)| { + reflection_trait_method_member(ctx, methods, trait_name, method_key) + }) + .transpose() + .map(Option::flatten) +} + +/// Returns the selected parameter member by PHP name or zero-based position. +fn reflection_parameter_member_for_selector( + parameters: &[ReflectionParameterMember], + selector: ReflectionParameterSelector, +) -> Option { + match selector { + ReflectionParameterSelector::Name(name) => parameters + .iter() + .find(|parameter| parameter.name == name) + .cloned(), + ReflectionParameterSelector::Position(position) if position >= 0 => { + parameters.get(position as usize).cloned() + } + ReflectionParameterSelector::Position(_) => None, + } +} + +/// Resolves `ReflectionClassConstant(class, constant)` metadata. +fn reflection_class_constant_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(class_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(constant_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_class = + const_string_or_class_operand(ctx, class_operand, "ReflectionClassConstant")?; + let constant_name = + const_required_string_operand(ctx, constant_operand, "ReflectionClassConstant")?; + if let Some((enum_name, case)) = + resolve_reflection_enum_case(ctx, &reflected_class, &constant_name) + { + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(constant_name.clone()), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members: Vec::new(), + constructor_member: None, + parent_class_name: Some(enum_name.to_string()), + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: constant_name.clone(), + }), + backing_value: None, + is_enum_case: true, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + }); + } + Ok( + reflection_class_constant_lookup(ctx, &reflected_class, &constant_name)? + .map(|metadata| reflection_class_constant_owner_metadata(constant_name, metadata)) + .unwrap_or_else(empty_reflection_metadata), + ) +} + +/// Resolves `ReflectionEnumUnitCase/BackedCase(enum, case)` metadata. +fn reflection_enum_case_metadata( + ctx: &FunctionContext<'_>, + class_name: &str, + inst: &Instruction, +) -> Result { + let Some(enum_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(case_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_enum = const_string_or_class_operand(ctx, enum_operand, class_name)?; + let case_name = const_required_string_operand(ctx, case_operand, class_name)?; + Ok( + resolve_reflection_enum_case(ctx, &reflected_enum, &case_name) + .map(|(enum_name, case)| ReflectionOwnerMetadata { + reflected_name: Some(case_name.clone()), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members: Vec::new(), + constructor_member: None, + parent_class_name: Some(enum_name.to_string()), + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case_name.clone(), + }), + backing_value: reflection_enum_case_backing_value(case), + is_enum_case: true, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + }) + .unwrap_or_else(empty_reflection_metadata), + ) +} + +/// Builds owner metadata for one resolved class/interface/trait/enum constant reflector. +fn reflection_class_constant_owner_metadata( + reflected_name: String, + metadata: ReflectionClassConstantMetadata, +) -> ReflectionOwnerMetadata { + let is_final = metadata.is_final; + let modifiers = reflection_class_constant_modifiers(&metadata.visibility, is_final); + let member_flags = + reflection_member_flags(false, &metadata.visibility, is_final, false, false, false); + ReflectionOwnerMetadata { + reflected_name: Some(reflected_name), + attr_names: metadata.attr_names, + attr_args: metadata.attr_args, + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members: Vec::new(), + constructor_member: None, + parent_class_name: Some(metadata.declaring_class_name), + constant_value: Some(metadata.value), + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers, + member_flags, + } +} + +/// Resolves static metadata for a direct `ReflectionClassConstant` constructor call. +fn reflection_class_constant_lookup( + ctx: &FunctionContext<'_>, + class_name: &str, + constant_name: &str, +) -> Result> { + if let Some((declaring_class_name, info)) = + resolve_reflection_class_constant(ctx, class_name, constant_name) + { + let Some(value_expr) = info.constants.get(constant_name) else { + return Ok(None); + }; + let value = + reflection_constant_value(ctx, declaring_class_name, Some(info), value_expr, 0)?; + return Ok(Some(ReflectionClassConstantMetadata { + declaring_class_name: declaring_class_name.to_string(), + attr_names: info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + value, + visibility: info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public), + is_final: info.final_constants.contains(constant_name), + })); + } + if let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) { + for interface_name in &class_info.interfaces { + if let Some(metadata) = + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name)? + { + return Ok(Some(metadata)); + } + } + } + if let Some(interface_name) = resolve_reflection_interface(ctx, class_name) { + if let Some(metadata) = + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name)? + { + return Ok(Some(metadata)); + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, class_name) { + if let Some(value_expr) = ctx + .module + .declared_trait_constants + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + { + let is_final = ctx + .module + .declared_trait_final_constants + .get(trait_name) + .is_some_and(|constants| constants.contains(constant_name)); + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + return Ok(Some(ReflectionClassConstantMetadata { + declaring_class_name: trait_name.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + value, + visibility: ctx + .module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public), + is_final, + })); + } + } + Ok(None) +} + +/// Resolves interface constant metadata with the original declaring interface preserved. +fn reflection_interface_class_constant_lookup( + ctx: &FunctionContext<'_>, + interface_name: &str, + constant_name: &str, +) -> Result> { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Ok(None); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Ok(None); + }; + let Some(value_expr) = info.constants.get(constant_name) else { + return Ok(None); + }; + let declaring_interface = + interface_constant_declaring_interface(info, interface_name, constant_name); + let is_final = ctx + .module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)); + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; + Ok(Some(ReflectionClassConstantMetadata { + declaring_class_name: declaring_interface.to_string(), attr_names: Vec::new(), attr_args: Vec::new(), + value, + visibility: Visibility::Public, + is_final, + })) +} + +/// Returns the interface that originally declared a visible interface constant. +fn interface_constant_declaring_interface<'a>( + info: &'a InterfaceInfo, + fallback_interface: &'a str, + constant_name: &str, +) -> &'a str { + info.constant_declaring_interfaces + .get(constant_name) + .map(String::as_str) + .unwrap_or(fallback_interface) +} + +/// Looks up class metadata by PHP-style case-insensitive name. +fn resolve_reflection_class<'a>( + ctx: &'a FunctionContext<'_>, + class_name: &str, +) -> Option<(&'a str, &'a crate::types::ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Returns true when a class name uses the parser's anonymous-class synthetic prefix. +fn is_reflection_anonymous_class_name(class_name: &str) -> bool { + class_name + .trim_start_matches('\\') + .starts_with("class@anonymous#") +} + +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_reflection_interface<'a>( + ctx: &'a FunctionContext<'_>, + interface_name: &str, +) -> Option<&'a str> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + ctx.module + .interface_infos + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(String::as_str) +} + +/// Looks up a declared trait by PHP-style case-insensitive name. +fn resolve_reflection_trait<'a>(ctx: &'a FunctionContext<'_>, trait_name: &str) -> Option<&'a str> { + let trait_key = php_symbol_key(trait_name.trim_start_matches('\\')); + ctx.module + .trait_table + .names + .iter() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == trait_key) + .map(String::as_str) +} + +/// Looks up enum metadata by PHP-style case-insensitive name. +fn is_reflection_enum(ctx: &FunctionContext<'_>, enum_name: &str) -> bool { + let enum_key = php_symbol_key(enum_name.trim_start_matches('\\')); + ctx.module + .enum_infos + .keys() + .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) +} + +/// Returns the canonical parent class name for a reflected class, if any. +fn reflection_parent_class_name( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, +) -> Option { + let parent = info.parent.as_ref()?; + resolve_reflection_class(ctx, parent) + .map(|(parent_name, _)| parent_name.to_string()) + .or_else(|| Some(parent.trim_start_matches('\\').to_string())) +} + +/// Returns direct and inherited parent class names for `ReflectionClass::isSubclassOf()`. +fn reflection_parent_class_names( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, +) -> Vec { + let mut names: Vec = Vec::new(); + let mut current = reflection_parent_class_name(ctx, info); + while let Some(parent_name) = current { + if names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + break; + } + current = resolve_reflection_class(ctx, &parent_name) + .and_then(|(_, parent_info)| reflection_parent_class_name(ctx, parent_info)); + names.push(parent_name); + } + names +} + +/// Returns PHP's `ReflectionClass::isInstantiable()` value for static class metadata. +fn reflection_class_is_instantiable( + info: &crate::types::ClassInfo, + is_enum: bool, + constructor_member: Option<&ReflectionListedMember>, +) -> bool { + if info.is_abstract || is_enum { + return false; + } + constructor_member + .map(|member| member.flags.is_public) + .unwrap_or(true) +} + +/// Returns PHP/elephc cloneability for a reflected class. +fn reflection_class_is_cloneable( + class_name: &str, + info: &crate::types::ClassInfo, + is_enum: bool, +) -> bool { + if info.is_abstract || is_enum || reflection_class_has_runtime_managed_storage(class_name) { + return false; + } + let clone_key = php_symbol_key("__clone"); + info.method_visibilities + .get(&clone_key) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for static class metadata. +fn reflection_class_is_iterable(info: &crate::types::ClassInfo, is_enum: bool) -> bool { + if info.is_abstract || is_enum { + return false; + } + info.interfaces + .iter() + .any(|name| name == "Iterator" || name == "IteratorAggregate") +} + +/// Returns whether a builtin's object layout is outside ordinary declared slots. +fn reflection_class_has_runtime_managed_storage(class_name: &str) -> bool { + let key = php_symbol_key(class_name); + matches!( + key.as_str(), + "throwable" + | "error" + | "exception" + | "valueerror" + | "runtimeexception" + | "reflectionexception" + | "jsonexception" + | "fiber" + | "fibererror" + | "generator" + | "reflectionattribute" + | "reflectionclass" + | "reflectionfunction" + | "reflectionmethod" + | "reflectionproperty" + | "reflectionparameter" + | "reflectionnamedtype" + | "reflectionuniontype" + | "reflectionintersectiontype" + | "reflectionclassconstant" + | "reflectionenumunitcase" + | "reflectionenumbackedcase" + | "splfixedarray" + | "spldoublylinkedlist" + | "splstack" + | "splqueue" + | "iteratoriterator" + | "filteriterator" + | "callbackfilteriterator" + | "recursivefilteriterator" + | "recursivecallbackfilteriterator" + | "recursiveiteratoriterator" + ) +} + +/// Returns whether the reflected class-like name belongs to compiler-injected PHP metadata. +fn reflection_class_like_is_internal(class_name: &str) -> bool { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + matches!( + key.as_str(), + "__elephcappenditeratorarrayiterator" + | "appenditerator" + | "arrayaccess" + | "arrayiterator" + | "arrayobject" + | "badfunctioncallexception" + | "badmethodcallexception" + | "cachingiterator" + | "callbackfilteriterator" + | "countable" + | "directoryiterator" + | "domainexception" + | "emptyiterator" + | "error" + | "exception" + | "fiber" + | "fibererror" + | "filteriterator" + | "filesystemiterator" + | "generator" + | "globiterator" + | "infiniteiterator" + | "internaliterator" + | "invalidargumentexception" + | "iterator" + | "iteratoraggregate" + | "iteratoriterator" + | "jsonexception" + | "jsonserializable" + | "lengthexception" + | "limititerator" + | "logicexception" + | "multipleiterator" + | "norewinditerator" + | "outeriterator" + | "outofboundsexception" + | "outofrangeexception" + | "overflowexception" + | "parentiterator" + | "phar" + | "phardata" + | "pharfileinfo" + | "php_user_filter" + | "rangeexception" + | "recursivearrayiterator" + | "recursivecachingiterator" + | "recursivecallbackfilteriterator" + | "recursivedirectoryiterator" + | "recursivefilteriterator" + | "recursiveiterator" + | "recursiveiteratoriterator" + | "recursiveregexiterator" + | "reflectionattribute" + | "reflectionclass" + | "reflectionclassconstant" + | "reflectionenumbackedcase" + | "reflectionenumunitcase" + | "reflectionexception" + | "reflectionfunction" + | "reflectionintersectiontype" + | "reflectionmethod" + | "reflectionnamedtype" + | "reflectionparameter" + | "reflectionproperty" + | "reflectionuniontype" + | "regexiterator" + | "runtimeexception" + | "seekableiterator" + | "sortdirection" + | "spldoublylinkedlist" + | "splfixedarray" + | "splfileinfo" + | "splfileobject" + | "splheap" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splobserver" + | "splpriorityqueue" + | "splqueue" + | "splstack" + | "splsubject" + | "spltempfileobject" + | "stdclass" + | "stringable" + | "throwable" + | "traversable" + | "typeerror" + | "underflowexception" + | "unexpectedvalueexception" + | "valueerror" + ) +} + +/// Collects direct and inherited parent interfaces for a reflected interface. +fn reflection_interface_parent_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let mut names = Vec::new(); + collect_reflection_interface_parent_names(ctx, interface_name, &mut names); + names +} + +/// Recursively collects interface parents without duplicating case-insensitive names. +fn collect_reflection_interface_parent_names( + ctx: &FunctionContext<'_>, + interface_name: &str, + names: &mut Vec, +) { + let Some(interface) = ctx.module.interface_infos.get(interface_name) else { + return; + }; + for parent in &interface.parents { + let parent_name = resolve_reflection_interface(ctx, parent) + .map(str::to_string) + .unwrap_or_else(|| parent.clone()); + if !names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + names.push(parent_name.clone()); + collect_reflection_interface_parent_names(ctx, &parent_name, names); + } + } +} + +/// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. +fn reflection_class_method_names(ctx: &FunctionContext<'_>, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, info)) = resolve_reflection_class(ctx, ¤t_name) else { + break; + }; + push_unique_method_names(info.methods.keys(), &mut names, &mut seen); + push_unique_method_names(info.static_methods.keys(), &mut names, &mut seen); + current = info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + names +} + +/// Returns PHP case-sensitive property names visible to `ReflectionClass::hasProperty()`. +fn reflection_class_property_names( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, +) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if is_reflection_enum(ctx, class_name) { + push_unique_property_name("name", &mut names, &mut seen); + } + for (name, _) in &info.properties { + if reflection_property_visible_from_class(info, class_name, name, false) { + push_unique_property_name(name, &mut names, &mut seen); + } + } + for (name, _) in &info.static_properties { + if reflection_property_visible_from_class(info, class_name, name, true) { + push_unique_property_name(name, &mut names, &mut seen); + } + } + names +} + +/// Returns PHP case-sensitive class constant names visible to `ReflectionClass::hasConstant()`. +fn reflection_class_constant_names( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_name(&case.name, &mut names, &mut seen); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for constant in current_info.constants.keys() { + push_unique_constant_name(constant, &mut names, &mut seen); + } + for interface_name in ¤t_info.interfaces { + for constant in reflection_interface_constant_names(ctx, interface_name) { + push_unique_constant_name(&constant, &mut names, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + names +} + +/// Returns materializable class constant values for `ReflectionClass::getConstants()`. +fn reflection_class_constant_members( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_member( + &case.name, + ReflectionConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: case.name.clone(), + }, + &mut members, + &mut seen, + ); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for (constant_name, value_expr) in ¤t_info.constants { + if seen.contains(constant_name) { + continue; + } + let value = + reflection_constant_value(ctx, resolved_name, Some(current_info), value_expr, 0)?; + push_unique_constant_member(constant_name, value, &mut members, &mut seen); + } + for interface_name in ¤t_info.interfaces { + for member in reflection_interface_constant_members(ctx, interface_name)? { + push_unique_constant_member(&member.name, member.value, &mut members, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + Ok(members) +} + +/// Returns materializable property defaults for `ReflectionClass::getDefaultProperties()`. +fn reflection_class_default_property_members( + info: &crate::types::ClassInfo, + property_names: &[String], +) -> Vec { + property_names + .iter() + .filter_map(|property_name| { + reflection_property_default_value(info, property_name).map(|value| { + ReflectionDefaultPropertyMember { + name: property_name.clone(), + value, + } + }) + }) + .collect() +} + +/// Returns static-property storage slots for `ReflectionClass::getStaticProperties()`. +fn reflection_class_static_property_members( + class_name: &str, + info: &crate::types::ClassInfo, +) -> Vec { + info.static_properties + .iter() + .map(|(property_name, php_type)| { + let declaring_class_name = info + .static_property_declaring_classes + .get(property_name) + .cloned() + .unwrap_or_else(|| class_name.to_string()); + ReflectionStaticPropertyMember { + name: property_name.clone(), + declaring_class_name, + php_type: php_type.clone(), + is_declared: info.declared_static_properties.contains(property_name), + } + }) + .collect() +} + +/// Returns materializable interface constant values for ReflectionClass metadata. +fn reflection_interface_constant_members( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + collect_interface_constant_members(ctx, interface_name, &mut members, &mut seen)?; + Ok(members) +} + +/// Appends flattened interface constants while preserving their declaring interface. +fn collect_interface_constant_members( + ctx: &FunctionContext<'_>, + interface_name: &str, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) -> Result<()> { + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + return Ok(()); + }; + for (constant_name, value_expr) in &interface_info.constants { + if seen.contains(constant_name) { + continue; + } + let declaring_interface = + interface_constant_declaring_interface(interface_info, interface_name, constant_name); + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; + push_unique_constant_member(constant_name, value, members, seen); + } + Ok(()) +} + +/// Returns materializable direct trait constant values for ReflectionClass metadata. +fn reflection_trait_constant_members( + ctx: &FunctionContext<'_>, + trait_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(constants) = ctx.module.declared_trait_constants.get(trait_name) { + for (constant_name, value_expr) in constants { + if seen.contains(constant_name) { + continue; + } + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + push_unique_constant_member(constant_name, value, &mut members, &mut seen); + } + } + Ok(members) +} + +/// Returns materializable constant-reflector objects for `ReflectionClass::getReflectionConstants()`. +fn reflection_class_constant_reflection_members( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_reflection_member( + &case.name, + class_name, + case.attribute_names.clone(), + case.attribute_args.clone(), + ReflectionConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: case.name.clone(), + }, + Visibility::Public, + false, + true, + &mut members, + &mut seen, + ); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for (constant_name, value_expr) in ¤t_info.constants { + if seen.contains(constant_name) { + continue; + } + let value = + reflection_constant_value(ctx, resolved_name, Some(current_info), value_expr, 0)?; + push_unique_constant_reflection_member( + constant_name, + resolved_name, + current_info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + current_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + value, + current_info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public), + current_info.final_constants.contains(constant_name), + false, + &mut members, + &mut seen, + ); + } + for interface_name in ¤t_info.interfaces { + for member in reflection_interface_constant_reflection_members(ctx, interface_name)? { + push_unique_listed_constant_member(member, &mut members, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + Ok(members) +} + +/// Returns enum-case reflector members for `ReflectionEnum::getCases()`. +fn reflection_enum_case_members( + ctx: &FunctionContext<'_>, + enum_name: &str, +) -> Vec { + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Vec::new(); + }; + enum_info + .cases + .iter() + .map(|case| ReflectionListedMember { + name: case.name.clone(), + declaring_class_name: Some(enum_name.to_string()), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case.name.clone(), + }), + backing_value: reflection_enum_case_backing_value(case), + is_enum_case: true, + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + type_metadata: None, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }) + .collect() +} + +/// Returns constant-reflector objects for interface constants. +fn reflection_interface_constant_reflection_members( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + collect_interface_constant_reflection_members(ctx, interface_name, &mut members, &mut seen)?; + Ok(members) +} + +/// Appends flattened interface constant-reflector objects with declaring-interface metadata. +fn collect_interface_constant_reflection_members( + ctx: &FunctionContext<'_>, + interface_name: &str, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) -> Result<()> { + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + return Ok(()); + }; + for (constant_name, value_expr) in &interface_info.constants { + let declaring_interface = + interface_constant_declaring_interface(interface_info, interface_name, constant_name); + let is_final = ctx + .module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)); + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; + push_unique_constant_reflection_member( + constant_name, + declaring_interface, + Vec::new(), + Vec::new(), + value, + Visibility::Public, + is_final, + false, + members, + seen, + ); + } + Ok(()) +} + +/// Returns constant-reflector objects for direct trait constants. +fn reflection_trait_constant_reflection_members( + ctx: &FunctionContext<'_>, + trait_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + let Some(constants) = ctx.module.declared_trait_constants.get(trait_name) else { + return Ok(members); + }; + let final_constants = ctx.module.declared_trait_final_constants.get(trait_name); + for (constant_name, value_expr) in constants { + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + push_unique_constant_reflection_member( + constant_name, + trait_name, + Vec::new(), + Vec::new(), + value, + ctx.module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public), + final_constants.is_some_and(|constants| constants.contains(constant_name)), + false, + &mut members, + &mut seen, + ); + } + Ok(members) +} + +/// Appends one constant-reflector member if a constant with this name was not already visible. +fn push_unique_constant_reflection_member( + name: &str, + declaring_class_name: &str, + attr_names: Vec, + attr_args: Vec>>, + value: ReflectionConstantValue, + visibility: Visibility, + is_final: bool, + is_enum_case: bool, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if !seen.insert(name.to_string()) { + return; + } + members.push(ReflectionListedMember { + name: name.to_string(), + declaring_class_name: Some(declaring_class_name.to_string()), + attr_names, + attr_args, + constant_value: Some(value), + backing_value: None, + is_enum_case, + flags: reflection_member_flags(false, &visibility, is_final, false, false, false), + modifiers: reflection_class_constant_modifiers(&visibility, is_final), + type_metadata: None, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }); +} + +/// Appends a prebuilt constant-reflector member if its name was not already visible. +fn push_unique_listed_constant_member( + member: ReflectionListedMember, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(member.name.clone()) { + members.push(member); + } +} + +/// Returns true when a property should be visible for `ReflectionClass::hasProperty()`. +fn reflection_property_visible_from_class( + info: &crate::types::ClassInfo, + reflected_class: &str, + property_name: &str, + is_static: bool, +) -> bool { + let visibility = if is_static { + info.static_property_visibilities.get(property_name) + } else { + info.property_visibilities.get(property_name) + }; + if visibility != Some(&Visibility::Private) { + return true; + } + let declaring_class = if is_static { + info.static_property_declaring_classes.get(property_name) + } else { + info.property_declaring_classes.get(property_name) + }; + declaring_class + .map(|declaring_class| php_symbol_key(declaring_class) == php_symbol_key(reflected_class)) + .unwrap_or(false) +} + +/// Returns the class that declares one reflected instance or static method. +fn reflection_method_declaring_class_name( + info: &crate::types::ClassInfo, + reflected_class_name: &str, + method_key: &str, +) -> Option { + info.method_declaring_classes + .get(method_key) + .or_else(|| info.static_method_declaring_classes.get(method_key)) + .cloned() + .or_else(|| { + reflection_class_has_method_kind(info, method_key, false) + .then(|| reflected_class_name.to_string()) + }) + .or_else(|| { + reflection_class_has_method_kind(info, method_key, true) + .then(|| reflected_class_name.to_string()) + }) +} + +/// Returns a prototype method for a reflected generated/AOT class method, if PHP exposes one. +fn reflection_class_method_prototype_member( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + method_key: &str, + flags: ReflectionMemberFlags, +) -> Result>> { + if !reflection_method_is_declared_on_class(info, class_name, method_key, flags.is_static) { + return Ok(None); + } + if let Some(member) = + reflection_parent_method_prototype_member(ctx, info, method_key, flags.is_static)? + { + return Ok(Some(Box::new(member))); + } + reflection_interface_method_prototype_member(ctx, info, method_key, flags.is_static) + .map(|member| member.map(Box::new)) +} + +/// Returns whether a visible method entry is declared by the reflected class itself. +fn reflection_method_is_declared_on_class( + info: &crate::types::ClassInfo, + class_name: &str, + method_key: &str, + is_static: bool, +) -> bool { + let declaring_class = if is_static { + info.static_method_declaring_classes.get(method_key) + } else { + info.method_declaring_classes.get(method_key) + }; + declaring_class + .map(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(class_name.trim_start_matches('\\')) + }) + .unwrap_or(false) +} + +/// Finds the nearest parent-class method that is a valid PHP prototype. +fn reflection_parent_method_prototype_member( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> Result> { + let mut current = reflection_parent_class_name(ctx, info); + let mut seen = std::collections::HashSet::new(); + while let Some(parent_name) = current { + if !seen.insert(php_symbol_key(&parent_name)) { + break; + } + let Some((resolved_parent_name, parent_info)) = resolve_reflection_class(ctx, &parent_name) + else { + break; + }; + if reflection_class_has_method_kind(parent_info, method_key, is_static) { + if let Some(member) = + reflection_class_method_member(ctx, resolved_parent_name, parent_info, method_key)? + { + if !member.flags.is_private && member.flags.is_static == is_static { + return Ok(Some(member)); + } + } + } + current = reflection_parent_class_name(ctx, parent_info); + } + Ok(None) +} + +/// Returns whether a class metadata entry has a method with the requested staticness. +fn reflection_class_has_method_kind( + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> bool { + if is_static { + info.static_methods.contains_key(method_key) + } else { + info.methods.contains_key(method_key) + } +} + +/// Finds the first implemented interface method that is a valid PHP prototype. +fn reflection_interface_method_prototype_member( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> Result> { + for interface_name in &info.interfaces { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + continue; + }; + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + continue; + }; + let has_method = if is_static { + interface_info.static_methods.contains_key(method_key) + } else { + interface_info.methods.contains_key(method_key) + }; + if !has_method { + continue; + } + if let Some(member) = + reflection_interface_method_member(ctx, interface_info, interface_name, method_key)? + { + if member.flags.is_static == is_static { + return Ok(Some(member)); + } + } + } + Ok(None) +} + +/// Returns the class that declares one reflected instance or static property. +fn reflection_property_declaring_class_name( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + info.property_declaring_classes + .get(property_name) + .or_else(|| info.static_property_declaring_classes.get(property_name)) + .cloned() +} + +/// Returns ReflectionMethod predicate flags for a method visible on one class. +fn reflection_method_member_flags( + info: &crate::types::ClassInfo, + method_key: &str, +) -> Option { + if info.methods.contains_key(method_key) { + let visibility = info + .method_visibilities + .get(method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags( + false, + visibility, + info.final_methods.contains(method_key), + !info.method_impl_classes.contains_key(method_key), + false, + false, + )); + } + if info.static_methods.contains_key(method_key) { + let visibility = info + .static_method_visibilities + .get(method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags( + true, + visibility, + info.final_static_methods.contains(method_key), + !info.static_method_impl_classes.contains_key(method_key), + false, + false, + )); + } + None +} + +/// Returns ReflectionProperty predicate flags for a property visible on one class. +fn reflection_property_member_flags( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if info + .properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + let mut flags = reflection_member_flags( + false, + visibility, + info.final_properties.contains(property_name), + info.abstract_properties.contains(property_name), + info.readonly_properties.contains(property_name), + info.promoted_properties.contains(property_name), + ); + flags.is_virtual = reflection_property_is_virtual(info, property_name); + return Some(flags); + } + if info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags( + true, + visibility, + info.final_static_properties.contains(property_name), + false, + false, + false, + )); + } + None +} + +/// Builds ReflectionMethod array entries for the methods visible on one class. +fn reflection_class_method_members( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + method_names: &[String], +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = reflection_class_method_member(ctx, class_name, info, method_name)? { + members.push(member); + } + } + Ok(members) +} + +/// Builds one ReflectionMethod array entry from class metadata. +fn reflection_class_method_member( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + method_name: &str, +) -> Result> { + let method_key = php_symbol_key(method_name); + let sig = info + .methods + .get(&method_key) + .or_else(|| info.static_methods.get(&method_key)); + let Some(sig) = sig else { + return Ok(None); + }; + let declaring_class_name = + reflection_method_declaring_class_name(info, class_name, &method_key); + let attr_names = info + .method_attribute_names + .get(&method_key) + .cloned() + .unwrap_or_default(); + let attr_args = info + .method_attribute_args + .get(&method_key) + .cloned() + .unwrap_or_default(); + let Some(flags) = reflection_method_member_flags(info, &method_key) else { + return Ok(None); + }; + let required_parameter_count = reflection_required_parameter_count(sig); + let type_metadata = reflection_return_type_metadata(sig); + let is_generator = reflection_method_is_generator( + ctx, + declaring_class_name.as_deref().unwrap_or(class_name), + &method_key, + ); + let prototype_member = + reflection_class_method_prototype_member(ctx, class_name, info, &method_key, flags)?; + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: declaring_class_name.clone(), + attr_names: attr_names.clone(), + attr_args: attr_args.clone(), + flags, + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: sig.deprecation.is_some(), + is_generator, + }; + let parameters = reflection_parameter_members_with_declaring_class( + ctx, + sig, + class_name, + Some(info), + declaring_class_name.as_deref(), + Some(declaring_function), + &reflection_promoted_constructor_parameter_names(info, &method_key), + )?; + Ok(Some(ReflectionListedMember { + name: method_key.clone(), + declaring_class_name, + attr_names, + attr_args, + constant_value: None, + backing_value: None, + is_enum_case: false, + flags, + modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count, + is_deprecated: sig.deprecation.is_some(), + is_generator, + prototype_member, + parameters, + })) +} + +/// Builds ReflectionMethod array entries for methods declared by an interface. +fn reflection_interface_method_members( + ctx: &FunctionContext<'_>, + info: &InterfaceInfo, + interface_name: &str, + method_names: &[String], +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = + reflection_interface_method_member(ctx, info, interface_name, method_name)? + { + members.push(member); + } + } + Ok(members) +} + +/// Builds one ReflectionMethod array entry from interface metadata. +fn reflection_interface_method_member( + ctx: &FunctionContext<'_>, + info: &InterfaceInfo, + interface_name: &str, + method_name: &str, +) -> Result> { + let method_key = php_symbol_key(method_name); + let Some((sig, is_static)) = info + .methods + .get(&method_key) + .map(|sig| (sig, false)) + .or_else(|| info.static_methods.get(&method_key).map(|sig| (sig, true))) + else { + return Ok(None); + }; + let declaring_class_name = info + .method_declaring_interfaces + .get(&method_key) + .or_else(|| info.static_method_declaring_interfaces.get(&method_key)) + .cloned() + .unwrap_or_else(|| interface_name.to_string()); + let required_parameter_count = reflection_required_parameter_count(sig); + let flags = reflection_member_flags(is_static, &Visibility::Public, false, true, false, false); + let type_metadata = reflection_return_type_metadata(sig); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: Some(declaring_class_name.clone()), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: sig.deprecation.is_some(), + is_generator: false, + }; + let parameters = reflection_parameter_members_with_declaring_class( + ctx, + sig, + declaring_class_name.as_str(), + None, + Some(declaring_class_name.as_str()), + Some(declaring_function), + &[], + )?; + Ok(Some(ReflectionListedMember { + name: method_key, + declaring_class_name: Some(declaring_class_name), + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags, + modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count, + is_deprecated: sig.deprecation.is_some(), + is_generator: false, + prototype_member: None, + parameters, + })) +} + +/// Builds ReflectionMethod array entries for methods declared by a trait. +fn reflection_trait_method_members( + ctx: &FunctionContext<'_>, + methods: &std::collections::HashMap, + trait_name: &str, + method_names: &[String], +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = reflection_trait_method_member(ctx, methods, trait_name, method_name)? + { + members.push(member); + } + } + Ok(members) +} + +/// Builds one ReflectionMethod array entry from retained trait metadata. +fn reflection_trait_method_member( + ctx: &FunctionContext<'_>, + methods: &std::collections::HashMap, + trait_name: &str, + method_name: &str, +) -> Result> { + let method_key = php_symbol_key(method_name); + let Some(info) = methods.get(&method_key) else { + return Ok(None); + }; + let flags = reflection_member_flags( + info.is_static, + &info.visibility, + info.is_final, + info.is_abstract, + false, + false, + ); + let required_parameter_count = reflection_required_parameter_count(&info.signature); + let type_metadata = reflection_return_type_metadata(&info.signature); + let is_generator = reflection_method_is_generator(ctx, trait_name, &method_key); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: Some(trait_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: info.signature.deprecation.is_some(), + is_generator, + }; + let parameters = reflection_parameter_members_with_declaring_class( + ctx, + &info.signature, + trait_name, + None, + Some(trait_name), + Some(declaring_function), + &[], + )?; + Ok(Some(ReflectionListedMember { + name: method_key, + declaring_class_name: Some(trait_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags, + modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count, + is_deprecated: info.signature.deprecation.is_some(), + is_generator, + prototype_member: None, + parameters, + })) +} + +/// Returns whether the lowered method body is a generator function. +fn reflection_method_is_generator( + ctx: &FunctionContext<'_>, + declaring_class_name: &str, + method_name: &str, +) -> bool { + let expected_key = php_symbol_key(&format!( + "{}::{}", + declaring_class_name.trim_start_matches('\\'), + method_name + )); + ctx.module.class_methods.iter().any(|function| { + php_symbol_key(function.name.trim_start_matches('\\')) == expected_key + && function.flags.is_generator + }) +} + +/// Builds ReflectionProperty array entries for the properties visible on one class. +fn reflection_class_property_members( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + property_names: &[String], +) -> Vec { + property_names + .iter() + .filter_map(|property_name| { + reflection_class_property_member(ctx, class_name, info, property_name) + }) + .collect() +} + +/// Builds one ReflectionProperty array entry from class or enum metadata. +fn reflection_class_property_member( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + let flags = reflection_property_member_flags(info, property_name).or_else(|| { + (is_reflection_enum(ctx, class_name) && property_name == "name").then_some( + reflection_member_flags(false, &Visibility::Public, false, false, true, false), + ) + })?; + let type_metadata = reflection_property_type_metadata(info, property_name); + let default_value = reflection_property_default_value(info, property_name); + let declaring_class_name = reflection_property_declaring_class_name(info, property_name) + .or_else(|| { + (is_reflection_enum(ctx, class_name) && property_name == "name") + .then(|| class_name.to_string()) + }); + let property_hook_members = reflection_property_hook_members( + info, + property_name, + declaring_class_name.as_deref(), + flags, + type_metadata.as_ref(), + ); + Some(ReflectionListedMember { + name: property_name.to_string(), + declaring_class_name, + attr_names: info + .property_attribute_names + .get(property_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .property_attribute_args + .get(property_name) + .cloned() + .unwrap_or_default(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags, + modifiers: reflection_property_modifiers_for_info(info, property_name) + .unwrap_or_else(|| reflection_property_modifiers_from_flags(flags)), + type_metadata, + default_value, + property_hook_members, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }) +} + +/// Returns reflection type metadata for one typed property visible on a class. +fn reflection_property_type_metadata( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if info.visible_property_is_declared(property_name) { + let (_, (_, property_type)) = info.visible_property(property_name)?; + return reflection_parameter_type_metadata(None, property_type); + } + if !info.declared_static_properties.contains(property_name) { + return None; + } + let (_, property_type) = info + .static_properties + .iter() + .find(|(name, _)| name == property_name)?; + reflection_parameter_type_metadata(None, property_type) +} + +/// Builds concrete or abstract property-hook ReflectionMethod metadata for one property. +fn reflection_property_hook_members( + info: &crate::types::ClassInfo, + property_name: &str, + declaring_class_name: Option<&str>, + property_flags: ReflectionMemberFlags, + property_type_metadata: Option<&ReflectionParameterTypeMetadata>, +) -> Vec<(String, ReflectionListedMember)> { + let mut members = Vec::new(); + let declaring_class_name = declaring_class_name.map(str::to_string).or_else(|| { + info.abstract_property_hooks + .get(property_name) + .map(|contract| contract.declaring_type.clone()) + }); + let has_concrete_get = info + .methods + .contains_key(&php_symbol_key(&property_hook_get_method(property_name))); + let has_concrete_set = info + .methods + .contains_key(&php_symbol_key(&property_hook_set_method(property_name))); + let contract = info.abstract_property_hooks.get(property_name); + if has_concrete_get || contract.and_then(|contract| contract.get_type.as_ref()).is_some() { + let return_type = contract + .and_then(|contract| contract.get_type.as_ref()) + .and_then(|ty| reflection_parameter_type_metadata(None, ty)) + .or_else(|| property_type_metadata.cloned()); + members.push(( + String::from("get"), + reflection_property_hook_method_member( + property_name, + "get", + declaring_class_name.clone(), + property_flags, + !has_concrete_get, + return_type, + None, + ), + )); + } + if has_concrete_set || contract.and_then(|contract| contract.set_type.as_ref()).is_some() { + let parameter_type = contract + .and_then(|contract| contract.set_type.as_ref()) + .and_then(|ty| reflection_parameter_type_metadata(None, ty)) + .or_else(|| property_type_metadata.cloned()); + members.push(( + String::from("set"), + reflection_property_hook_method_member( + property_name, + "set", + declaring_class_name, + property_flags, + !has_concrete_set, + Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("void", false), + )), + parameter_type, + ), + )); + } + members +} + +/// Builds one ReflectionMethod metadata record for a property hook. +fn reflection_property_hook_method_member( + property_name: &str, + hook_name: &str, + declaring_class_name: Option, + property_flags: ReflectionMemberFlags, + is_abstract: bool, + return_type: Option, + parameter_type: Option, +) -> ReflectionListedMember { + let visibility = reflection_visibility_from_member_flags(property_flags); + let flags = reflection_member_flags(false, &visibility, false, is_abstract, false, false); + let name = format!("${property_name}::{hook_name}"); + let required_parameter_count = i64::from(hook_name == "set"); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: name.clone(), + declaring_class_name: declaring_class_name.clone(), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + type_metadata: return_type.clone(), + is_deprecated: false, + is_generator: false, + }; + let parameters = if hook_name == "set" { + vec![reflection_property_hook_parameter_member( + declaring_class_name.clone(), + declaring_function.clone(), + parameter_type, + )] + } else { + Vec::new() + }; + ReflectionListedMember { + name, + declaring_class_name, + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags, + modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata: return_type, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters, + } +} + +/// Builds the synthetic `value` parameter exposed by set-hook ReflectionMethod objects. +fn reflection_property_hook_parameter_member( + declaring_class_name: Option, + declaring_function: ReflectionDeclaringFunctionMember, + type_metadata: Option, +) -> ReflectionParameterMember { + let has_type = type_metadata.is_some(); + let allows_null = type_metadata.as_ref().is_some_and(reflection_type_allows_null); + let is_array_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + ReflectionParameterMember { + name: String::from("value"), + declaring_class_name, + declaring_function: Some(declaring_function), + attr_names: Vec::new(), + attr_args: Vec::new(), + position: 0, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + is_promoted: false, + has_type, + allows_null, + is_array_type, + is_callable_type, + type_metadata, + default_value: None, + default_value_constant_name: None, + } +} + +/// Returns supported default metadata for one reflected property. +fn reflection_property_default_value( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if let Some((index, (name, _))) = info.visible_property(property_name) { + return reflection_property_slot_default_value( + info.property_slot_is_declared(index, name), + info.defaults.get(index).and_then(Option::as_ref), + ); + } + info.static_properties + .iter() + .position(|(name, _)| name == property_name) + .and_then(|index| { + reflection_property_slot_default_value( + info.declared_static_properties.contains(property_name), + info.static_defaults.get(index).and_then(Option::as_ref), + ) + }) +} + +/// Converts one physical property slot default into PHP Reflection metadata. +fn reflection_property_slot_default_value( + is_declared: bool, + default: Option<&Expr>, +) -> Option { + match default { + Some(default) => reflection_literal_parameter_default_value(default), + None if !is_declared => Some(ReflectionParameterDefaultValue::Null), + None => None, + } +} + +/// Formats retained generated property metadata for `ReflectionProperty::__toString()`. +fn reflection_property_to_string( + property_name: &str, + flags: ReflectionMemberFlags, + type_metadata: Option<&ReflectionParameterTypeMetadata>, + default_value: Option<&ReflectionParameterDefaultValue>, +) -> String { + let mut parts = Vec::new(); + if flags.is_abstract { + parts.push(String::from("abstract")); + } + if flags.is_final { + parts.push(String::from("final")); + } + parts.push(reflection_property_visibility_label(flags).to_string()); + if flags.is_static { + parts.push(String::from("static")); + } + if flags.is_readonly { + parts.push(String::from("readonly")); + } + if let Some(type_name) = type_metadata.map(reflection_type_metadata_to_string) { + parts.push(type_name); + } + parts.push(format!("${property_name}")); + + let default = if flags.is_virtual { + String::new() + } else { + default_value + .and_then(reflection_default_value_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default() + }; + format!("Property [ {}{} ]", parts.join(" "), default) +} + +/// Returns PHP's lowercase visibility label for one reflected property. +fn reflection_property_visibility_label(flags: ReflectionMemberFlags) -> &'static str { + if flags.is_private { + "private" + } else if flags.is_protected { + "protected" + } else { + "public" + } +} + +/// Formats retained ReflectionType metadata for property string output. +fn reflection_type_metadata_to_string(type_metadata: &ReflectionParameterTypeMetadata) -> String { + match type_metadata { + ReflectionParameterTypeMetadata::Named(named) => { + if named.allows_null && named.name != "mixed" { + format!("?{}", named.name) + } else { + named.name.clone() + } + } + ReflectionParameterTypeMetadata::Union(union) => { + let mut names = union + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>(); + if union.allows_null && names.iter().all(|name| name != "null") { + names.push(String::from("null")); + } + names.join("|") + } + ReflectionParameterTypeMetadata::Intersection(intersection) => intersection + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>() + .join("&"), + } +} + +/// Formats retained scalar defaults for property string output. +fn reflection_default_value_to_string( + default: &ReflectionParameterDefaultValue, +) -> Option { + match default { + ReflectionParameterDefaultValue::Int(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Bool(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Float(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Str(value) => Some(format!("'{value}'")), + ReflectionParameterDefaultValue::Null => Some(String::from("NULL")), + ReflectionParameterDefaultValue::Object { .. } + | ReflectionParameterDefaultValue::Array(_) + | ReflectionParameterDefaultValue::AssocArray(_) => None, + } +} + +/// Builds placeholder ReflectionMethod entries for class-like metadata without full method schemas. +fn default_method_members( + method_names: &[String], + is_interface: bool, + declaring_class_name: &str, +) -> Vec { + method_names + .iter() + .map(|name| ReflectionListedMember { + name: name.clone(), + declaring_class_name: Some(declaring_class_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + false, + ), + modifiers: reflection_method_modifiers_from_flags(reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + false, + )), + type_metadata: None, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }) + .collect() +} + +/// Builds placeholder ReflectionProperty entries for class-like metadata without full property schemas. +fn default_property_members( + property_names: &[String], + is_interface: bool, + declaring_class_name: &str, +) -> Vec { + property_names + .iter() + .map(|name| ReflectionListedMember { + name: name.clone(), + declaring_class_name: Some(declaring_class_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + backing_value: None, + is_enum_case: false, + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + false, + ), + modifiers: reflection_property_modifiers( + &Visibility::Public, + false, + false, + is_interface, + false, + is_interface, + None, + ), + type_metadata: None, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }) + .collect() +} + +/// Returns PHP's required parameter count for a reflected native signature. +fn reflection_required_parameter_count(sig: &FunctionSig) -> i64 { + let fixed_count = sig + .variadic + .as_deref() + .and_then(|variadic| { + sig.params + .iter() + .position(|(name, _)| name.as_str() == variadic) + }) + .unwrap_or(sig.params.len()); + (0..fixed_count) + .rfind(|index| !sig.defaults.get(*index).is_some_and(Option::is_some)) + .map_or(0, |index| index as i64 + 1) +} + +/// Returns promoted constructor property names for ReflectionParameter metadata. +fn reflection_promoted_constructor_parameter_names( + info: &crate::types::ClassInfo, + method_key: &str, +) -> Vec { + if method_key.eq_ignore_ascii_case("__construct") { + info.promoted_properties.iter().cloned().collect() + } else { + Vec::new() + } +} + +/// Builds reflected parameter metadata and attaches declaring class metadata when present. +fn reflection_parameter_members_with_declaring_class( + ctx: &FunctionContext<'_>, + sig: &FunctionSig, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + declaring_class_name: Option<&str>, + declaring_function: Option, + promoted_parameter_names: &[String], +) -> Result> { + reflection_parameter_members_with_declaring_function( + ctx, + sig, + current_class, + current_info, + declaring_class_name, + declaring_function, + promoted_parameter_names, + ) +} + +/// Builds reflected parameter metadata with optional declaring owner metadata. +fn reflection_parameter_members_with_declaring_function( + ctx: &FunctionContext<'_>, + sig: &FunctionSig, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + declaring_class_name: Option<&str>, + declaring_function: Option, + promoted_parameter_names: &[String], +) -> Result> { + let mut parameters = Vec::new(); + for (index, (name, ty)) in sig.params.iter().enumerate() { + let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); + // `declared_params` doubles as the runtime invoker's boxed-ABI marker + // (see `eir_runtime_metadata_signature`), which only ever raises the + // flag for Mixed/Union params. Non-Mixed declared params are always + // genuine (source hints, builtin signatures, variadics); a declared + // Mixed param needs the source type expression to distinguish a real + // `mixed` hint from an untyped param widened for the boxed ABI. + let declared = sig.declared_params.get(index).copied().unwrap_or(false); + let has_type = declared + && (!matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + || sig + .param_type_exprs + .get(index) + .and_then(Option::as_ref) + .is_some()); + let type_metadata = reflection_parameter_type_metadata( + sig.param_type_exprs.get(index).and_then(Option::as_ref), + ty, + ) + .filter(|_| has_type); + let default_expr = sig.defaults.get(index).and_then(Option::as_ref); + let default_value = default_expr + .map(|default| { + reflection_parameter_default_value(ctx, current_class, current_info, default) + }) + .transpose()? + .flatten(); + let default_value_constant_name = + default_expr.and_then(reflection_parameter_default_constant_name); + let is_array_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + parameters.push(ReflectionParameterMember { + name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.clone(), + attr_names: sig + .param_attributes + .get(index) + .map(|groups| crate::types::collect_attribute_names(groups)) + .unwrap_or_default(), + attr_args: sig + .param_attributes + .get(index) + .map(|groups| crate::types::collect_attribute_args(groups)) + .unwrap_or_default(), + position: index as i64, + is_optional: is_variadic + || sig + .defaults + .get(index) + .map(|default| default.is_some()) + .unwrap_or(false), + is_variadic, + is_passed_by_reference: sig.ref_params.get(index).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), + has_type, + allows_null: reflection_parameter_allows_null( + has_type, + type_metadata.as_ref(), + default_value.as_ref(), + ), + is_array_type, + is_callable_type, + type_metadata, + default_value, + default_value_constant_name, + }); + } + Ok(parameters) +} + +/// Returns whether retained parameter metadata is one named type with the requested name. +fn reflection_parameter_has_named_type( + type_metadata: Option<&ReflectionParameterTypeMetadata>, + expected_name: &str, +) -> bool { + matches!( + type_metadata, + Some(ReflectionParameterTypeMetadata::Named(named)) + if named.name.eq_ignore_ascii_case(expected_name) + ) +} + +/// Returns PHP's `ReflectionParameter::allowsNull()` value for static metadata. +fn reflection_parameter_allows_null( + has_type: bool, + type_metadata: Option<&ReflectionParameterTypeMetadata>, + default_value: Option<&ReflectionParameterDefaultValue>, +) -> bool { + !has_type + || matches!(default_value, Some(ReflectionParameterDefaultValue::Null)) + || type_metadata.is_some_and(reflection_type_allows_null) +} + +/// Returns whether one retained ReflectionType metadata value accepts null. +fn reflection_type_allows_null(type_metadata: &ReflectionParameterTypeMetadata) -> bool { + match type_metadata { + ReflectionParameterTypeMetadata::Named(named_type) => named_type.allows_null, + ReflectionParameterTypeMetadata::Union(union_type) => union_type.allows_null, + ReflectionParameterTypeMetadata::Intersection(_) => false, + } +} + +/// Converts a supported parameter default expression into Reflection metadata. +fn reflection_parameter_default_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + default: &Expr, +) -> Result> { + if let Some(value) = + reflection_object_parameter_default_value(ctx, current_class, current_info, default)? + { + return Ok(Some(value)); + } + if let Some(value) = reflection_literal_parameter_default_value(default) { + return Ok(Some(value)); + } + match &default.kind { + ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => { + let value = reflection_constant_value(ctx, current_class, current_info, default, 0)?; + Ok(reflection_parameter_default_from_constant_value(value)) + } + _ => Ok(None), + } +} + +/// Converts a top-level object parameter default into Reflection metadata. +fn reflection_object_parameter_default_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + default: &Expr, +) -> Result> { + let ExprKind::NewObject { class_name, args } = &default.kind else { + return Ok(None); + }; + let Some(args) = reflection_object_parameter_default_args( + ctx, + current_class, + current_info, + class_name.as_str(), + args, + )? + else { + return Ok(None); + }; + Ok(Some(ReflectionParameterDefaultValue::Object { + class_name: class_name.as_str().to_string(), + args, + })) +} + +/// Returns constructor args for an object default, including supported omitted defaults. +fn reflection_object_parameter_default_args( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + class_name: &str, + args: &[Expr], +) -> Result>> { + if args.len() > 8 { + return Ok(None); + } + let mut values = Vec::with_capacity(args.len()); + for arg in args { + let Some(value) = + reflection_parameter_default_non_object_value(ctx, current_class, current_info, arg)? + else { + return Ok(None); + }; + values.push(value); + } + let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) else { + return Ok(Some(values)); + }; + let constructor = class_info.methods.get(&php_symbol_key("__construct")); + let Some(constructor) = constructor else { + return if values.is_empty() { + Ok(Some(values)) + } else { + Ok(None) + }; + }; + if constructor.variadic.is_some() + || values.len() > constructor.params.len() + || constructor.params.len() > 8 + { + return Ok(None); + } + for default in constructor.defaults.iter().skip(values.len()) { + let Some(default) = default.as_ref() else { + return Ok(None); + }; + let Some(value) = reflection_parameter_default_non_object_value( + ctx, + class_name, + Some(class_info), + default, + )? + else { + return Ok(None); + }; + values.push(value); + } + if values.len() == constructor.params.len() { + Ok(Some(values)) + } else { + Ok(None) + } +} + +/// Converts a supported non-object parameter default expression into metadata. +fn reflection_parameter_default_non_object_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + default: &Expr, +) -> Result> { + if let Some(value) = reflection_literal_parameter_default_non_object_value(default) { + return Ok(Some(value)); + } + match &default.kind { + ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => { + let value = reflection_constant_value(ctx, current_class, current_info, default, 0)?; + Ok(reflection_parameter_default_from_constant_value(value)) + } + _ => Ok(None), + } +} + +/// Converts constructor arguments for object defaults, rejecting nested objects. +fn reflection_literal_parameter_default_non_object_value( + default: &Expr, +) -> Option { + let value = reflection_literal_parameter_default_value(default)?; + if reflection_default_value_contains_object(&value) { + return None; + } + Some(value) +} + +/// Returns whether a retained default contains an object value. +fn reflection_default_value_contains_object(value: &ReflectionParameterDefaultValue) -> bool { + match value { + ReflectionParameterDefaultValue::Object { .. } => true, + ReflectionParameterDefaultValue::Array(elements) => elements + .iter() + .any(reflection_default_value_contains_object), + ReflectionParameterDefaultValue::AssocArray(entries) => entries + .iter() + .any(|entry| reflection_default_value_contains_object(&entry.value)), + ReflectionParameterDefaultValue::Int(_) + | ReflectionParameterDefaultValue::Bool(_) + | ReflectionParameterDefaultValue::Float(_) + | ReflectionParameterDefaultValue::Str(_) + | ReflectionParameterDefaultValue::Null => false, + } +} + +/// Converts a literal parameter/property default expression into Reflection metadata. +fn reflection_literal_parameter_default_value( + default: &Expr, +) -> Option { + match &default.kind { + ExprKind::IntLiteral(value) => Some(ReflectionParameterDefaultValue::Int(*value)), + ExprKind::BoolLiteral(value) => Some(ReflectionParameterDefaultValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(*value)), + ExprKind::StringLiteral(value) => Some(ReflectionParameterDefaultValue::Str(value.clone())), + ExprKind::Null => Some(ReflectionParameterDefaultValue::Null), + ExprKind::ArrayLiteral(items) => items + .iter() + .map(reflection_literal_parameter_default_value) + .collect::>>() + .map(ReflectionParameterDefaultValue::Array), + ExprKind::ArrayLiteralAssoc(entries) => reflection_assoc_array_default_value(entries), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value + .checked_neg() + .map(ReflectionParameterDefaultValue::Int), + ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(-value)), + _ => None, + }, + _ => None, + } +} + +/// Converts an associative array literal into normalized Reflection default metadata. +fn reflection_assoc_array_default_value( + entries: &[(Expr, Expr)], +) -> Option { + entries + .iter() + .map(|(key, value)| { + let key = reflection_default_array_key(key)?; + let value = reflection_literal_parameter_default_value(value)?; + Some(ReflectionDefaultAssocEntry { key, value }) + }) + .collect::>>() + .map(ReflectionParameterDefaultValue::AssocArray) +} + +/// Converts one supported associative-array key expression into PHP-normalized metadata. +fn reflection_default_array_key(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(ReflectionDefaultArrayKey::Int(*value)), + ExprKind::BoolLiteral(value) => Some(ReflectionDefaultArrayKey::Int(i64::from(*value))), + ExprKind::FloatLiteral(value) => Some(ReflectionDefaultArrayKey::Int(*value as i64)), + ExprKind::StringLiteral(value) => reflection_default_string_array_key(value), + ExprKind::Null => Some(ReflectionDefaultArrayKey::Str(String::new())), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg().map(ReflectionDefaultArrayKey::Int), + ExprKind::FloatLiteral(value) => Some(ReflectionDefaultArrayKey::Int((-*value) as i64)), + _ => None, + }, + _ => None, + } +} + +/// Normalizes a string array key according to PHP integer-string key rules. +fn reflection_default_string_array_key(value: &str) -> Option { + if is_php_integer_array_key(value) { + value + .parse::() + .ok() + .map(ReflectionDefaultArrayKey::Int) + } else { + Some(ReflectionDefaultArrayKey::Str(value.to_string())) + } +} + +/// Converts scalar/null constant metadata into a parameter default value. +fn reflection_parameter_default_from_constant_value( + value: ReflectionConstantValue, +) -> Option { + match value { + ReflectionConstantValue::Int(value) => Some(ReflectionParameterDefaultValue::Int(value)), + ReflectionConstantValue::Bool(value) => Some(ReflectionParameterDefaultValue::Bool(value)), + ReflectionConstantValue::Float(value) => { + Some(ReflectionParameterDefaultValue::Float(value)) + } + ReflectionConstantValue::Str(value) => Some(ReflectionParameterDefaultValue::Str(value)), + ReflectionConstantValue::Null => Some(ReflectionParameterDefaultValue::Null), + ReflectionConstantValue::EnumCase { .. } => None, + } +} + +/// Returns PHP's constant-name metadata for parameter defaults that name a class constant. +fn reflection_parameter_default_constant_name(default: &Expr) -> Option { + match &default.kind { + ExprKind::ScopedConstantAccess { receiver, name } => Some(format!( + "{}::{}", + reflection_static_receiver_label(receiver), + name + )), + _ => None, + } +} + +/// Returns the PHP source-visible receiver label for ReflectionParameter constant defaults. +fn reflection_static_receiver_label(receiver: &StaticReceiver) -> String { + match receiver { + StaticReceiver::Named(name) => name.as_str().trim_start_matches('\\').to_string(), + StaticReceiver::Self_ => "self".to_string(), + StaticReceiver::Static => "static".to_string(), + StaticReceiver::Parent => "parent".to_string(), + } +} + +/// Converts a normalized parameter type into a supported `ReflectionType` subset. +fn reflection_parameter_type_metadata( + type_expr: Option<&TypeExpr>, + ty: &PhpType, +) -> Option { + if let Some(TypeExpr::Intersection(members)) = type_expr { + return reflection_intersection_type_metadata(members); + } + match ty { + PhpType::Union(members) => reflection_union_or_nullable_type_metadata(members), + _ => reflection_named_type_metadata(ty).map(ReflectionParameterTypeMetadata::Named), + } +} + +/// Converts a declared return type into the supported `ReflectionType` subset. +fn reflection_return_type_metadata(sig: &FunctionSig) -> Option { + if !sig.declared_return { + return None; + } + match &sig.return_type { + PhpType::Void => Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("void", false), + )), + PhpType::Never => Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("never", false), + )), + PhpType::Union(members) => reflection_union_or_nullable_type_metadata(members), + ty => reflection_named_type_metadata(ty).map(ReflectionParameterTypeMetadata::Named), + } +} + +/// Converts a normalized non-union parameter type into a simple `ReflectionNamedType`. +fn reflection_named_type_metadata(ty: &PhpType) -> Option { + match ty { + PhpType::Int => Some(reflection_builtin_named_type("int", false)), + PhpType::Float => Some(reflection_builtin_named_type("float", false)), + PhpType::Str => Some(reflection_builtin_named_type("string", false)), + PhpType::Bool => Some(reflection_builtin_named_type("bool", false)), + PhpType::Iterable => Some(reflection_builtin_named_type("iterable", false)), + PhpType::Mixed => Some(reflection_builtin_named_type("mixed", true)), + PhpType::Array(_) | PhpType::AssocArray { .. } => { + Some(reflection_builtin_named_type("array", false)) + } + PhpType::Callable => Some(reflection_builtin_named_type("callable", false)), + PhpType::Object(name) => Some(ReflectionNamedTypeMetadata { + name: name.clone(), + allows_null: false, + is_builtin: false, + }), + _ => None, + } +} + +/// Builds metadata for one builtin named type. +fn reflection_builtin_named_type(name: &str, allows_null: bool) -> ReflectionNamedTypeMetadata { + ReflectionNamedTypeMetadata { + name: name.to_string(), + allows_null, + is_builtin: true, + } +} + +/// Handles `T|null` as a nullable named type and wider unions as `ReflectionUnionType`. +fn reflection_union_or_nullable_type_metadata( + members: &[PhpType], +) -> Option { + let allows_null = members.iter().any(|member| matches!(member, PhpType::Void)); + let non_null_members = members + .iter() + .filter(|member| !matches!(member, PhpType::Void)) + .collect::>(); + if non_null_members.len() == 1 { + let mut metadata = reflection_named_type_metadata(non_null_members[0])?; + metadata.allows_null = allows_null; + return Some(ReflectionParameterTypeMetadata::Named(metadata)); + } + let types = non_null_members + .into_iter() + .map(reflection_named_type_metadata) + .collect::>>()?; + (!types.is_empty()).then_some(ReflectionParameterTypeMetadata::Union( + ReflectionUnionTypeMetadata { types, allows_null }, + )) +} + +/// Converts a declared `A&B` type into `ReflectionIntersectionType` metadata. +fn reflection_intersection_type_metadata( + members: &[TypeExpr], +) -> Option { + let types = members + .iter() + .map(reflection_named_type_metadata_from_type_expr) + .collect::>>()?; + (!types.is_empty()).then_some(ReflectionParameterTypeMetadata::Intersection( + ReflectionIntersectionTypeMetadata { types }, + )) +} + +/// Converts one declared type atom into `ReflectionNamedType` metadata. +fn reflection_named_type_metadata_from_type_expr( + type_expr: &TypeExpr, +) -> Option { + match type_expr { + TypeExpr::Int => Some(reflection_builtin_named_type("int", false)), + TypeExpr::Float => Some(reflection_builtin_named_type("float", false)), + TypeExpr::Bool => Some(reflection_builtin_named_type("bool", false)), + TypeExpr::Str => Some(reflection_builtin_named_type("string", false)), + TypeExpr::Iterable => Some(reflection_builtin_named_type("iterable", false)), + TypeExpr::Array(_) => Some(reflection_builtin_named_type("array", false)), + TypeExpr::Named(name) => { + let raw_name = name.as_str().trim_start_matches('\\'); + match raw_name.to_ascii_lowercase().as_str() { + "array" | "callable" | "mixed" | "object" => { + Some(reflection_builtin_named_type(raw_name, false)) + } + _ => Some(ReflectionNamedTypeMetadata { + name: raw_name.to_string(), + allows_null: false, + is_builtin: false, + }), + } + } + _ => None, + } +} + +/// Returns the `__construct` member object metadata when the reflected class-like symbol has one. +fn reflection_constructor_member( + method_members: &[ReflectionListedMember], +) -> Option { + method_members + .iter() + .find(|member| php_symbol_key(&member.name) == "__construct") + .cloned() +} + +/// Builds common ReflectionMethod/ReflectionProperty predicate flags. +fn reflection_member_flags( + is_static: bool, + visibility: &Visibility, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_promoted: bool, +) -> ReflectionMemberFlags { + ReflectionMemberFlags { + is_static, + is_public: visibility == &Visibility::Public, + is_protected: visibility == &Visibility::Protected, + is_private: visibility == &Visibility::Private, + is_final, + is_abstract, + is_readonly, + is_promoted, + is_virtual: false, + } +} + +/// Returns PHP case-insensitive method names declared by an interface and its parents. +fn reflection_interface_method_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + push_unique_method_names(info.methods.keys(), &mut names, &mut seen); + push_unique_method_names(info.static_methods.keys(), &mut names, &mut seen); + names +} + +/// Returns PHP case-sensitive property names declared by an interface and its parents. +fn reflection_interface_property_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for property in info.properties.keys() { + push_unique_property_name(property, &mut names, &mut seen); + } + names +} + +/// Returns PHP case-sensitive constant names declared by an interface and its parents. +fn reflection_interface_constant_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for constant in info.constants.keys() { + push_unique_constant_name(constant, &mut names, &mut seen); + } + names +} + +/// Returns PHP case-insensitive direct method names declared by a trait. +fn reflection_trait_method_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_method_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + +/// Returns PHP case-sensitive direct property names declared by a trait. +fn reflection_trait_property_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_property_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + +/// Returns PHP case-sensitive direct constant names declared by a trait. +fn reflection_trait_constant_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_constant_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + +/// Appends lower-case method names while preserving first-seen order. +fn push_unique_method_names<'a>( + method_names: impl Iterator, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + for method_name in method_names { + let key = php_symbol_key(method_name); + if seen.insert(key.clone()) { + names.push(key); + } + } +} + +/// Appends one case-sensitive property name while preserving first-seen order. +fn push_unique_property_name( + property_name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(property_name.to_string()) { + names.push(property_name.to_string()); + } +} + +/// Appends one case-sensitive class constant name while preserving first-seen order. +fn push_unique_constant_name( + constant_name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(constant_name.to_string()) { + names.push(constant_name.to_string()); + } +} + +/// Appends one constant metadata member while preserving first-seen order. +fn push_unique_constant_member( + constant_name: &str, + value: ReflectionConstantValue, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(constant_name.to_string()) { + members.push(ReflectionConstantMember { + name: constant_name.to_string(), + value, + }); + } +} + +/// Evaluates one class/interface/trait constant expression for static Reflection metadata. +fn reflection_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + expr: &Expr, + depth: usize, +) -> Result { + if depth > 16 { + return Err(CodegenIrError::unsupported( + "deep recursive ReflectionClass constant metadata", + )); + } + match &expr.kind { + ExprKind::IntLiteral(value) => Ok(ReflectionConstantValue::Int(*value)), + ExprKind::BoolLiteral(value) => Ok(ReflectionConstantValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Ok(ReflectionConstantValue::Float(*value)), + ExprKind::StringLiteral(value) => Ok(ReflectionConstantValue::Str(value.clone())), + ExprKind::Null => Ok(ReflectionConstantValue::Null), + ExprKind::Negate(inner) => { + match reflection_constant_value(ctx, current_class, current_info, inner, depth + 1)? { + ReflectionConstantValue::Int(value) => Ok(ReflectionConstantValue::Int(-value)), + ReflectionConstantValue::Float(value) => Ok(ReflectionConstantValue::Float(-value)), + other => Err(unsupported_reflection_constant_value(other)), + } + } + ExprKind::BinaryOp { left, op, right } => reflection_binary_constant_value( + ctx, + current_class, + current_info, + left, + op, + right, + depth + 1, + ), + ExprKind::ClassConstant { receiver } => { + let class_name = + reflection_static_receiver_name(current_class, current_info, receiver)?; + Ok(ReflectionConstantValue::Str(class_name)) + } + ExprKind::ScopedConstantAccess { receiver, name } => reflection_scoped_constant_value( + ctx, + current_class, + current_info, + receiver, + name, + depth + 1, + ), + other => Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata expression {:?}", + other + ))), + } +} + +/// Evaluates one supported binary operator in a static Reflection constant expression. +fn reflection_binary_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + left: &Expr, + op: &BinOp, + right: &Expr, + depth: usize, +) -> Result { + let left = reflection_constant_value(ctx, current_class, current_info, left, depth)?; + let right = reflection_constant_value(ctx, current_class, current_info, right, depth)?; + match (&left, op, &right) { + ( + ReflectionConstantValue::Int(left), + BinOp::Add, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left + *right)) + } + ( + ReflectionConstantValue::Int(left), + BinOp::Sub, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left - *right)) + } + ( + ReflectionConstantValue::Int(left), + BinOp::Mul, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left * *right)) + } + ( + ReflectionConstantValue::Int(left), + BinOp::Mod, + ReflectionConstantValue::Int(right), + ) if *right != 0 => { + Ok(ReflectionConstantValue::Int(*left % *right)) + } + ( + ReflectionConstantValue::Int(left), + BinOp::Pow, + ReflectionConstantValue::Int(right), + ) if *right >= 0 => + { + Ok(ReflectionConstantValue::Int((*left).pow(*right as u32))) + } + ( + ReflectionConstantValue::Str(left), + BinOp::Concat, + ReflectionConstantValue::Str(right), + ) => Ok(ReflectionConstantValue::Str(format!("{}{}", left, right))), + ( + left, + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, + right, + ) => reflection_float_binary_constant_value(left, op, right).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata binary value {:?} {:?}", + reflection_constant_value_kind(left), + reflection_constant_value_kind(right) + )) + }), + (left, _, right) => Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata binary value {:?} {:?}", + reflection_constant_value_kind(left), + reflection_constant_value_kind(right) + ))), + } +} + +/// Evaluates a numeric binary operator that must produce a float Reflection value. +fn reflection_float_binary_constant_value( + left: &ReflectionConstantValue, + op: &BinOp, + right: &ReflectionConstantValue, +) -> Option { + let left = reflection_constant_value_as_float(left)?; + let right = reflection_constant_value_as_float(right)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + Some(ReflectionConstantValue::Float(value)) +} + +/// Returns the float representation of numeric Reflection constant metadata. +fn reflection_constant_value_as_float(value: &ReflectionConstantValue) -> Option { + match value { + ReflectionConstantValue::Int(value) => Some(*value as f64), + ReflectionConstantValue::Float(value) => Some(*value), + _ => None, + } +} + +/// Resolves and evaluates one scoped class/interface/trait constant value. +fn reflection_scoped_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + receiver: &StaticReceiver, + constant_name: &str, + depth: usize, +) -> Result { + let class_name = reflection_static_receiver_name(current_class, current_info, receiver)?; + if let Some((resolved_name, info)) = resolve_reflection_class(ctx, &class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return reflection_constant_value(ctx, resolved_name, Some(info), value_expr, depth); + } + for interface_name in &info.interfaces { + if let Some(value_expr) = + reflection_interface_constant_expr(ctx, interface_name, constant_name) + { + return reflection_constant_value(ctx, interface_name, None, &value_expr, depth); + } + } + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &class_name) { + if let Some(value_expr) = + reflection_interface_constant_expr(ctx, interface_name, constant_name) + { + return reflection_constant_value(ctx, interface_name, None, &value_expr, depth); + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &class_name) { + if let Some(value_expr) = ctx + .module + .declared_trait_constants + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + { + return reflection_constant_value(ctx, trait_name, None, value_expr, depth); + } + } + if ctx + .module + .enum_infos + .get(&class_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == constant_name)) + { + return Ok(ReflectionConstantValue::EnumCase { + enum_name: class_name, + case_name: constant_name.to_string(), + }); + } + Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata for {}::{}", + current_class, constant_name + ))) +} + +/// Returns an interface constant expression, including inherited parent interfaces. +fn reflection_interface_constant_expr( + ctx: &FunctionContext<'_>, + interface_name: &str, + constant_name: &str, +) -> Option { + let mut visited = std::collections::HashSet::new(); + let mut queue = vec![interface_name.to_string()]; + while let Some(name) = queue.pop() { + if !visited.insert(name.clone()) { + continue; + } + if let Some(info) = ctx.module.interface_infos.get(&name) { + if let Some(value) = info.constants.get(constant_name) { + return Some(value.clone()); + } + queue.extend(info.parents.iter().cloned()); + } + } + None +} + +/// Resolves a static receiver against the current reflected declaration. +fn reflection_static_receiver_name( + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + receiver: &StaticReceiver, +) -> Result { + match receiver { + StaticReceiver::Named(name) => Ok(name.as_str().trim_start_matches('\\').to_string()), + StaticReceiver::Self_ | StaticReceiver::Static => Ok(current_class.to_string()), + StaticReceiver::Parent => current_info + .and_then(|info| info.parent.clone()) + .ok_or_else(|| { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata parent receiver in {}", + current_class + )) + }), + } +} + +/// Returns a small label for unsupported constant-value diagnostics. +fn reflection_constant_value_kind(value: &ReflectionConstantValue) -> &'static str { + match value { + ReflectionConstantValue::Int(_) => "int", + ReflectionConstantValue::Bool(_) => "bool", + ReflectionConstantValue::Float(_) => "float", + ReflectionConstantValue::Str(_) => "string", + ReflectionConstantValue::Null => "null", + ReflectionConstantValue::EnumCase { .. } => "enum-case", + } +} + +/// Reports an unsupported unary constant value while avoiding large debug output. +fn unsupported_reflection_constant_value(value: ReflectionConstantValue) -> CodegenIrError { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata unary value {}", + reflection_constant_value_kind(&value) + )) +} + +/// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. +fn resolve_reflection_class_constant<'a>( + ctx: &'a FunctionContext<'_>, + class_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a crate::types::ClassInfo)> { + let (resolved_name, info) = resolve_reflection_class(ctx, class_name)?; + if info.constants.contains_key(constant_name) { + return Some((resolved_name, info)); + } + let parent = info.parent.as_deref()?; + resolve_reflection_class_constant(ctx, parent, constant_name) +} + +/// Looks up enum-case metadata by PHP-style enum name and case-sensitive case name. +fn resolve_reflection_enum_case<'a>( + ctx: &'a FunctionContext<'_>, + enum_name: &str, + case_name: &str, +) -> Option<(&'a str, &'a crate::types::EnumCaseInfo)> { + let enum_key = php_symbol_key(enum_name.trim_start_matches('\\')); + ctx.module + .enum_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) + .and_then(|(name, info)| { + info.cases + .iter() + .find(|case| case.name == case_name) + .map(|case| (name.as_str(), case)) + }) +} + +/// Returns a static Reflection value for a backed enum case, when present. +fn reflection_enum_case_backing_value(case: &EnumCaseInfo) -> Option { + match case.value.as_ref()? { + EnumCaseValue::Int(value) => Some(ReflectionConstantValue::Int(*value)), + EnumCaseValue::Str(value) => Some(ReflectionConstantValue::Str(value.clone())), + } +} + +/// Returns empty Reflection metadata for unsupported dynamic constructor operands. +fn empty_reflection_metadata() -> ReflectionOwnerMetadata { + ReflectionOwnerMetadata { + reflected_name: None, + attr_names: Vec::new(), + attr_args: Vec::new(), + interface_names: Vec::new(), + trait_names: Vec::new(), + trait_aliases: Vec::new(), + parent_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + default_property_members: Vec::new(), + static_property_members: Vec::new(), + constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + property_hook_members: Vec::new(), + constructor_member: None, + parent_class_name: None, + constant_value: None, + backing_value: None, + is_enum_case: false, + parameter_members: Vec::new(), + type_metadata: None, + property_default_value: None, + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_anonymous: false, + is_instantiable: false, + is_cloneable: false, + is_iterable: false, + modifiers: 0, + member_flags: ReflectionMemberFlags::default(), + } +} + +/// Extracts a constant string or class-name operand from an EIR value. +fn const_string_or_class_operand( + ctx: &FunctionContext<'_>, + value: ValueId, + owner: &str, +) -> Result { + const_data_operand(ctx, value, owner, true) +} + +/// Extracts a constant string operand from an EIR value. +fn const_required_string_operand( + ctx: &FunctionContext<'_>, + value: ValueId, + owner: &str, +) -> Result { + const_data_operand(ctx, value, owner, false) +} + +/// Extracts a constant ReflectionParameter name or offset selector from EIR. +fn const_parameter_selector_operand( + ctx: &FunctionContext<'_>, + value: ValueId, +) -> Result { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Err(CodegenIrError::unsupported( + "ReflectionParameter constructor with non-literal parameter selector", + )); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + match inst_ref.op { + Op::ConstI64 => match inst_ref.immediate { + Some(Immediate::I64(value)) => Ok(ReflectionParameterSelector::Position(value)), + _ => Err(CodegenIrError::invalid_module( + "ReflectionParameter position selector missing i64 immediate", + )), + }, + Op::ConstStr => { + let Some(Immediate::Data(data)) = inst_ref.immediate else { + return Err(CodegenIrError::invalid_module( + "ReflectionParameter name selector missing data id", + )); + }; + ctx.module + .data + .strings + .get(data.as_raw() as usize) + .cloned() + .map(ReflectionParameterSelector::Name) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw())) + } + _ => Err(CodegenIrError::unsupported( + "ReflectionParameter constructor with non-literal parameter selector", + )), + } +} + +/// Reads a `ConstStr` or optional `ConstClassName` value from the module data pool. +fn const_data_operand( + ctx: &FunctionContext<'_>, + value: ValueId, + owner: &str, + allow_class_name: bool, +) -> Result { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Err(CodegenIrError::unsupported(format!( + "{} constructor with non-literal reflection argument", + owner + ))); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + let Some(Immediate::Data(data)) = inst_ref.immediate else { + return Err(CodegenIrError::invalid_module(format!( + "{} reflection literal missing data id", + owner + ))); + }; + match inst_ref.op { + Op::ConstStr => ctx + .module + .data + .strings + .get(data.as_raw() as usize) + .cloned() + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw())), + Op::ConstClassName if allow_class_name => ctx + .module + .data + .class_names + .get(data.as_raw() as usize) + .cloned() + .ok_or_else(|| CodegenIrError::missing_entry("class data", data.as_raw())), + _ => Err(CodegenIrError::unsupported(format!( + "{} constructor with non-literal reflection argument", + owner + ))), + } +} + +/// Writes a heap-persisted string into the current Reflection object result slot. +fn emit_reflection_string_property( + ctx: &mut FunctionContext<'_>, + value: &str, + low_offset: usize, + high_offset: usize, +) { + let (label, len) = ctx.data.add_string(value.as_bytes()); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, "x1", object_reg, low_offset); + abi::emit_store_to_address(ctx.emitter, "x2", object_reg, high_offset); + } + Arch::X86_64 => { + abi::emit_symbol_address(ctx.emitter, "rax", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, "rax", object_reg, low_offset); + abi::emit_store_to_address(ctx.emitter, "rdx", object_reg, high_offset); + } + } + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); +} + +/// Writes a heap-persisted string into a named Reflection owner property slot. +fn emit_reflection_owner_string_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + value: &str, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + emit_reflection_string_property(ctx, value, low_offset, low_offset + 8); + Ok(()) +} + +/// Replaces the Reflection object's default `__attrs` array with populated metadata. +fn emit_reflection_attrs_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + attr_names: &[String], + attr_args: &[Option>], +) -> Result<()> { + let (attrs_low_offset, attrs_high_offset) = reflection_attrs_offsets(ctx, class_name)?; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + super::super::builtins::attributes::emit_reflection_attribute_array( + ctx, + attr_names, + attr_args, + reflection_attribute_target_for_owner(class_name), + )?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + attrs_high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Returns PHP's `Attribute::TARGET_*` bitmask for attributes on one Reflection owner type. +fn reflection_attribute_target_for_owner(class_name: &str) -> i64 { + match class_name { + "ReflectionClass" | "ReflectionObject" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS + } + "ReflectionFunction" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_FUNCTION + } + "ReflectionMethod" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_METHOD + } + "ReflectionProperty" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_PROPERTY + } + "ReflectionParameter" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_PARAMETER + } + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT + } + _ => 0, + } +} + +/// Replaces a Reflection owner private array slot with an indexed string array. +fn emit_reflection_owner_string_array_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + names: &[String], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_string_array(ctx, names)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private slot with name-keyed ReflectionClass objects. +fn emit_reflection_class_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + names: &[String], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_class_array(ctx, names)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private slot with an associative constant-value array. +fn emit_reflection_constant_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + members: &[ReflectionConstantMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_constant_array(ctx, members)?; + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private slot with an associative default-property array. +fn emit_reflection_default_property_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + members: &[ReflectionDefaultPropertyMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_default_property_array(ctx, members); + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private slot with current static-property values. +fn emit_reflection_static_property_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + members: &[ReflectionStaticPropertyMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_static_property_array(ctx, members); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a reflection-owner private array slot with member reflector objects. +fn emit_reflection_member_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + member_class_name: &str, + members: &[ReflectionListedMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_member_array(ctx, member_class_name, members)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionProperty private slot with string-keyed hook ReflectionMethod objects. +fn emit_reflection_property_hook_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + members: &[(String, ReflectionListedMember)], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_property_hook_array(ctx, members)?; + let assoc_type = reflection_property_hook_map_type(); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + runtime_value_tag(&assoc_type) as i64, + ); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private constructor slot with `ReflectionMethod|null`. +fn emit_reflection_constructor_property( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + member: Option<&ReflectionListedMember>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__constructor")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(member) = member { + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } else { + super::emit_boxed_null(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Replaces a ReflectionMethod private prototype slot with `ReflectionMethod|null`. +fn emit_reflection_method_prototype_property( + ctx: &mut FunctionContext<'_>, + member: Option<&ReflectionListedMember>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionMethod") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__prototype")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(member) = member { + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Replaces a ReflectionClass-like private parent slot with `ReflectionClass|false`. +fn emit_reflection_parent_class_property( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + parent_class_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__parent_class")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(parent_class_name) = parent_class_name { + let parent_metadata = reflection_class_metadata_for_name(ctx, parent_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &parent_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Replaces a member reflector's private declaring-class slot with `ReflectionClass|false`. +fn emit_reflection_declaring_class_property( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + declaring_class_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let Some(low_offset) = class_info + .property_offsets + .get("__declaring_class") + .copied() + else { + return Ok(()); + }; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(declaring_class_name) = declaring_class_name { + let declaring_metadata = + reflection_shallow_class_metadata_for_name(ctx, declaring_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &declaring_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Replaces an enum-case reflector's private enum slot with `ReflectionEnum`. +fn emit_reflection_enum_property( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + enum_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let Some(low_offset) = class_info.property_offsets.get("__enum").copied() else { + return Ok(()); + }; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(enum_name) = enum_name { + let enum_metadata = reflection_enum_metadata_for_name(ctx, enum_name)?; + emit_reflection_owner_object(ctx, "ReflectionEnum", &enum_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionEnum".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Replaces a ReflectionMethod private array slot with ReflectionParameter objects. +fn emit_reflection_parameter_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + parameters: &[ReflectionParameterMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_parameter_array(ctx, parameters)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Allocates an indexed array of populated ReflectionMethod/ReflectionProperty objects. +fn emit_reflection_member_array( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + members: &[ReflectionListedMember], +) -> Result<()> { + emit_reflection_indexed_array(ctx, members.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object(member_class_name.to_string()), + ); + + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_member_object(ctx, member_class_name, member)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); + } + + Ok(()) +} + +/// Allocates a string-keyed hook map with populated ReflectionMethod objects. +fn emit_reflection_property_hook_array( + ctx: &mut FunctionContext<'_>, + members: &[(String, ReflectionListedMember)], +) -> Result<()> { + emit_empty_assoc_array_literal_to_result( + ctx, + &PhpType::Object("ReflectionMethod".to_string()), + ); + for (key, member) in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_reflection_method_hash_insert(ctx, key); + } + Ok(()) +} + +/// Allocates an indexed array of populated ReflectionParameter objects. +fn emit_reflection_parameter_array( + ctx: &mut FunctionContext<'_>, + parameters: &[ReflectionParameterMember], +) -> Result<()> { + emit_reflection_indexed_array(ctx, parameters.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object("ReflectionParameter".to_string()), + ); + + for parameter in parameters { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_parameter_object(ctx, parameter)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); + } + + Ok(()) +} + +/// Allocates and populates the associative ReflectionClass constant map. +fn emit_reflection_constant_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionConstantMember], +) -> Result<()> { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, &member.value); + emit_reflection_constant_hash_insert(ctx, &member.name); + } + Ok(()) +} + +/// Allocates and populates a name-keyed map of full ReflectionClass objects. +fn emit_reflection_class_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Object("ReflectionClass".to_string())); + for name in names { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + let metadata = reflection_class_metadata_for_name(ctx, name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &metadata)?; + emit_reflection_class_hash_insert(ctx, name); + } + Ok(()) +} + +/// Inserts the current ReflectionClass object into the stacked associative array. +fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // object hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Object("ReflectionClass".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("xor r8, r8"); // object hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Object("ReflectionClass".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Inserts the current ReflectionMethod object into the stacked associative array. +fn emit_reflection_method_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionMethod object as the hook hash payload + ctx.emitter.instruction("mov x4, xzr"); // object hook hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Object("ReflectionMethod".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionMethod object as the hook hash payload + ctx.emitter.instruction("xor r8, r8"); // object hook hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Object("ReflectionMethod".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Returns the associative map type used by `ReflectionProperty::getHooks()`. +fn reflection_property_hook_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionMethod".to_string())), + } +} + +/// Replaces a ReflectionClass-like private slot with a string-keyed string-value map. +fn emit_reflection_string_assoc_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + entries: &[(String, String)], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_hash"); + emit_reflection_string_assoc_array(ctx, entries); + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + runtime_value_tag(&assoc_type) as i64, + ); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Allocates and populates a string-keyed associative array of string values. +fn emit_reflection_string_assoc_array( + ctx: &mut FunctionContext<'_>, + entries: &[(String, String)], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Str); + for (key, value) in entries { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_string_literal_default_to_result(ctx, value); + emit_reflection_string_hash_insert(ctx, key); + } +} + +/// Inserts the current owned string value into the stacked associative array. +#[rustfmt::skip] +fn emit_reflection_string_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + ctx.emitter.instruction("mov x3, x1"); // pass the persistent Reflection string as the hash payload pointer + ctx.emitter.instruction("mov x4, x2"); // pass the Reflection string length as the hash payload high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate(ctx.emitter, "x5", runtime_value_tag(&PhpType::Str) as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + ctx.emitter.instruction("mov rcx, rax"); // pass the persistent Reflection string as the hash payload pointer + ctx.emitter.instruction("mov r8, rdx"); // pass the Reflection string length as the hash payload high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate(ctx.emitter, "r9", runtime_value_tag(&PhpType::Str) as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Allocates and populates the associative ReflectionClass default-property map. +fn emit_reflection_default_property_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionDefaultPropertyMember], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_default_value_as_mixed(ctx, &member.value); + emit_reflection_constant_hash_insert(ctx, &member.name); + } +} + +/// Allocates and populates current static-property values for ReflectionClass. +fn emit_reflection_static_property_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionStaticPropertyMember], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + let skip_label = emit_skip_if_static_property_uninitialized(ctx, member); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + let symbol = static_property_symbol(&member.declaring_class_name, &member.name); + abi::emit_load_symbol_to_result(ctx.emitter, &symbol, &member.php_type); + emit_box_current_value_as_mixed(ctx.emitter, &member.php_type.codegen_repr()); + emit_reflection_static_property_hash_insert(ctx, &member.name); + if let Some(skip_label) = skip_label { + ctx.emitter.label(&skip_label); + } + } +} + +/// Emits a branch over uninitialized typed static properties, matching PHP reflection. +#[rustfmt::skip] +fn emit_skip_if_static_property_uninitialized( + ctx: &mut FunctionContext<'_>, + member: &ReflectionStaticPropertyMember, +) -> Option { + if !member.is_declared { + return None; + } + let skip_label = ctx.next_label("reflection_static_uninitialized"); + let symbol = static_property_symbol(&member.declaring_class_name, &member.name); + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, marker_reg, &symbol, 8); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction(&format!("b.eq {}", skip_label)); // omit uninitialized typed static properties from the reflection map + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction(&format!("je {}", skip_label)); // omit uninitialized typed static properties from the reflection map + } + } + Some(skip_label) +} + +/// Materializes one Reflection constant value as a boxed Mixed cell. +fn emit_reflection_constant_value_as_mixed( + ctx: &mut FunctionContext<'_>, + value: &ReflectionConstantValue, +) { + match value { + ReflectionConstantValue::Int(value) => emit_boxed_int_literal_to_result(ctx, *value), + ReflectionConstantValue::Bool(value) => emit_boxed_bool_literal_to_result(ctx, *value), + ReflectionConstantValue::Float(value) => emit_boxed_float_literal_to_result(ctx, *value), + ReflectionConstantValue::Str(value) => { + emit_boxed_string_literal_default_to_result(ctx, value) + } + ReflectionConstantValue::Null => emit_boxed_null_literal_to_result(ctx), + ReflectionConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &case_label, + 0, + ); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Materializes one Reflection default-property value as a boxed Mixed cell. +fn emit_reflection_default_value_as_mixed( + ctx: &mut FunctionContext<'_>, + value: &ReflectionParameterDefaultValue, +) { + match value { + ReflectionParameterDefaultValue::Int(value) => { + emit_boxed_int_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Bool(value) => { + emit_boxed_bool_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Float(value) => { + emit_boxed_float_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Str(value) => { + emit_boxed_string_literal_default_to_result(ctx, value) + } + ReflectionParameterDefaultValue::Null => emit_boxed_null_literal_to_result(ctx), + ReflectionParameterDefaultValue::Object { args, .. } if args.is_empty() => { + emit_boxed_null_literal_to_result(ctx) + } + ReflectionParameterDefaultValue::Object { args, .. } => { + emit_reflection_indexed_array_default_as_mixed(ctx, args) + } + ReflectionParameterDefaultValue::Array(elements) => { + emit_reflection_indexed_array_default_as_mixed(ctx, elements) + } + ReflectionParameterDefaultValue::AssocArray(entries) => { + emit_reflection_assoc_array_default_as_mixed(ctx, entries) + } + } +} + +/// Materializes an indexed Reflection default array as a boxed Mixed cell. +fn emit_reflection_indexed_array_default_as_mixed( + ctx: &mut FunctionContext<'_>, + elements: &[ReflectionParameterDefaultValue], +) { + emit_reflection_indexed_array_default_to_result(ctx, elements); + emit_box_current_owned_value_as_mixed(ctx.emitter, &PhpType::Array(Box::new(PhpType::Mixed))); +} + +/// Allocates and populates an indexed array whose slots hold boxed Mixed defaults. +fn emit_reflection_indexed_array_default_to_result( + ctx: &mut FunctionContext<'_>, + elements: &[ReflectionParameterDefaultValue], +) { + emit_reflection_mixed_array_allocation(ctx, elements.len()); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + for element in elements { + emit_reflection_default_value_as_mixed(ctx, element); + append_reflection_mixed_array_default_element(ctx); + } + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); +} + +/// Allocates an indexed array stamped for boxed Mixed payload slots. +fn emit_reflection_mixed_array_allocation(ctx: &mut FunctionContext<'_>, element_count: usize) { + let capacity = element_count.max(4); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", 8); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", 8); + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_new"); + emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Mixed, + ); +} + +/// Appends the boxed Mixed result value to the indexed array saved on the stack. +#[rustfmt::skip] +fn append_reflection_mixed_array_default_element(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x9"); + abi::emit_push_reg(ctx.emitter, "x0"); + ctx.emitter.instruction("mov x1, x0"); // pass the boxed Reflection default to the array append helper + ctx.emitter.instruction("mov x0, x9"); // pass the saved default-array pointer to the append helper + abi::emit_call_label(ctx.emitter, "__rt_array_push_refcounted"); + emit_release_pushed_refcounted_temp_after_array_push(ctx.emitter, &PhpType::Mixed); + abi::emit_push_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "r11"); + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rsi, rax"); // pass the boxed Reflection default to the array append helper + ctx.emitter.instruction("mov rdi, r11"); // pass the saved default-array pointer to the append helper + abi::emit_call_label(ctx.emitter, "__rt_array_push_refcounted"); + emit_release_pushed_refcounted_temp_after_array_push(ctx.emitter, &PhpType::Mixed); + abi::emit_push_reg(ctx.emitter, "rax"); + } + } +} + +/// Materializes an associative Reflection default array as a boxed Mixed cell. +fn emit_reflection_assoc_array_default_as_mixed( + ctx: &mut FunctionContext<'_>, + entries: &[ReflectionDefaultAssocEntry], +) { + emit_reflection_assoc_array_default_to_result(ctx, entries); + emit_box_current_owned_value_as_mixed( + ctx.emitter, + &PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, + ); +} + +/// Allocates and populates an associative array whose values are boxed Mixed defaults. +fn emit_reflection_assoc_array_default_to_result( + ctx: &mut FunctionContext<'_>, + entries: &[ReflectionDefaultAssocEntry], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for entry in entries { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_default_value_as_mixed(ctx, &entry.value); + emit_reflection_assoc_array_default_insert(ctx, &entry.key); + } +} + +/// Inserts the current boxed Mixed default value into the stacked associative default array. +#[rustfmt::skip] +fn emit_reflection_assoc_array_default_insert( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection default as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + emit_reflection_default_array_key_aarch64(ctx, key); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection default as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + emit_reflection_default_array_key_x86_64(ctx, key); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Materializes an associative default-array key in AArch64 hash-key registers. +fn emit_reflection_default_array_key_aarch64( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match key { + ReflectionDefaultArrayKey::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, "x1", *value); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + ReflectionDefaultArrayKey::Str(value) => { + let (key_label, key_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + } + } +} + +/// Materializes an associative default-array key in x86_64 SysV hash-key registers. +fn emit_reflection_default_array_key_x86_64( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match key { + ReflectionDefaultArrayKey::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", *value); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + ReflectionDefaultArrayKey::Str(value) => { + let (key_label, key_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + } + } +} + +/// Inserts the current boxed Mixed constant value into the stacked associative array. +fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Inserts the current boxed Mixed static-property value into the stacked associative array. +#[rustfmt::skip] +fn emit_reflection_static_property_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection static value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection static value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Allocates and populates one ReflectionMethod/ReflectionProperty object. +fn emit_reflection_member_object( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + member: &ReflectionListedMember, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", member_class_name)) + })?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let name_offset = reflection_property_offset(class_info, "__name")?; + emit_reflection_string_property(ctx, &member.name, name_offset, name_offset + 8); + emit_reflection_attrs_property( + ctx, + member_class_name, + &member.attr_names, + &member.attr_args, + )?; + emit_reflection_declaring_class_property( + ctx, + member_class_name, + member.declaring_class_name.as_deref(), + )?; + if member_class_name == "ReflectionMethod" { + emit_reflection_parameter_array_property_by_name( + ctx, + member_class_name, + "__parameters", + &member.parameters, + )?; + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__required_parameter_count", + member.required_parameter_count, + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_return_type", + member.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_deprecated", + member.is_deprecated, + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_generator", + member.is_generator, + )?; + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + member.modifiers, + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_prototype", + member.prototype_member.is_some(), + )?; + emit_reflection_method_prototype_property(ctx, member.prototype_member.as_deref())?; + } + if member_class_name == "ReflectionProperty" { + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + member.modifiers, + )?; + emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?; + emit_reflection_owner_type_property_by_name( + ctx, + member_class_name, + "__settable_type", + member.type_metadata.as_ref(), + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_default_value", + member.default_value.is_some(), + )?; + emit_reflection_owner_default_value_property( + ctx, + member_class_name, + member.default_value.as_ref(), + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_hooks", + !member.property_hook_members.is_empty(), + )?; + if !member.property_hook_members.is_empty() { + emit_reflection_property_hook_array_property_by_name( + ctx, + member_class_name, + "__hooks", + &member.property_hook_members, + )?; + } + let property_string = reflection_property_to_string( + &member.name, + member.flags, + member.type_metadata.as_ref(), + member.default_value.as_ref(), + ); + emit_reflection_owner_string_property_by_name( + ctx, + member_class_name, + "__string", + &property_string, + )?; + } + if matches!( + member_class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + if let Some(value) = &member.constant_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, member_class_name, "__value")?; + } + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_enum_case", + member.is_enum_case, + )?; + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + member.modifiers, + )?; + } + if member_class_name == "ReflectionEnumBackedCase" { + if let Some(value) = &member.backing_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result( + ctx, + member_class_name, + "__backing_value", + )?; + } + } + if matches!( + member_class_name, + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + emit_reflection_enum_property(ctx, member_class_name, member.declaring_class_name.as_deref())?; + } + emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; + Ok(()) +} + +/// Allocates and populates one ReflectionParameter object. +fn emit_reflection_parameter_object( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionParameter"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + emit_reflection_parameter_properties(ctx, parameter) +} + +/// Writes one ReflectionParameter object's private metadata properties. +fn emit_reflection_parameter_properties( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let name_offset = reflection_property_offset(class_info, "__name")?; + let default_value_constant_name_offset = + reflection_property_offset(class_info, "__default_value_constant_name")?; + let default_value_object_class_offset = + reflection_property_offset(class_info, "__default_value_object_class")?; + emit_reflection_string_property(ctx, ¶meter.name, name_offset, name_offset + 8); + emit_reflection_attrs_property( + ctx, + "ReflectionParameter", + ¶meter.attr_names, + ¶meter.attr_args, + )?; + emit_reflection_owner_int_property( + ctx, + "ReflectionParameter", + "__position", + parameter.position, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__optional", + parameter.is_optional, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__variadic", + parameter.is_variadic, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_passed_by_reference", + parameter.is_passed_by_reference, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_promoted", + parameter.is_promoted, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__has_type", + parameter.has_type, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__allows_null", + parameter.allows_null, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_array_type", + parameter.is_array_type, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_callable_type", + parameter.is_callable_type, + )?; + emit_reflection_parameter_type_property(ctx, parameter)?; + emit_reflection_parameter_class_property(ctx, parameter)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__has_default_value", + parameter.default_value.is_some(), + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_default_value_constant", + parameter.default_value_constant_name.is_some(), + )?; + emit_reflection_string_property( + ctx, + parameter + .default_value_constant_name + .as_deref() + .unwrap_or(""), + default_value_constant_name_offset, + default_value_constant_name_offset + 8, + ); + emit_reflection_string_property( + ctx, + reflection_parameter_default_object_class(parameter.default_value.as_ref()).unwrap_or(""), + default_value_object_class_offset, + default_value_object_class_offset + 8, + ); + emit_reflection_parameter_default_property(ctx, parameter)?; + emit_reflection_parameter_declaring_class_property(ctx, parameter)?; + emit_reflection_parameter_declaring_function_property(ctx, parameter)?; + Ok(()) +} + +/// Returns the class name for object parameter defaults that are materialized lazily. +fn reflection_parameter_default_object_class( + default_value: Option<&ReflectionParameterDefaultValue>, +) -> Option<&str> { + match default_value { + Some(ReflectionParameterDefaultValue::Object { class_name, .. }) => Some(class_name), + _ => None, + } +} + +/// Writes one ReflectionParameter object's declaring-function slot. +fn emit_reflection_parameter_declaring_function_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let declaring_function_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__declaring_function")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match parameter.declaring_function.as_ref() { + Some(ReflectionDeclaringFunctionMember::Function { + name, + attr_names, + attr_args, + required_parameter_count, + type_metadata, + is_deprecated, + is_generator, + }) => { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(name.clone()); + metadata.attr_names = attr_names.clone(); + metadata.attr_args = attr_args.clone(); + metadata.required_parameter_count = *required_parameter_count; + metadata.type_metadata = type_metadata.clone(); + metadata.is_deprecated = *is_deprecated; + metadata.is_generator = *is_generator; + emit_reflection_owner_object(ctx, "ReflectionFunction", &metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionFunction".to_string()), + ); + } + Some(ReflectionDeclaringFunctionMember::Method { + name, + declaring_class_name, + attr_names, + attr_args, + flags, + required_parameter_count, + type_metadata, + is_deprecated, + is_generator, + }) => { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(name.clone()); + metadata.parent_class_name = declaring_class_name.clone(); + metadata.attr_names = attr_names.clone(); + metadata.attr_args = attr_args.clone(); + metadata.member_flags = *flags; + metadata.required_parameter_count = *required_parameter_count; + metadata.type_metadata = type_metadata.clone(); + metadata.modifiers = reflection_method_modifiers_from_flags(*flags); + metadata.is_deprecated = *is_deprecated; + metadata.is_generator = *is_generator; + emit_reflection_owner_object(ctx, "ReflectionMethod", &metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } + None => emit_boxed_null_literal_to_result(ctx), + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address( + ctx.emitter, + result_reg, + object_reg, + declaring_function_offset, + ); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, declaring_function_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Writes one ReflectionParameter object's nullable declaring-class slot. +fn emit_reflection_parameter_declaring_class_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let declaring_class_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__declaring_class")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(declaring_class_name) = parameter.declaring_class_name.as_deref() { + let declaring_metadata = + reflection_shallow_class_metadata_for_name(ctx, declaring_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &declaring_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, declaring_class_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, declaring_class_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Writes one ReflectionParameter object's nullable `ReflectionNamedType` slot. +fn emit_reflection_parameter_type_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + emit_reflection_owner_type_property( + ctx, + "ReflectionParameter", + parameter.type_metadata.as_ref(), + ) +} + +/// Writes one ReflectionParameter object's legacy nullable class-type slot. +fn emit_reflection_parameter_class_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let class_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__class")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(class_name) = reflection_parameter_class_name(parameter) { + let class_metadata = reflection_shallow_class_metadata_for_name(ctx, class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &class_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, class_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, class_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Returns the retained object class name for ReflectionParameter::getClass(). +fn reflection_parameter_class_name(parameter: &ReflectionParameterMember) -> Option<&str> { + match parameter.type_metadata.as_ref()? { + ReflectionParameterTypeMetadata::Named(metadata) if !metadata.is_builtin => { + Some(metadata.name.as_str()) + } + _ => None, + } +} + +/// Writes one reflection owner's nullable type slot. +fn emit_reflection_owner_type_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + type_metadata: Option<&ReflectionParameterTypeMetadata>, +) -> Result<()> { + emit_reflection_owner_type_property_by_name(ctx, class_name, "__type", type_metadata) +} + +/// Writes one reflection owner's nullable type-like slot. +fn emit_reflection_owner_type_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + type_metadata: Option<&ReflectionParameterTypeMetadata>, +) -> Result<()> { + let type_offset = { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, property_name)? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match type_metadata { + Some(ReflectionParameterTypeMetadata::Named(type_metadata)) => { + emit_reflection_named_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionNamedType".to_string()), + ); + } + Some(ReflectionParameterTypeMetadata::Union(type_metadata)) => { + emit_reflection_union_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionUnionType".to_string()), + ); + } + Some(ReflectionParameterTypeMetadata::Intersection(type_metadata)) => { + emit_reflection_intersection_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionIntersectionType".to_string()), + ); + } + None => emit_boxed_null_literal_to_result(ctx), + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, type_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, type_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Writes one ReflectionParameter object's boxed default-value slot. +fn emit_reflection_parameter_default_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + emit_reflection_owner_default_value_property( + ctx, + "ReflectionParameter", + parameter.default_value.as_ref(), + ) +} + +/// Writes one reflection owner's boxed default-value slot. +fn emit_reflection_owner_default_value_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + default_value: Option<&ReflectionParameterDefaultValue>, +) -> Result<()> { + let default_offset = { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__default_value")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match default_value { + Some(value) => emit_reflection_default_value_as_mixed(ctx, value), + None => emit_boxed_null_literal_to_result(ctx), + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, default_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, default_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Allocates and populates one `ReflectionUnionType` object. +fn emit_reflection_union_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionUnionTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionUnionType") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionUnionType"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + emit_reflection_union_type_types_property(ctx, &type_metadata.types)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionUnionType", + "__allows_null", + type_metadata.allows_null, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionUnionType", + "__is_builtin", + type_metadata.types.iter().all(|member| member.is_builtin), + )?; + Ok(()) +} + +/// Writes the `ReflectionUnionType::__types` array of `ReflectionNamedType` objects. +fn emit_reflection_union_type_types_property( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + let types_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionUnionType") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__types")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_reflection_named_type_array(ctx, types)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, types_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + types_offset + 8, + ); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Allocates and populates one `ReflectionIntersectionType` object. +fn emit_reflection_intersection_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionIntersectionTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionIntersectionType") + .ok_or_else(|| { + CodegenIrError::unsupported("unknown class ReflectionIntersectionType") + })?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + emit_reflection_intersection_type_types_property(ctx, &type_metadata.types)?; + emit_reflection_owner_bool_property(ctx, "ReflectionIntersectionType", "__allows_null", false)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionIntersectionType", + "__is_builtin", + type_metadata.types.iter().all(|member| member.is_builtin), + )?; + Ok(()) +} + +/// Writes the `ReflectionIntersectionType::__types` array of `ReflectionNamedType` objects. +fn emit_reflection_intersection_type_types_property( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + let types_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionIntersectionType") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__types")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_reflection_named_type_array(ctx, types)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, types_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + types_offset + 8, + ); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Allocates an indexed array of populated `ReflectionNamedType` objects. +fn emit_reflection_named_type_array( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + emit_reflection_indexed_array(ctx, types.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object("ReflectionNamedType".to_string()), + ); + for type_metadata in types { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_named_type_object(ctx, type_metadata)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); } + Ok(()) } -/// Extracts a constant string or class-name operand from an EIR value. -fn const_string_or_class_operand( - ctx: &FunctionContext<'_>, - value: ValueId, - owner: &str, -) -> Result { - const_data_operand(ctx, value, owner, true) +/// Allocates and populates one `ReflectionNamedType` object. +fn emit_reflection_named_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionNamedTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets, name_offset) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionNamedType") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionNamedType"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + reflection_property_offset(class_info, "__name")?, + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + &[], + )?; + emit_reflection_string_property(ctx, &type_metadata.name, name_offset, name_offset + 8); + emit_reflection_owner_bool_property( + ctx, + "ReflectionNamedType", + "__allows_null", + type_metadata.allows_null, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionNamedType", + "__is_builtin", + type_metadata.is_builtin, + )?; + Ok(()) } -/// Extracts a constant string operand from an EIR value. -fn const_required_string_operand( - ctx: &FunctionContext<'_>, - value: ValueId, - owner: &str, -) -> Result { - const_data_operand(ctx, value, owner, false) +/// Allocates an indexed array for static reflection metadata. +fn emit_reflection_indexed_array(ctx: &mut FunctionContext<'_>, capacity: usize, stride: i64) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", stride); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", stride); + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_new"); } -/// Reads a `ConstStr` or optional `ConstClassName` value from the module data pool. -fn const_data_operand( - ctx: &FunctionContext<'_>, - value: ValueId, - owner: &str, - allow_class_name: bool, -) -> Result { - let value_ref = ctx - .function - .value(value) - .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; - let ValueDef::Instruction { inst, .. } = value_ref.def else { - return Err(CodegenIrError::unsupported(format!( - "{} constructor with non-literal reflection argument", - owner - ))); - }; - let inst_ref = ctx - .function - .instruction(inst) - .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; - let Some(Immediate::Data(data)) = inst_ref.immediate else { - return Err(CodegenIrError::invalid_module(format!( - "{} reflection literal missing data id", - owner - ))); - }; - match inst_ref.op { - Op::ConstStr => ctx - .module - .data - .strings - .get(data.as_raw() as usize) - .cloned() - .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw())), - Op::ConstClassName if allow_class_name => ctx - .module - .data - .class_names - .get(data.as_raw() as usize) - .cloned() - .ok_or_else(|| CodegenIrError::missing_entry("class data", data.as_raw())), - _ => Err(CodegenIrError::unsupported(format!( - "{} constructor with non-literal reflection argument", - owner - ))), +/// Appends the stacked member object to the stacked member array and leaves the array in result. +fn emit_append_reflection_member_object(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x1"); + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "rsi"); + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); + } } } -/// Writes a heap-persisted string into the current Reflection object result slot. -fn emit_reflection_string_property( - ctx: &mut FunctionContext<'_>, - value: &str, - low_offset: usize, - high_offset: usize, -) { - let (label, len) = ctx.data.add_string(value.as_bytes()); - let object_reg = abi::symbol_scratch_reg(ctx.emitter); - let result_reg = abi::int_result_reg(ctx.emitter); - abi::emit_push_reg(ctx.emitter, result_reg); +/// Allocates an indexed string array containing ReflectionClass metadata names. +fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_symbol_address(ctx.emitter, "x1", &label); - abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); - abi::emit_call_label(ctx.emitter, "__rt_str_persist"); - abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, "x1", object_reg, low_offset); - abi::emit_store_to_address(ctx.emitter, "x2", object_reg, high_offset); + abi::emit_load_int_immediate(ctx.emitter, "x0", names.len().max(1) as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", 16); } Arch::X86_64 => { - abi::emit_symbol_address(ctx.emitter, "rax", &label); - abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); - abi::emit_call_label(ctx.emitter, "__rt_str_persist"); - abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, "rax", object_reg, low_offset); - abi::emit_store_to_address(ctx.emitter, "rdx", object_reg, high_offset); + abi::emit_load_int_immediate(ctx.emitter, "rdi", names.len().max(1) as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", 16); } } - abi::emit_push_reg(ctx.emitter, object_reg); - abi::emit_pop_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_array_new"); + match ctx.emitter.target.arch { + Arch::AArch64 => emit_reflection_string_array_fill_aarch64(ctx, names), + Arch::X86_64 => emit_reflection_string_array_fill_x86_64(ctx, names), + } + Ok(()) } -/// Replaces the Reflection object's default `__attrs` array with populated metadata. -fn emit_reflection_attrs_property( +/// Appends ReflectionClass metadata names to the current ARM64 result array. +fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + for name in names { + let (label, len) = ctx.data.add_string(name.as_bytes()); + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + } + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result +} + +/// Appends ReflectionClass metadata names to the current x86_64 result array. +fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + for name in names { + let (label, len) = ctx.data.add_string(name.as_bytes()); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + } + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result +} + +/// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. +fn emit_reflection_member_flag_properties( ctx: &mut FunctionContext<'_>, class_name: &str, - attr_names: &[String], - attr_args: &[Option>], + flags: ReflectionMemberFlags, +) -> Result<()> { + match class_name { + "ReflectionMethod" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_static", flags.is_static)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_abstract", + flags.is_abstract, + )?; + } + "ReflectionProperty" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_static", flags.is_static)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_abstract", + flags.is_abstract, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_readonly", + flags.is_readonly, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_promoted", + flags.is_promoted, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_virtual", flags.is_virtual)?; + } + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + } + _ => {} + } + Ok(()) +} + +/// Stores one boolean property on the current Reflection owner object result. +fn emit_reflection_owner_bool_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + value: bool, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + emit_reflection_int_property(ctx, i64::from(value), low_offset, low_offset + 8); + Ok(()) +} + +/// Stores one integer property on the current Reflection owner object result. +fn emit_reflection_owner_int_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + value: i64, ) -> Result<()> { - let (attrs_low_offset, attrs_high_offset) = reflection_attrs_offsets(class_name); + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; let result_reg = abi::int_result_reg(ctx.emitter); - let object_reg = abi::symbol_scratch_reg(ctx.emitter); - abi::emit_push_reg(ctx.emitter, result_reg); - abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); - abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); - abi::emit_call_label(ctx.emitter, "__rt_decref_array"); - super::super::builtins::attributes::emit_reflection_attribute_array( - ctx, attr_names, attr_args, - )?; - abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); - abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); - abi::emit_store_to_address( - ctx.emitter, - abi::secondary_scratch_reg(ctx.emitter), - object_reg, - attrs_high_offset, - ); - abi::emit_push_reg(ctx.emitter, object_reg); - abi::emit_pop_reg(ctx.emitter, result_reg); + let value_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, value_reg, value); + abi::emit_store_to_address(ctx.emitter, value_reg, result_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, result_reg, high_offset); Ok(()) } -/// Returns the low/high object offsets for the private `__attrs` slot. -fn reflection_attrs_offsets(class_name: &str) -> (usize, usize) { - if class_name == "ReflectionClass" { - (24, 32) +/// Stores the current boxed Mixed result into one Reflection owner property. +fn emit_reflection_owner_mixed_property_from_result( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let value_reg = abi::int_result_reg(ctx.emitter); + let owner_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, owner_reg); + abi::emit_store_to_address(ctx.emitter, value_reg, owner_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, owner_reg, high_offset); + abi::emit_reg_move(ctx.emitter, value_reg, owner_reg); + Ok(()) +} + +/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for class metadata. +fn reflection_class_modifiers( + is_final: bool, + is_abstract: bool, + is_readonly_class: bool, + is_enum: bool, +) -> i64 { + let mut modifiers = 0; + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly_class && !is_enum { + modifiers |= 65_536; + } + modifiers +} + +/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask. +fn reflection_class_constant_modifiers(visibility: &Visibility, is_final: bool) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_final { + modifiers |= 32; + } + modifiers +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask from class metadata. +fn reflection_property_modifiers_for_info( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if info + .properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_modifiers( + visibility, + false, + info.final_properties.contains(property_name), + info.abstract_properties.contains(property_name), + info.readonly_properties.contains(property_name), + reflection_property_is_virtual(info, property_name), + info.property_set_visibilities.get(property_name), + )); + } + if info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_modifiers( + visibility, + true, + info.final_static_properties.contains(property_name), + false, + false, + false, + None, + )); + } + None +} + +/// Returns whether a property is virtual because it has or requires hooks. +fn reflection_property_is_virtual(info: &crate::types::ClassInfo, property_name: &str) -> bool { + let get_method = php_symbol_key(&property_hook_get_method(property_name)); + let set_method = php_symbol_key(&property_hook_set_method(property_name)); + info.abstract_property_hooks.contains_key(property_name) + || info.methods.contains_key(&get_method) + || info.methods.contains_key(&set_method) +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask. +fn reflection_property_modifiers( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, + set_visibility: Option<&Visibility>, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(Visibility::Private) => modifiers |= 32 | 4096, + Some(Visibility::Protected) => modifiers |= 2048, + Some(Visibility::Public) | None => { + if is_readonly && visibility == &Visibility::Public { + modifiers |= 2048; + } + } + } + modifiers +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask from predicate flags. +fn reflection_property_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { + let visibility = reflection_visibility_from_member_flags(flags); + reflection_property_modifiers( + &visibility, + flags.is_static, + flags.is_final, + flags.is_abstract, + flags.is_readonly, + flags.is_virtual, + None, + ) +} + +/// Converts retained member visibility flags back into a `Visibility` value. +fn reflection_visibility_from_member_flags(flags: ReflectionMemberFlags) -> Visibility { + if flags.is_private { + Visibility::Private + } else if flags.is_protected { + Visibility::Protected } else { - (8, 16) + Visibility::Public + } +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from method flags. +fn reflection_method_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { + let mut modifiers = 0; + if flags.is_public { + modifiers |= 1; + } + if flags.is_protected { + modifiers |= 2; + } + if flags.is_private { + modifiers |= 4; + } + if flags.is_static { + modifiers |= 16; + } + if flags.is_final { + modifiers |= 32; } + if flags.is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Returns one declared property offset from a synthetic Reflection class layout. +fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> Result { + info.property_offsets.get(property).copied().ok_or_else(|| { + CodegenIrError::invalid_module(format!( + "Reflection owner missing property offset for ${}", + property + )) + }) +} + +/// Returns the low/high object offsets for the private `__attrs` slot. +fn reflection_attrs_offsets(ctx: &FunctionContext<'_>, class_name: &str) -> Result<(usize, usize)> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let attrs_low_offset = reflection_property_offset(class_info, "__attrs")?; + Ok((attrs_low_offset, attrs_low_offset + 8)) } diff --git a/src/codegen/lower_inst/scoped_constants.rs b/src/codegen/lower_inst/scoped_constants.rs index 7cf14b16b3..49be7bfd5e 100644 --- a/src/codegen/lower_inst/scoped_constants.rs +++ b/src/codegen/lower_inst/scoped_constants.rs @@ -14,27 +14,40 @@ use crate::ir::Instruction; use crate::names::enum_case_symbol; use super::super::context::FunctionContext; -use super::{expect_data, store_if_result}; +use super::{builtins, expect_data, store_if_result}; use crate::codegen::{CodegenIrError, Result}; /// Lowers a scoped enum-case read into the current object pointer result register. -pub(super) fn lower_scoped_constant_get(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_scoped_constant_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let (enum_name, case_name) = scoped_constant_label(ctx, inst)?; - let enum_info = ctx - .module - .enum_infos - .get(enum_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("scoped constant {}::{}", enum_name, case_name)))?; - if !enum_info.cases.iter().any(|case| case.name == case_name) { - return Err(CodegenIrError::unsupported(format!( - "scoped enum constant {}::{}", - enum_name, - case_name - ))); + let class_name = enum_name.to_string(); + let constant_name = case_name.to_string(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name.as_str()) { + if enum_info + .cases + .iter() + .any(|case| case.name == constant_name.as_str()) + { + let symbol = enum_case_symbol(&class_name, &constant_name); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &symbol, + 0, + ); + return store_if_result(ctx, inst); + } + } + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_class_constant_fetch(ctx, inst, &class_name, &constant_name); } - let symbol = enum_case_symbol(enum_name, case_name); - abi::emit_load_symbol_to_reg(ctx.emitter, abi::int_result_reg(ctx.emitter), &symbol, 0); - store_if_result(ctx, inst) + Err(CodegenIrError::unsupported(format!( + "scoped constant {}::{}", + class_name, constant_name + ))) } /// Resolves the string immediate `Enum::Case` attached to a scoped constant read. diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index c5a2bc9898..7dc0ab36e1 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -8,7 +8,9 @@ //! Key details: //! - This slice supports public scalar/string/array/object static properties with //! named, lexical `self`, and lexical `parent` receivers, but not late static -//! binding, references, or non-indexed array mutation. +//! references or non-indexed array mutation. +//! - `static::` receivers use native class-id branches for generated classes and +//! the eval native-frame override when late static scope points at an eval class. //! - Typed static properties use the same high-word uninitialized sentinel as //! the emitted code before reads. @@ -21,7 +23,7 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; -use super::{expect_data, expect_operand, store_if_result}; +use super::{builtins, expect_data, expect_operand, load_value_to_first_int_arg, store_if_result}; use crate::codegen::{CodegenIrError, Result}; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; @@ -45,37 +47,126 @@ struct StaticPropertyBranch { } /// Lowers a direct static property read into the current result register(s). -pub(super) fn lower_load_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - let slot = resolve_static_property_slot(ctx, inst)?; +pub(super) fn lower_load_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + if let Some((class_name, property)) = eval_dynamic_static_property_target(ctx, inst)? { + return builtins::lower_eval_static_property_get(ctx, inst, &class_name, &property); + } + let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; + let eval_done_label = emit_eval_native_frame_static_property_get_if_needed(ctx, inst, &slot)?; if slot.late_bound && !slot.branches.is_empty() { let class_id_reg = class_id_work_reg(ctx.emitter); if emit_called_class_id_to_reg(ctx, class_id_reg)? { emit_dynamic_load_static_property_result(ctx, &slot, class_id_reg)?; - return store_if_result(ctx, inst); + store_if_result(ctx, inst)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + return Ok(()); } } emit_direct_load_static_property_result(ctx, &slot); - store_if_result(ctx, inst) + store_if_result(ctx, inst)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + Ok(()) +} + +/// Returns an eval dynamic static-property target when no AOT class owns the receiver. +fn eval_dynamic_static_property_target( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + if !builtins::has_eval_context(ctx) { + return Ok(None); + } + let label = static_property_label(ctx, inst)?; + let (receiver, property) = parse_static_property_label(label)?; + let receiver = resolve_static_property_receiver(ctx, receiver, inst)?; + if ctx.module.class_infos.contains_key(receiver.as_str()) { + return Ok(None); + } + Ok(Some((receiver, property.to_string()))) } /// Lowers a direct static property write from one SSA operand into symbol-backed storage. -pub(super) fn lower_store_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_store_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; - let slot = resolve_static_property_slot(ctx, inst)?; + if let Some((class_name, property)) = eval_dynamic_static_property_target(ctx, inst)? { + return builtins::lower_eval_static_property_set( + ctx, + inst, + value, + &class_name, + &property, + ); + } + let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; let value_ty = ctx.value_php_type(value)?; ensure_static_property_value_supported(&slot, &value_ty, inst)?; - load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; let release_previous = !value_is_same_static_property_load(ctx, value, &slot)?; + let eval_done_label = + emit_eval_native_frame_static_property_set_if_needed(ctx, inst, value, &slot)?; + load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; if slot.late_bound && !slot.branches.is_empty() { let class_id_reg = class_id_work_reg(ctx.emitter); if emit_called_class_id_to_reg(ctx, class_id_reg)? { emit_dynamic_store_static_property_result(ctx, &slot, class_id_reg, release_previous); + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } return Ok(()); } } emit_direct_store_static_property_result(ctx, &slot, release_previous); + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + Ok(()) +} + +/// Lowers a Reflection static property read, bypassing PHP member visibility. +pub(super) fn lower_load_reflection_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let slot = resolve_static_property_slot(ctx, inst, false)?; + ensure_static_property_type_supported(&slot.php_type, inst)?; + emit_direct_load_static_property_result(ctx, &slot); + store_if_result(ctx, inst) +} + +/// Lowers a Reflection static-property initialization probe. +pub(super) fn lower_reflection_static_property_initialized( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let slot = resolve_static_property_slot(ctx, inst, false)?; + emit_direct_static_property_initialized_result(ctx, &slot); + store_if_result(ctx, inst) +} + +/// Lowers a Reflection static property write, bypassing PHP member visibility. +pub(super) fn lower_store_reflection_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let value = expect_operand(inst, 0)?; + let slot = resolve_static_property_slot(ctx, inst, false)?; + ensure_static_property_type_supported(&slot.php_type, inst)?; + let value_ty = ctx.value_php_type(value)?; + ensure_static_property_value_supported(&slot, &value_ty, inst)?; + load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; + let release_previous = !value_is_same_static_property_load(ctx, value, &slot)?; + emit_direct_store_static_property_result(ctx, &slot, release_previous); Ok(()) } @@ -94,16 +185,21 @@ fn value_is_same_static_property_load( let Some(inst_ref) = ctx.function.instruction(inst) else { return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); }; - if inst_ref.op != crate::ir::Op::LoadStaticProperty { + if !matches!( + inst_ref.op, + crate::ir::Op::LoadStaticProperty | crate::ir::Op::LoadReflectionStaticProperty + ) { return Ok(false); } - Ok(resolve_static_property_slot(ctx, inst_ref)?.symbol == slot.symbol) + let enforce_visibility = inst_ref.op == crate::ir::Op::LoadStaticProperty; + Ok(resolve_static_property_slot(ctx, inst_ref, enforce_visibility)?.symbol == slot.symbol) } /// Resolves a static property immediate into declaring-class symbol metadata. fn resolve_static_property_slot( ctx: &FunctionContext<'_>, inst: &Instruction, + enforce_visibility: bool, ) -> Result { let label = static_property_label(ctx, inst)?; let (receiver, property) = parse_static_property_label(label)?; @@ -135,7 +231,9 @@ fn resolve_static_property_slot( .class_infos .get(declaring_class) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown static property declaring class {}", declaring_class)))?; - ensure_static_property_visibility(ctx, declaring_class, property, declaring_info, inst)?; + if enforce_visibility { + ensure_static_property_visibility(ctx, declaring_class, property, declaring_info, inst)?; + } let (raw_receiver, _) = parse_static_property_label(label)?; let late_bound = raw_receiver.trim_start_matches('\\') == "static"; let branches = dynamic_static_property_branches(ctx, late_bound, property, declaring_class); @@ -187,6 +285,18 @@ fn emit_direct_load_static_property_result( abi::emit_load_symbol_to_result(ctx.emitter, &slot.symbol, &slot.php_type); } +/// Emits `true` when the static-property slot is initialized. +fn emit_direct_static_property_initialized_result( + ctx: &mut FunctionContext<'_>, + slot: &StaticPropertySlot, +) { + if !slot.is_declared { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + return; + } + emit_static_property_initialized_bool(ctx, slot); +} + /// Emits a direct static property store into the fallback declaring-class symbol. fn emit_direct_store_static_property_result( ctx: &mut FunctionContext<'_>, @@ -197,6 +307,28 @@ fn emit_direct_store_static_property_result( clear_uninitialized_marker_after_static_store(ctx, &slot.symbol, &slot.php_type); } +/// Compares a typed static-property marker with the uninitialized sentinel. +fn emit_static_property_initialized_bool( + ctx: &mut FunctionContext<'_>, + slot: &StaticPropertySlot, +) { + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, marker_reg, &slot.symbol, 8); + abi::emit_load_int_immediate(ctx.emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction("cset x0, ne"); // materialize true when the static property is initialized + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction("setne al"); // materialize true when the static property is initialized + ctx.emitter.instruction("movzx rax, al"); // widen the initialization flag into the integer result register + } + } +} + /// Loads the forwarded called-class id into `dest_reg` when the current frame has it. fn emit_called_class_id_to_reg( ctx: &mut FunctionContext<'_>, @@ -210,6 +342,56 @@ fn emit_called_class_id_to_reg( Ok(true) } +/// Emits an eval late-static override read before falling back to native slots. +fn emit_eval_native_frame_static_property_get_if_needed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + slot: &StaticPropertySlot, +) -> Result> { + if !slot.late_bound || !ctx.module.required_runtime_features.eval_bridge { + return Ok(None); + } + let frame_class = super::current_method_class(ctx)?.to_string(); + let no_override_label = ctx.next_label("eval_late_static_prop_get_no_override"); + let done_label = ctx.next_label("eval_late_static_prop_get_done"); + builtins::lower_eval_native_frame_static_property_get( + ctx, + inst, + &frame_class, + &slot.property, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Ok(Some(done_label)) +} + +/// Emits an eval late-static override write before falling back to native slots. +fn emit_eval_native_frame_static_property_set_if_needed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + slot: &StaticPropertySlot, +) -> Result> { + if !slot.late_bound || !ctx.module.required_runtime_features.eval_bridge { + return Ok(None); + } + let frame_class = super::current_method_class(ctx)?.to_string(); + let no_override_label = ctx.next_label("eval_late_static_prop_set_no_override"); + let done_label = ctx.next_label("eval_late_static_prop_set_done"); + builtins::lower_eval_native_frame_static_property_set( + ctx, + inst, + value, + &frame_class, + &slot.property, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Ok(Some(done_label)) +} + /// Emits a late-bound static property read selected by the runtime called-class id. fn emit_dynamic_load_static_property_result( ctx: &mut FunctionContext<'_>, @@ -526,6 +708,9 @@ fn ensure_static_property_value_supported( if is_empty_array_for_array_static_property(value_ty, &slot.php_type) { return Ok(()); } + if can_coerce_mixed_to_scalar_static_property(value_ty, &slot.php_type) { + return Ok(()); + } Err(CodegenIrError::unsupported(format!( "{} assigning PHP type {:?} to {}::${} with PHP type {:?}", inst.op.name(), @@ -547,6 +732,15 @@ fn is_empty_array_for_array_static_property(value_ty: &PhpType, slot_ty: &PhpTyp matches!(value_elem.codegen_repr(), PhpType::Never | PhpType::Void) } +/// Returns true when a boxed Mixed value can be coerced before a scalar static-property store. +fn can_coerce_mixed_to_scalar_static_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + && matches!( + slot_ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) +} + /// Returns true when a value can materialize the inline nullable-int static-property shape. fn can_store_value_as_tagged_scalar_static_property( value_ty: &PhpType, @@ -600,6 +794,20 @@ fn load_static_property_store_value_to_result( } return Ok(()); } + if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + load_value_to_first_int_arg(ctx, value)?; + match slot_ty.codegen_repr() { + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + } + PhpType::Int => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"), + PhpType::Bool => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"), + PhpType::Float => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"), + _ => {} + } + return Ok(()); + } ctx.load_value_to_result(value)?; if matches!(slot_ty.codegen_repr(), PhpType::Int) && matches!(value_ty.codegen_repr(), PhpType::Mixed) diff --git a/src/codegen/lower_inst/strings.rs b/src/codegen/lower_inst/strings.rs index 0725c7caab..3d1896ea90 100644 --- a/src/codegen/lower_inst/strings.rs +++ b/src/codegen/lower_inst/strings.rs @@ -66,6 +66,7 @@ fn lower_late_static_class_name(ctx: &mut FunctionContext<'_>) -> Result<()> { fn lower_late_static_class_name_arm64(ctx: &mut FunctionContext<'_>) -> Result<()> { let missing = ctx.next_label("static_class_missing"); let done = ctx.next_label("static_class_done"); + emit_eval_native_frame_called_class_override_probe(ctx, &done); emit_late_static_class_id_to_reg(ctx, "x12")?; abi::emit_load_symbol_to_reg(ctx.emitter, "x10", "_class_name_count", 0); ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the emitted class-name table @@ -87,6 +88,7 @@ fn lower_late_static_class_name_arm64(ctx: &mut FunctionContext<'_>) -> Result<( fn lower_late_static_class_name_x86_64(ctx: &mut FunctionContext<'_>) -> Result<()> { let missing = ctx.next_label("static_class_missing"); let done = ctx.next_label("static_class_done"); + emit_eval_native_frame_called_class_override_probe(ctx, &done); emit_late_static_class_id_to_reg(ctx, "r8")?; abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_class_name_count", 0); ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the emitted class-name table @@ -104,6 +106,92 @@ fn lower_late_static_class_name_x86_64(ctx: &mut FunctionContext<'_>) -> Result< Ok(()) } +/// Probes eval's thread-local called-class override before falling back to AOT class ids. +fn emit_eval_native_frame_called_class_override_probe( + ctx: &mut FunctionContext<'_>, + done_label: &str, +) { + if !ctx.module.required_runtime_features.eval_bridge { + return; + } + let Some(frame_class) = current_late_static_frame_class(ctx).map(str::to_string) else { + return; + }; + match ctx.emitter.target.arch { + Arch::AArch64 => { + emit_eval_native_frame_called_class_override_probe_aarch64(ctx, &frame_class, done_label); + } + Arch::X86_64 => { + emit_eval_native_frame_called_class_override_probe_x86_64(ctx, &frame_class, done_label); + } + } +} + +/// Emits the AArch64 eval override probe for one generated/AOT method frame. +fn emit_eval_native_frame_called_class_override_probe_aarch64( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + done_label: &str, +) { + let no_override = ctx.next_label("static_class_no_eval_override"); + let (class_label, class_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + abi::emit_symbol_address(ctx.emitter, "x0", &class_label); + abi::emit_load_int_immediate(ctx.emitter, "x1", class_len as i64); + abi::emit_temporary_stack_address(ctx.emitter, "x2", 0); + abi::emit_temporary_stack_address(ctx.emitter, "x3", 8); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + ctx.emitter.instruction("cmp x0, #0"); // check whether eval has a late-static class override for this AOT frame + ctx.emitter.instruction(&format!("b.eq {}", no_override)); // fall back to the emitted class-id metadata when no override is active + ctx.emitter.instruction("ldr x1, [sp]"); // load the eval called-class name pointer into the string result + ctx.emitter.instruction("ldr x2, [sp, #8]"); // load the eval called-class name length into the string result + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the AOT class-id metadata path after using the eval override + ctx.emitter.label(&no_override); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Emits the x86_64 eval override probe for one generated/AOT method frame. +fn emit_eval_native_frame_called_class_override_probe_x86_64( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + done_label: &str, +) { + let no_override = ctx.next_label("static_class_no_eval_override"); + let (class_label, class_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + abi::emit_symbol_address(ctx.emitter, "rdi", &class_label); + abi::emit_load_int_immediate(ctx.emitter, "rsi", class_len as i64); + abi::emit_temporary_stack_address(ctx.emitter, "rdx", 0); + abi::emit_temporary_stack_address(ctx.emitter, "rcx", 8); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + ctx.emitter.instruction("cmp rax, 0"); // check whether eval has a late-static class override for this AOT frame + ctx.emitter.instruction(&format!("je {}", no_override)); // fall back to the emitted class-id metadata when no override is active + ctx.emitter.instruction("mov rax, QWORD PTR [rsp]"); // load the eval called-class name pointer into the string result + ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // load the eval called-class name length into the string result + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the AOT class-id metadata path after using the eval override + ctx.emitter.label(&no_override); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Returns the generated/AOT class encoded in the current method frame name. +fn current_late_static_frame_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .flags + .is_method + .then(|| ctx.function.name.rsplit_once("::").map(|(class_name, _)| class_name)) + .flatten() +} + /// Loads the late-static class id from the hidden static frame slot or `$this`. fn emit_late_static_class_id_to_reg(ctx: &mut FunctionContext<'_>, reg: &str) -> Result<()> { if let Some(slot) = ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM) { diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index c91bbeb39a..2c50001126 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,15 @@ mod block_emit; pub(crate) mod context; +mod eval_callable_helpers; +mod eval_class_constant_helpers; +mod eval_constructor_helpers; +mod eval_method_helpers; +mod eval_property_helpers; +mod eval_ref_arg_helpers; +mod eval_reflection_helpers; +mod eval_reflection_owner_helpers; +mod eval_static_property_helpers; mod fibers; mod frame; mod function_variants; @@ -187,17 +196,57 @@ pub fn generate_user_asm_from_ir_with_options( fn finalize_user_asm( module: &Module, mut emitter: Emitter, - data: DataSection, + mut data: DataSection, emit: Emit, exported_functions: &HashMap, ) -> String { + let eval_bridge = module.required_runtime_features.eval_bridge; + let emit_eval_reflection_metadata = + eval_bridge || module.required_runtime_features.eval_scope; + if eval_bridge { + eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); + eval_static_property_helpers::emit_eval_static_property_helpers( + module, + &mut emitter, + &mut data, + ); + eval_class_constant_helpers::emit_eval_class_constant_helpers( + module, + &mut emitter, + &mut data, + ); + } + let eval_callable_support_needed = + eval_bridge && eval_callable_helpers::module_needs_eval_callable_descriptor_support(module); + let eval_callable_support = eval_callable_helpers::emit_eval_callable_descriptor_support( + module, + &mut emitter, + &mut data, + eval_callable_support_needed, + ); + if eval_bridge { + eval_constructor_helpers::emit_eval_constructor_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); + eval_method_helpers::emit_eval_method_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); + eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); + eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); + } let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); let user_functions = runtime_user_function_sigs(module); let function_variant_groups = runtime_function_variant_groups(module); let mut allowed_class_names = runtime_referenced_class_names(module); - if module_uses_dynamic_callable_lookup(module) { + if module_uses_dynamic_callable_lookup(module) || module.required_runtime_features.eval_bridge { allowed_class_names.extend(module.class_infos.keys().cloned()); } let runtime_interfaces = runtime_referenced_interfaces(module, &allowed_class_names); @@ -220,9 +269,22 @@ fn finalize_user_asm( &user_functions, &function_variant_groups, &runtime_interfaces, + &module.declared_interface_names, + &module.trait_table.names, + &module.declared_trait_uses, + &module.declared_trait_source_lines, &runtime_classes, &module.enum_infos, Some(&allowed_class_names), + emit_eval_reflection_metadata, + // The source path feeds eval Reflection source-location hooks only; + // embedding it in native-only programs leaks the build path into the + // assembly (and trips needle-based optimizer asm asserts). + if emit_eval_reflection_metadata { + module.source_path.as_deref() + } else { + None + }, ); let mut user_asm = emitter.output(); @@ -284,6 +346,8 @@ fn ir_function_sig(function: &Function) -> FunctionSig { .iter() .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; function.params.len()], + param_attributes: vec![Vec::new(); function.params.len()], defaults: vec![None; function.params.len()], return_type: function.return_php_type.clone(), declared_return: false, @@ -449,6 +513,12 @@ fn intrinsic_method_wrapper_specs(module: &Module) -> Vec HashMap { let emitted_methods = emitted_class_method_keys(module); + let emitted_property_init_thunks = module + .functions + .iter() + .filter(|function| is_property_init_thunk_function(function)) + .map(|function| function.name.as_str()) + .collect::>(); let mut classes = module.class_infos.clone(); for class_info in classes.values_mut() { class_info @@ -461,6 +531,10 @@ fn runtime_class_infos(module: &Module) -> HashMap { .retain(|method_name, impl_class| { emitted_methods.contains(&(impl_class.clone(), method_name.clone(), true)) }); + let property_init_thunk = format!("_class_propinit_{}", class_info.class_id); + if !emitted_property_init_thunks.contains(property_init_thunk.as_str()) { + class_info.defaults.fill(None); + } } classes } @@ -510,11 +584,222 @@ fn runtime_referenced_class_names(module: &Module) -> HashSet { } } seed_runtime_throwable_class_names(module, &mut names); + seed_runtime_stdclass_name(module, &mut names); seed_builtin_reflection_class_names(module, &mut names); expand_class_dependencies(&mut names, &module.class_infos); names } +/// Returns enum names whose singleton case slots must be allocated before main runs. +fn runtime_referenced_enum_singleton_names(module: &Module) -> HashSet { + let mut names = HashSet::new(); + for function in all_runtime_scanned_functions(module) { + for inst in &function.instructions { + collect_scoped_constant_enum_singleton_name(module, inst, &mut names); + collect_static_method_enum_singleton_name(module, function, inst, &mut names); + collect_reflection_enum_singleton_name(module, function, inst, &mut names); + } + } + seed_eval_visible_enum_singleton_names(module, &mut names); + names +} + +/// Iterates all function-like EIR bodies considered by runtime metadata scanners. +fn all_runtime_scanned_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Adds every enum when `eval` can dynamically fetch AOT enum cases by name. +fn seed_eval_visible_enum_singleton_names(module: &Module, names: &mut HashSet) { + if !module_contains_eval_state(module) { + return; + } + names.extend(module.enum_infos.keys().cloned()); +} + +/// Returns true when any scanned function owns persistent eval runtime state. +fn module_contains_eval_state(module: &Module) -> bool { + all_runtime_scanned_functions(module).any(|function| { + function.locals.iter().any(|local| { + matches!( + local.kind, + crate::ir::LocalKind::EvalContext + | crate::ir::LocalKind::EvalScope + | crate::ir::LocalKind::EvalGlobalScope + ) + }) + }) +} + +/// Adds enum names referenced by `Enum::Case` scoped constant reads. +fn collect_scoped_constant_enum_singleton_name( + module: &Module, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::ScopedConstantGet) { + return; + } + let Some(label) = data_string_immediate(module, inst) else { + return; + }; + let Some((class_name, case_name)) = label.rsplit_once("::") else { + return; + }; + let Some(enum_name) = canonical_module_enum_name(module, class_name) else { + return; + }; + if module + .enum_infos + .get(&enum_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == case_name)) + { + names.insert(enum_name); + } +} + +/// Adds enum names referenced through `cases()`, `from()`, or `tryFrom()`. +fn collect_static_method_enum_singleton_name( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::StaticMethodCall) { + return; + } + let Some(label) = data_string_immediate(module, inst) else { + return; + }; + let Some((receiver, method_name)) = label.rsplit_once("::") else { + return; + }; + let Some(receiver) = resolve_static_method_metadata_class(module, function, receiver) else { + return; + }; + let Some(enum_name) = canonical_module_enum_name(module, &receiver) else { + return; + }; + if enum_static_method_needs_singletons(method_name) { + names.insert(enum_name); + } +} + +/// Adds enum names whose case values can be materialized by known Reflection objects. +fn collect_reflection_enum_singleton_name( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::ObjectNew) || inst.operands.is_empty() { + return; + } + let Some(class_name) = class_name_immediate(module, inst) else { + return; + }; + if !reflection_constructor_can_materialize_enum_case(class_name) { + return; + } + let Some(reflected_name) = const_class_like_name_value(module, function, inst.operands[0]) + else { + return; + }; + if let Some(enum_name) = canonical_module_enum_name(module, reflected_name) { + names.insert(enum_name); + } +} + +/// Returns true for enum static helpers that load one or more singleton case objects. +fn enum_static_method_needs_singletons(method_name: &str) -> bool { + matches!( + php_symbol_key(method_name).as_str(), + "cases" | "from" | "tryfrom" + ) +} + +/// Returns true for Reflection constructors whose methods can expose enum case values. +fn reflection_constructor_can_materialize_enum_case(class_name: &str) -> bool { + matches!( + php_symbol_key(class_name.trim_start_matches('\\')).as_str(), + "reflectionclass" + | "reflectionclassconstant" + | "reflectionenumunitcase" + | "reflectionenumbackedcase" + ) +} + +/// Returns a data-string immediate attached to an EIR instruction. +fn data_string_immediate<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module + .data + .strings + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Returns a class-name immediate attached to an EIR instruction. +fn class_name_immediate<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module + .data + .class_names + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Returns a compile-time class-like name from a string or `::class` value. +fn const_class_like_name_value<'a>( + module: &'a Module, + function: &'a Function, + value: crate::ir::ValueId, +) -> Option<&'a str> { + let value_ref = function.value(value)?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return None; + }; + let inst_ref = function.instruction(inst)?; + let Some(Immediate::Data(data)) = inst_ref.immediate else { + return None; + }; + match inst_ref.op { + Op::ConstClassName => module + .data + .class_names + .get(data.as_raw() as usize) + .map(String::as_str), + Op::ConstStr => module + .data + .strings + .get(data.as_raw() as usize) + .map(String::as_str), + _ => None, + } +} + +/// Resolves an enum name against module metadata using PHP case-insensitive rules. +fn canonical_module_enum_name(module: &Module, enum_name: &str) -> Option { + let wanted = php_symbol_key(enum_name.trim_start_matches('\\')); + module + .enum_infos + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == wanted) + .cloned() +} + /// Adds builtin throwable classes that runtime helpers can materialize without EIR class references. fn seed_runtime_throwable_class_names(module: &Module, names: &mut HashSet) { if names.contains("Fiber") && module.class_infos.contains_key("FiberError") { @@ -529,6 +814,7 @@ fn seed_runtime_throwable_class_names(module: &Module, names: &mut HashSet) { + if module.class_infos.contains_key("stdClass") { + names.insert("stdClass".to_string()); + } +} + /// Adds builtin reflection classes whose objects can be materialized by metadata helpers. fn seed_builtin_reflection_class_names(module: &Module, names: &mut HashSet) { for class_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", "ReflectionMethod", "ReflectionProperty", "ReflectionFunction", "ReflectionParameter", "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", ] { - if module.class_infos.contains_key(class_name) { + let emitted_natively = module.class_methods.iter().any(|function| { + current_function_class(function) + .is_some_and(|owner| php_symbol_key(owner) == php_symbol_key(class_name)) + }); + if module.class_infos.contains_key(class_name) + && (module.required_runtime_features.eval_bridge || emitted_natively) + { names.insert(class_name.to_string()); } } @@ -577,6 +883,9 @@ fn runtime_referenced_interfaces( class_names: &HashSet, ) -> HashMap { let mut names = HashSet::new(); + if module.required_runtime_features.eval_bridge { + names.extend(module.interface_infos.keys().cloned()); + } if module_uses_dynamic_instanceof(module) { names.extend(dynamic_instanceof_interface_names(module)); } @@ -782,7 +1091,14 @@ fn referenced_static_property_class_names(module: &Module) -> HashSet { .chain(module.runtime_callable_invokers.iter()) { for inst in &function.instructions { - if !matches!(inst.op, Op::LoadStaticProperty | Op::StoreStaticProperty) { + if !matches!( + inst.op, + Op::LoadStaticProperty + | Op::StoreStaticProperty + | Op::LoadReflectionStaticProperty + | Op::ReflectionStaticPropertyInitialized + | Op::StoreReflectionStaticProperty + ) { continue; } let Some(Immediate::Data(data)) = inst.immediate else { diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index ab96d0baed..a1c6f43cfe 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -20,12 +20,18 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::{abi, emit_box_current_value_as_mixed, emit_box_runtime_payload_as_mixed}; +use crate::codegen_support::try_handlers::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; use crate::parser::ast::{Expr, ExprKind}; use crate::types::{FunctionSig, PhpType}; const INVOKER_DESCRIPTOR_OFFSET: usize = 8; const INVOKER_CONCAT_OFFSET: usize = 16; const INVOKER_FRAME_SIZE: usize = 32; +const INVOKER_ARG_ARRAY_OFFSET: usize = 24; +const INVOKER_BOUNDARY_FRAME_SIZE: usize = INVOKER_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE + 16; +const INVOKER_BOUNDARY_BASE_OFFSET: usize = INVOKER_BOUNDARY_FRAME_SIZE - 16; /// Runtime invoker metadata emitted beside callable descriptors. pub(super) struct RuntimeCallableInvoker<'a> { @@ -76,21 +82,65 @@ pub(super) fn emit_runtime_callable_invoker( emitter: &mut Emitter, data: &mut DataSection, invoker: &RuntimeCallableInvoker<'_>, +) { + emit_runtime_callable_invoker_impl(emitter, data, invoker, false); +} + +/// Emits a descriptor invoker wrapper that catches native throws for eval callbacks. +pub(crate) fn emit_runtime_callable_invoker_with_exception_boundary( + emitter: &mut Emitter, + data: &mut DataSection, + invoker: &RuntimeCallableInvoker<'_>, +) { + emit_runtime_callable_invoker_impl(emitter, data, invoker, true); +} + +/// Emits a descriptor invoker wrapper, optionally bounded by an exception handler. +fn emit_runtime_callable_invoker_impl( + emitter: &mut Emitter, + data: &mut DataSection, + invoker: &RuntimeCallableInvoker<'_>, + catch_native_throws: bool, ) { let mut ctx = InvokerEmitContext::new(invoker.label); let call_reg = abi::nested_call_reg(emitter); + let escape_label = format!("{}_eval_escape", invoker.label); + let frame_size = if catch_native_throws { + INVOKER_BOUNDARY_FRAME_SIZE + } else { + INVOKER_FRAME_SIZE + }; emitter.blank(); emitter.comment(&format!("runtime callable invoker {}", invoker.label)); emitter.raw(".align 2"); emitter.label_global(invoker.label); - abi::emit_frame_prologue(emitter, INVOKER_FRAME_SIZE); + abi::emit_frame_prologue(emitter, frame_size); abi::store_at_offset( emitter, abi::int_arg_reg_name(emitter.target, 0), INVOKER_DESCRIPTOR_OFFSET, ); - emit_descriptor_entry_to_call_reg(emitter, call_reg); + if catch_native_throws { + abi::store_at_offset( + emitter, + abi::int_arg_reg_name(emitter.target, 1), + INVOKER_ARG_ARRAY_OFFSET, + ); + emit_invoker_exception_boundary_push( + emitter, + INVOKER_BOUNDARY_BASE_OFFSET, + &escape_label, + ); + abi::load_at_offset( + emitter, + abi::int_arg_reg_name(emitter.target, 1), + INVOKER_ARG_ARRAY_OFFSET, + ); + emit_saved_descriptor_entry_to_call_reg(emitter, call_reg); + } else { + emit_descriptor_entry_to_call_reg(emitter, call_reg); + } let ret_ty = emit_loaded_array_callback_call( LoadedArraySource::ArgumentRegister(1), @@ -103,8 +153,18 @@ pub(super) fn emit_runtime_callable_invoker( data, ); emit_box_current_value_as_mixed(emitter, &ret_ty.codegen_repr()); - abi::emit_frame_restore(emitter, INVOKER_FRAME_SIZE); + if catch_native_throws { + emit_invoker_exception_boundary_pop(emitter, INVOKER_BOUNDARY_BASE_OFFSET); + } + abi::emit_frame_restore(emitter, frame_size); abi::emit_return(emitter); + if catch_native_throws { + emitter.label(&escape_label); + emit_invoker_exception_boundary_pop(emitter, INVOKER_BOUNDARY_BASE_OFFSET); + emit_null_invoker_result(emitter); + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); + } } /// Loads the descriptor entry slot from the first invoker argument into `call_reg`. @@ -120,6 +180,99 @@ fn emit_descriptor_entry_to_call_reg(emitter: &mut Emitter, call_reg: &str) { callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); } +/// Loads the saved descriptor entry slot into `call_reg` after a `setjmp` boundary. +fn emit_saved_descriptor_entry_to_call_reg(emitter: &mut Emitter, call_reg: &str) { + abi::load_at_offset(emitter, call_reg, INVOKER_DESCRIPTOR_OFFSET); + callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); +} + +/// Pushes a native exception boundary around an eval-owned descriptor invoker call. +fn emit_invoker_exception_boundary_push( + emitter: &mut Emitter, + handler_base: usize, + escape_label: &str, +) { + emitter.comment("push eval callable exception boundary"); + match emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("stur x10, [x29, #-{}]", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("stur x10, [x29, #-{}]", handler_base - 8)); // preserve the caller activation frame across callable unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "stur x10, [x29, #-{}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("sub x10, x29, #{}", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "sub x0, x29, #{}", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering the callable + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a callable Throwable escaped + } + Arch::X86_64 => { + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); // preserve the caller activation frame across callable unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering the callable + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a callable Throwable escaped + } + } +} + +/// Pops the native exception boundary around an eval-owned descriptor invoker call. +fn emit_invoker_exception_boundary_pop(emitter: &mut Emitter, handler_base: usize) { + emitter.comment("pop eval callable exception boundary"); + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction(&format!("ldur x10, [x29, #-{}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldur x10, [x29, #-{}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); + } + Arch::X86_64 => { + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); + } + } +} + +/// Leaves a null boxed-Mixed result for Rust to translate into a pending throwable. +fn emit_null_invoker_result(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // return null so magician takes the pending Throwable + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // return null so magician takes the pending Throwable + } + } +} + /// Source location for a callback argument array already materialized by caller code. #[derive(Clone, Copy)] enum LoadedArraySource { @@ -423,7 +576,13 @@ fn emit_loaded_indexed_array_callback_call( abi::emit_jump(emitter, &loop_label); emitter.label(&loop_done_label); emitter.label(&done_label); - arg_types.push(PhpType::Array(Box::new(variadic_elem_ty))); + let variadic_ty = PhpType::Array(Box::new(variadic_elem_ty)); + if variadic_param_is_by_ref(sig) { + wrap_pushed_value_in_ref_cell(emitter, &variadic_ty); + arg_types.push(PhpType::Int); + } else { + arg_types.push(variadic_ty); + } } push_descriptor_captures_as_hidden_args(captures, emitter, &mut arg_types); @@ -528,7 +687,12 @@ fn emit_loaded_assoc_array_callback_call( ctx, data, ); - arg_types.push(variadic_ty); + if variadic_param_is_by_ref(sig) { + wrap_pushed_value_in_ref_cell(emitter, &variadic_ty); + arg_types.push(PhpType::Int); + } else { + arg_types.push(variadic_ty); + } } push_descriptor_captures_as_hidden_args(captures, emitter, &mut arg_types); @@ -553,6 +717,16 @@ fn callback_arg_target_ty<'a>( } } +/// Returns whether the visible variadic parameter is declared by-reference. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + sig.variadic.is_some() + && sig + .ref_params + .get(sig.params.len().saturating_sub(1)) + .copied() + .unwrap_or(false) +} + /// Returns the declared target PHP type for a parameter. fn declared_target_ty<'a>(sig: Option<&'a FunctionSig>, param_idx: usize) -> Option<&'a PhpType> { sig.and_then(|sig| { @@ -1020,6 +1194,20 @@ fn push_current_result_ref_arg_address( PhpType::Int } +/// Wraps the value currently on top of the invoker stack in a heap reference cell. +fn wrap_pushed_value_in_ref_cell(emitter: &mut Emitter, val_ty: &PhpType) { + abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 16); + abi::emit_call_label(emitter, "__rt_heap_alloc"); + let cell_reg = abi::symbol_scratch_reg(emitter); + emitter.instruction(&format!( + "mov {}, {}", + cell_reg, + abi::int_result_reg(emitter) + )); + store_pushed_value_to_ref_cell(emitter, cell_reg, val_ty); + abi::emit_push_reg(emitter, cell_reg); +} + /// Stores a just-pushed value into a heap reference cell. fn store_pushed_value_to_ref_cell(emitter: &mut Emitter, cell_reg: &str, val_ty: &PhpType) { let temp_reg = abi::temp_int_reg(emitter.target); diff --git a/src/codegen_support/abi/calls/mod.rs b/src/codegen_support/abi/calls/mod.rs index 6d2cd8b299..60e968a233 100644 --- a/src/codegen_support/abi/calls/mod.rs +++ b/src/codegen_support/abi/calls/mod.rs @@ -15,7 +15,10 @@ mod stack; pub use incoming::emit_store_incoming_param; pub use invoke::{emit_call_label, emit_call_reg}; -pub use outgoing::{build_outgoing_arg_assignments_for_target, materialize_outgoing_args}; +pub use outgoing::{ + build_outgoing_arg_assignments_for_target, materialize_outgoing_args, + outgoing_call_stack_pad_bytes, +}; pub use stack::{ emit_load_temporary_stack_slot, emit_pop_float_reg, emit_pop_reg, emit_pop_reg_pair, emit_push_float_reg, emit_push_reg, emit_push_reg_pair, emit_push_result_value, diff --git a/src/codegen_support/abi/calls/outgoing.rs b/src/codegen_support/abi/calls/outgoing.rs index a693daa8b5..0b406022aa 100644 --- a/src/codegen_support/abi/calls/outgoing.rs +++ b/src/codegen_support/abi/calls/outgoing.rs @@ -255,3 +255,16 @@ pub fn materialize_outgoing_args( overflow_bytes } + +/// Returns the temporary caller-stack padding needed after outgoing stack args are materialized. +/// +/// AArch64 callees read the first spilled incoming argument at `x29+32`, which leaves a +/// 16-byte gap above the outgoing stack-argument area after the callee saves `x29/x30`. +/// x86_64 already gets the corresponding gap from `call` pushing the return address and the +/// callee saving `rbp`. +pub fn outgoing_call_stack_pad_bytes(target: Target, overflow_bytes: usize) -> usize { + match target.arch { + Arch::AArch64 if overflow_bytes > 0 => 16, + _ => 0, + } +} diff --git a/src/codegen_support/abi/mod.rs b/src/codegen_support/abi/mod.rs index 0d425ed45b..33e3a4851e 100644 --- a/src/codegen_support/abi/mod.rs +++ b/src/codegen_support/abi/mod.rs @@ -29,6 +29,7 @@ pub use calls::{ emit_push_float_reg, emit_push_reg, emit_push_reg_pair, emit_push_result_value, emit_release_temporary_stack, emit_reserve_temporary_stack, emit_store_incoming_param, emit_store_to_sp, emit_temporary_stack_address, materialize_outgoing_args, + outgoing_call_stack_pad_bytes, }; pub use frame::{ emit_frame_prologue, emit_frame_restore, emit_frame_slot_address, emit_load_from_address, diff --git a/src/codegen_support/callable_descriptor.rs b/src/codegen_support/callable_descriptor.rs index fd1725c4ff..63eea5e4dd 100644 --- a/src/codegen_support/callable_descriptor.rs +++ b/src/codegen_support/callable_descriptor.rs @@ -24,11 +24,17 @@ use crate::types::{FunctionSig, PhpType}; pub(crate) const CALLABLE_DESC_KIND_CLOSURE: u64 = CallableDescriptorShape::Closure as u64; pub(crate) const CALLABLE_DESC_KIND_FIRST_CLASS: u64 = CallableDescriptorShape::FirstClass as u64; +pub(crate) const CALLABLE_DESC_KIND_CALLBACK_ADAPTER: u64 = + CallableDescriptorShape::CallbackAdapter as u64; +pub(crate) const CALLABLE_DESC_KIND_OBJECT_INVOKE: u64 = + CallableDescriptorShape::ObjectInvoke as u64; pub(crate) const CALLABLE_DESC_KIND_FUNCTION: u64 = CallableDescriptorShape::Function as u64; pub(crate) const CALLABLE_DESC_KIND_BUILTIN: u64 = CallableDescriptorShape::Builtin as u64; pub(crate) const CALLABLE_DESC_KIND_EXTERN: u64 = CallableDescriptorShape::Extern as u64; pub(crate) const CALLABLE_DESC_KIND_STATIC_METHOD: u64 = CallableDescriptorShape::StaticMethod as u64; +pub(crate) const CALLABLE_DESC_KIND_INSTANCE_METHOD: u64 = + CallableDescriptorShape::InstanceMethod as u64; pub(crate) const CALLABLE_DESC_ENTRY_OFFSET: usize = 8; #[allow(dead_code)] @@ -586,6 +592,8 @@ mod tests { ("label".to_string(), PhpType::Str), ("rest".to_string(), PhpType::Array(Box::new(PhpType::Mixed))), ], + param_type_exprs: vec![None, None, None], + param_attributes: Vec::new(), defaults: vec![ None, Some(Expr::new(ExprKind::StringLiteral("fallback".to_string()), Span::dummy())), @@ -634,6 +642,8 @@ mod tests { let mut data = DataSection::new(); let sig = FunctionSig { params: vec![("value".to_string(), PhpType::Int)], + param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Int, declared_return: false, diff --git a/src/codegen_support/callable_dispatch.rs b/src/codegen_support/callable_dispatch.rs index 4e47bd02eb..4afc9173a5 100644 --- a/src/codegen_support/callable_dispatch.rs +++ b/src/codegen_support/callable_dispatch.rs @@ -113,6 +113,9 @@ pub(crate) fn runtime_builtin_wrapper_supported( "strtolower" | "strtoupper" | "trim" => source_arg_ty.is_none_or(|source_arg_ty| { matches!(source_arg_ty, PhpType::Str) }), + // Type predicates take a Mixed wrapper param; concrete source types + // fold to a static boolean in the predicate lowering. + "is_array" | "is_integer" => true, "gettype" => true, _ => false, } @@ -127,6 +130,8 @@ fn runtime_builtin_name_supported(name: &str) -> bool { | "floatval" | "gettype" | "intval" + | "is_array" + | "is_integer" | "strlen" | "strtolower" | "strtoupper" diff --git a/src/codegen_support/data_section.rs b/src/codegen_support/data_section.rs index 05a27a5df6..2311aff074 100644 --- a/src/codegen_support/data_section.rs +++ b/src/codegen_support/data_section.rs @@ -131,6 +131,11 @@ impl DataSection { label } + /// Returns true when common storage has been declared for `label`. + pub fn has_comm(&self, label: &str) -> bool { + self.comm_dedup.contains_key(label) + } + /// Adds words to the current runtime or metadata collection. pub fn add_words(&mut self, words: Vec) -> String { if let Some(label) = self.word_dedup.get(&words) { diff --git a/src/codegen_support/program_usage/required_classes/collect.rs b/src/codegen_support/program_usage/required_classes/collect.rs index f42f6317bd..1b79caef13 100644 --- a/src/codegen_support/program_usage/required_classes/collect.rs +++ b/src/codegen_support/program_usage/required_classes/collect.rs @@ -287,6 +287,7 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet | ExprKind::Throw(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) + | ExprKind::Clone(expr) | ExprKind::Spread(expr) | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } => collect_required_class_names_in_expr(expr, names), @@ -425,6 +426,17 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet collect_required_class_names_in_expr(arg, names); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_required_class_names_in_expr(object, names); + collect_required_class_names_in_expr(method, names); + for arg in args { + collect_required_class_names_in_expr(arg, names); + } + } ExprKind::StaticMethodCall { receiver, args, .. } => { if let crate::parser::ast::StaticReceiver::Named(name) = receiver { names.insert(name.as_str().to_string()); diff --git a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs index 1a5498843b..c2896b238f 100644 --- a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs +++ b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs @@ -158,6 +158,7 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool { | ExprKind::Throw(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) + | ExprKind::Clone(expr) | ExprKind::Spread(expr) | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } @@ -241,6 +242,15 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_dynamic_instanceof(object) || args.iter().any(expr_has_dynamic_instanceof) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_dynamic_instanceof(object) + || expr_has_dynamic_instanceof(method) + || args.iter().any(expr_has_dynamic_instanceof) + } ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. diff --git a/src/codegen_support/reflection.rs b/src/codegen_support/reflection.rs index c362247fff..636b0e0805 100644 --- a/src/codegen_support/reflection.rs +++ b/src/codegen_support/reflection.rs @@ -16,6 +16,10 @@ use crate::names::{php_symbol_key, Name}; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Stmt, StmtKind}; use crate::types::{AttrArgEntry, AttrArgValue, AttrKey, ClassInfo}; +/// Borrowed attribute-name/argument metadata from a reflection-visible source. +pub(crate) type AttributeMetadataSource<'a> = + (&'a [String], &'a [Option>]); + #[derive(Clone)] /// Factory record for compile-time reflection attribute metadata. /// `id` is assigned sequentially and must match across all compilation units @@ -46,10 +50,20 @@ pub(crate) fn resolve_class_name<'a>( } /// Scans every class in `classes` and collects all distinct class-level, -/// method-level, and property-level attribute name/argument pairs into a -/// sorted vector of `ReflectionAttributeFactory` records with sequential ids. +/// method-level, property-level, and constant-level attribute name/argument +/// pairs into a sorted vector of `ReflectionAttributeFactory` records with +/// sequential ids. pub(crate) fn collect_attribute_factories( classes: &HashMap, +) -> Vec { + collect_attribute_factories_with_extra(classes, &[]) +} + +/// Scans class metadata plus additional attribute metadata sources and collects +/// all distinct attribute name/argument pairs into deterministic factory records. +pub(crate) fn collect_attribute_factories_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], ) -> Vec { let mut unique = BTreeMap::new(); for class_info in classes.values() { @@ -69,6 +83,14 @@ pub(crate) fn collect_attribute_factories( collect_from_attribute_lists(classes, names, args, &mut unique); } } + for (member, names) in &class_info.constant_attribute_names { + if let Some(args) = class_info.constant_attribute_args.get(member) { + collect_from_attribute_lists(classes, names, args, &mut unique); + } + } + } + for (names, args) in extra_attrs { + collect_from_attribute_lists(classes, names, args, &mut unique); } unique @@ -85,11 +107,11 @@ pub(crate) fn collect_attribute_factories( .collect() } -/// Returns the factory id for the given attribute `attr_name` with -/// `attr_args`. Returns 0 if the class cannot be resolved or no matching -/// factory exists. -pub(crate) fn attribute_factory_id( +/// Returns the factory id for an attribute, considering classes plus extra +/// metadata sources such as top-level function attributes retained by EIR. +pub(crate) fn attribute_factory_id_with_extra( classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], attr_name: &str, attr_args: &[AttrArgEntry], ) -> i64 { @@ -99,17 +121,21 @@ pub(crate) fn attribute_factory_id( let lookup_name = resolve_class_name(classes, attr_name) .map(|resolved| resolved.to_string()) .unwrap_or_else(|| attr_name.to_string()); - collect_attribute_factories(classes) + collect_attribute_factories_with_extra(classes, extra_attrs) .into_iter() .find(|factory| factory.class_name == lookup_name && factory.args == attr_args) .map(|factory| factory.id) .unwrap_or(0) } -/// Builds the synthetic dispatch body for `ReflectionAttribute::newInstance()`. -pub(crate) fn build_attribute_new_instance_body(classes: &HashMap) -> Vec { +/// Builds the synthetic `ReflectionAttribute::newInstance()` body using class +/// metadata plus additional attribute metadata sources. +pub(crate) fn build_attribute_new_instance_body_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], +) -> Vec { let span = crate::span::Span::dummy(); - let factories = collect_attribute_factories(classes); + let factories = collect_attribute_factories_with_extra(classes, extra_attrs); let mut body = Vec::new(); for factory in factories { // Only resolvable attribute classes can be instantiated. Non-class @@ -126,7 +152,18 @@ pub(crate) fn build_attribute_new_instance_body(classes: &HashMap Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(attr_arg_expr(&entry.value)), + }, + span, + ), + _ => attr_arg_expr(&entry.value), + }) .collect(), }, span, @@ -250,17 +287,14 @@ fn attr_key_expr(key: &AttrKey) -> Expr { Expr::new(kind, span) } -/// Builds the synthetic body for `ReflectionAttribute::getArguments()`. For -/// each attribute whose class resolves, it dispatches on the factory id and -/// returns the captured arguments as a lowered array literal — so named -/// arguments and associative arrays are materialized through the normal array -/// path. Attributes without a resolvable class fall back to the `$__args` -/// property populated at construction. -pub(crate) fn build_attribute_get_arguments_body( +/// Builds the synthetic `ReflectionAttribute::getArguments()` body using class +/// metadata plus additional attribute metadata sources. +pub(crate) fn build_attribute_get_arguments_body_with_extra( classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], ) -> Vec { let span = crate::span::Span::dummy(); - let factories = collect_attribute_factories(classes); + let factories = collect_attribute_factories_with_extra(classes, extra_attrs); let mut body = Vec::new(); for factory in factories { let condition = factory_condition(factory.id); @@ -278,11 +312,17 @@ pub(crate) fn build_attribute_get_arguments_body( span, )); } - // Every attribute with supported arguments is registered as a factory - // above (class or not), so this is only a defensive default; return an - // empty associative array to match the declared return type. + // Runtime eval materializes `ReflectionAttribute` objects with factory id + // zero and stores their retained arguments in `__args`, so the fallback + // must preserve that path instead of returning an empty factory miss. body.push(Stmt::new( - StmtKind::Return(Some(entries_to_array_expr(&[], true))), + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, span)), + property: "__args".to_string(), + }, + span, + ))), span, )); body diff --git a/src/codegen_support/runtime/arrays/hash_free_deep.rs b/src/codegen_support/runtime/arrays/hash_free_deep.rs index c4478c732f..fafabc3ef1 100644 --- a/src/codegen_support/runtime/arrays/hash_free_deep.rs +++ b/src/codegen_support/runtime/arrays/hash_free_deep.rs @@ -93,6 +93,7 @@ pub fn emit_hash_free_deep(emitter: &mut Emitter) { emitter.instruction("cmn x15, #1"); // check whether this entry stores an inline integer key emitter.instruction("b.eq __rt_hash_free_deep_after_key"); // integer keys have no heap ownership to release emitter.instruction("ldr x0, [x13, #8]"); // load key pointer + emitter.instruction("cbz x0, __rt_hash_free_deep_after_key"); // empty string keys store no heap key pointer emitter.instruction("str x11, [sp, #24]"); // preserve loop index across helper call emitter.instruction("ldr w15, [x0, #-12]"); // load key refcount from heap header emitter.instruction("subs w15, w15, #1"); // decrement key refcount diff --git a/src/codegen_support/runtime/data/fixed.rs b/src/codegen_support/runtime/data/fixed.rs index a6736b9a29..281a4efc0f 100644 --- a/src/codegen_support/runtime/data/fixed.rs +++ b/src/codegen_support/runtime/data/fixed.rs @@ -99,6 +99,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".comm _fiber_main_saved_sp, 8, 3\n"); out.push_str(".comm _fiber_main_saved_exc, 8, 3\n"); out.push_str(".comm _fiber_main_saved_call_frame, 8, 3\n"); + out.push_str(".comm _elephc_eval_dynamic_object_destruct_fn, 8, 3\n"); out.push_str(".comm _rt_diag_suppression, 8, 3\n"); // elephc_web_capture: per-request output-capture mode flag read by // __rt_stdout_write. Zero (the default) routes echo output to the plain diff --git a/src/codegen_support/runtime/data/mod.rs b/src/codegen_support/runtime/data/mod.rs index 7626705ba8..bd133beeb6 100644 --- a/src/codegen_support/runtime/data/mod.rs +++ b/src/codegen_support/runtime/data/mod.rs @@ -15,6 +15,7 @@ mod user; pub(crate) use fixed::emit_runtime_data_fixed; /// Emit fixed runtime data section (heap globals, fatal/assertion messages, lookup tables, builtin callable metadata). pub(crate) use user::emit_runtime_data_user; +pub(crate) use user::{is_user_filter_contract_method, is_user_wrapper_contract_method}; /// Fatal error message when `php_uname()` receives a `$mode` argument whose length is not exactly 1. pub(crate) const PHP_UNAME_MODE_LEN_MSG: &str = diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index ae22628320..248f39e7b8 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -19,6 +19,33 @@ use crate::types::{ClassInfo, EnumInfo, FunctionSig, InterfaceInfo, PhpType}; use super::instanceof::{escaped_ascii, escaped_bytes}; +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; +const EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT: u64 = 40; +const EVAL_REFLECTION_PROPERTY_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_PROPERTY_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_PROPERTY_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +const EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED_SET: u64 = 2048; +const EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE_SET: u64 = 4096; +const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_METHOD_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_METHOD_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_METHOD_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT: u64 = 40; + /// Emit the user-dependent data section — globals, statics, class metadata. /// This changes per program and cannot be cached. pub(crate) fn emit_runtime_data_user( @@ -27,9 +54,15 @@ pub(crate) fn emit_runtime_data_user( functions: &HashMap, function_variant_groups: &HashSet, interfaces: &HashMap, + interface_names: &[String], + trait_names: &[String], + declared_trait_uses: &HashMap>, + declared_trait_source_lines: &HashMap, classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, + emit_eval_reflection_metadata: bool, + source_path: Option<&str>, ) -> String { let mut out = String::new(); @@ -76,14 +109,14 @@ pub(crate) fn emit_runtime_data_user( let mut sorted_enum_names: Vec<&String> = enums.keys().collect(); sorted_enum_names.sort(); - for enum_name in sorted_enum_names { - let Some(enum_info) = enums.get(enum_name) else { + for enum_name in &sorted_enum_names { + let Some(enum_info) = enums.get(*enum_name) else { continue; }; for case in &enum_info.cases { out.push_str(&format!( ".comm {}, 8, 3\n", - enum_case_symbol(enum_name, &case.name) + enum_case_symbol(*enum_name, &case.name) )); } } @@ -119,6 +152,28 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); super::instanceof::emit_instanceof_target_lookup_data(&mut out, &sorted_interfaces, &sorted_classes); emit_class_name_lookup_data(&mut out, max_class_id, &class_name_by_id); + emit_name_lookup_data( + &mut out, + "_interface_names_count", + "_interface_names", + "_interface_name", + interface_names, + ); + emit_name_lookup_data( + &mut out, + "_trait_names_count", + "_trait_names", + "_trait_name", + trait_names, + ); + let enum_names: Vec = sorted_enum_names.iter().map(|name| (*name).clone()).collect(); + emit_name_lookup_data( + &mut out, + "_enum_names_count", + "_enum_names", + "_enum_name", + &enum_names, + ); // Per-program class id of the built-in `Fiber` class. The fiber runtime // checks this against the receiver's class_id in __rt_object_free_deep so @@ -228,6 +283,33 @@ pub(crate) fn emit_runtime_data_user( } } + out.push_str(".globl _class_object_payload_sizes\n_class_object_payload_sizes:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let payload_size = + match (class_info_by_id.get(&class_id), class_name_by_id.get(&class_id)) { + (Some(class_info), Some(class_name)) => { + class_object_payload_size(class_name, class_info) + } + _ => 0, + }; + out.push_str(&format!(" .quad {}\n", payload_size)); + } + } + + out.push_str(".globl _class_object_dynamic_prop_flags\n_class_object_dynamic_prop_flags:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let flag = match (class_info_by_id.get(&class_id), class_name_by_id.get(&class_id)) { + (Some(class_info), Some(class_name)) => { + u8::from(class_uses_dynamic_property_tail(class_name, class_info)) + } + _ => 0, + }; + out.push_str(&format!(" .quad {}\n", flag)); + } + } + out.push_str(".globl _class_gc_desc_count\n_class_gc_desc_count:\n"); out.push_str(&format!( " .quad {}\n", @@ -427,8 +509,35 @@ pub(crate) fn emit_runtime_data_user( } out.push_str(".p2align 3\n"); emit_static_callable_method_data(&mut out, &sorted_classes); + if emit_eval_reflection_metadata { + out.push_str(".p2align 3\n"); + emit_eval_reflection_source_file_data(&mut out, source_path); + } out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); + if emit_eval_reflection_metadata { + out.push_str(".p2align 3\n"); + emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); + out.push_str(".p2align 3\n"); + emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_lookup_data( + &mut out, + &sorted_classes, + &sorted_interfaces, + declared_trait_source_lines, + ); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_lookup_data( + &mut out, + &sorted_classes, + declared_trait_uses, + ); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_alias_lookup_data(&mut out, &sorted_classes); + } // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -635,6 +744,11 @@ pub(crate) fn emit_runtime_data_user( .properties .iter() .enumerate() + .filter(|(prop_index, (name, _))| { + class_info + .visible_property_index(name) + .is_some_and(|visible_index| visible_index == *prop_index) + }) .filter(|(_, (name, _))| { class_info .property_visibilities @@ -674,7 +788,7 @@ pub(crate) fn emit_runtime_data_user( } out.push_str(&format!(" .quad {}\n", public_props.len())); for (prop_index, (prop_name, prop_ty)) in &public_props { - let tag = if class_info.reference_properties.contains(prop_name) { + let tag = if class_info.property_slot_is_reference(*prop_index, prop_name) { 0 } else { match prop_ty { @@ -718,7 +832,7 @@ pub(crate) fn emit_runtime_data_user( out.push_str(", "); } let prop_name = &class_info.properties[i].0; - let tag = if class_info.reference_properties.contains(prop_name) { + let tag = if class_info.property_slot_is_reference(i, prop_name) { 0 } else { match prop_ty { @@ -880,6 +994,903 @@ fn emit_class_name_lookup_data( out.push_str(" .p2align 3\n"); } +/// Emits a compact `(name_ptr, name_len)` table for runtime class-like name probes. +fn emit_name_lookup_data( + out: &mut String, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + names: &[String], +) { + let mut sorted_names: Vec<&String> = names.iter().collect(); + sorted_names.sort(); + for (idx, name) in sorted_names.iter().enumerate() { + out.push_str(&format!( + ".globl {0}_{1}\n{0}_{1}:\n .ascii \"{2}\"\n", + label_prefix, + idx, + escaped_ascii(name) + )); + } + out.push_str(".p2align 3\n"); + out.push_str(&format!(".globl {0}\n{0}:\n", count_symbol)); + out.push_str(&format!(" .quad {}\n", sorted_names.len())); + out.push_str(&format!(".globl {0}\n{0}:\n", table_symbol)); + for (idx, name) in sorted_names.iter().enumerate() { + out.push_str(&format!(" .quad {}_{}\n", label_prefix, idx)); + out.push_str(&format!(" .quad {}\n", name.len())); + } +} + +/// Emits the source filename used by eval Reflection source-location hooks. +fn emit_eval_reflection_source_file_data(out: &mut String, source_path: Option<&str>) { + let source_path = source_path.unwrap_or(""); + out.push_str(".globl _eval_reflection_source_file\n_eval_reflection_source_file:\n"); + out.push_str(&format!(" .ascii \"{}\"\n", escaped_ascii(source_path))); + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_source_file_len\n_eval_reflection_source_file_len:\n"); + out.push_str(&format!(" .quad {}\n", source_path.len())); +} + +/// Emits AOT method flag rows consumed by eval ReflectionMethod metadata probes. +fn emit_eval_reflection_method_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + sorted_interfaces: &[(&String, &InterfaceInfo)], +) { + let mut entries = Vec::new(); + let class_infos = sorted_classes + .iter() + .map(|(name, info)| (name.as_str(), *info)) + .collect::>(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + let mut methods = class_info.methods.keys().collect::>(); + methods.sort(); + for method_name in methods { + let declaring_class = eval_reflection_instance_method_declaring_class( + class_name, + class_info, + method_name, + ); + let declaring_info = class_infos.get(declaring_class).copied().unwrap_or(class_info); + let flags = eval_reflection_method_flags_with_source_lines( + eval_reflection_instance_method_flags(class_info, method_name), + declaring_info, + method_name, + false, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + class_name, + method_name, + flags, + declaring_class, + ); + } + + let mut static_methods = class_info.static_methods.keys().collect::>(); + static_methods.sort(); + for method_name in static_methods { + let declaring_class = + eval_reflection_static_method_declaring_class(class_name, class_info, method_name); + let declaring_info = class_infos.get(declaring_class).copied().unwrap_or(class_info); + let flags = eval_reflection_method_flags_with_source_lines( + eval_reflection_static_method_flags(class_info, method_name), + declaring_info, + method_name, + true, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + class_name, + method_name, + flags, + declaring_class, + ); + } + } + + for (interface_name, interface_info) in sorted_interfaces { + let mut methods = interface_info.methods.keys().collect::>(); + methods.sort(); + for method_name in methods { + let declaring_interface = eval_reflection_interface_method_declaring_interface( + interface_name, + interface_info, + method_name, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + interface_name, + method_name, + eval_reflection_interface_method_flags(false), + declaring_interface, + ); + } + + let mut static_methods = interface_info.static_methods.keys().collect::>(); + static_methods.sort(); + for method_name in static_methods { + let declaring_interface = eval_reflection_interface_static_method_declaring_interface( + interface_name, + interface_info, + method_name, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + interface_name, + method_name, + eval_reflection_interface_method_flags(true), + declaring_interface, + ); + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_method_count\n_eval_reflection_method_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_methods\n_eval_reflection_methods:\n"); + for (class_label, class_len, method_label, method_len, flags, declaring_label, declaring_len) in + entries + { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", method_label)); + out.push_str(&format!(" .quad {}\n", method_len)); + out.push_str(&format!(" .quad {}\n", flags)); + out.push_str(&format!(" .quad {}\n", declaring_label)); + out.push_str(&format!(" .quad {}\n", declaring_len)); + } +} + +/// Adds one eval ReflectionMethod lookup row and its backing string labels. +fn push_eval_reflection_method_lookup_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize, u64, String, usize)>, + index: &mut usize, + class_name: &str, + method_name: &str, + flags: u64, + declaring_class: &str, +) { + let class_label = format!("_eval_reflection_method_class_{}", *index); + let method_label = format!("_eval_reflection_method_name_{}", *index); + let declaring_label = format!("_eval_reflection_method_declaring_class_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + method_label, + escaped_ascii(method_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); + entries.push(( + class_label, + class_name.len(), + method_label, + method_name.len(), + flags, + declaring_label, + declaring_class.len(), + )); + *index += 1; +} + +/// Adds source start/end line bits to an AOT ReflectionMethod flag word when available. +fn eval_reflection_method_flags_with_source_lines( + flags: u64, + class_info: &ClassInfo, + method_name: &str, + is_static: bool, +) -> u64 { + let Some(method) = class_info + .method_decls + .iter() + .find(|method| method.is_static == is_static && php_symbol_key(&method.name) == method_name) + else { + return flags; + }; + let start_line = u64::from(method.span.line); + if start_line == 0 || start_line > EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK { + return flags; + } + flags + | (start_line << EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) + | (start_line << EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) +} + +/// Returns the class name that declares one visible instance method. +fn eval_reflection_instance_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .method_impl_classes + .get(method_name) + .or_else(|| class_info.method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one visible static method. +fn eval_reflection_static_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .static_method_impl_classes + .get(method_name) + .or_else(|| class_info.static_method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the interface name that declares one visible instance method. +fn eval_reflection_interface_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns the interface name that declares one visible static method. +fn eval_reflection_interface_static_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .static_method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns eval ReflectionMethod bitflags for one instance method entry. +fn eval_reflection_instance_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { + let visibility = class_info + .method_visibilities + .get(method_name) + .unwrap_or(&Visibility::Public); + let mut flags = eval_reflection_method_visibility_flags(visibility); + if class_info.final_methods.contains(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_FINAL; + } + if !class_info.method_impl_classes.contains_key(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + } + flags +} + +/// Returns eval ReflectionMethod bitflags for one static method entry. +fn eval_reflection_static_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { + let visibility = class_info + .static_method_visibilities + .get(method_name) + .unwrap_or(&Visibility::Public); + let mut flags = + EVAL_REFLECTION_METHOD_FLAG_STATIC | eval_reflection_method_visibility_flags(visibility); + if class_info.final_static_methods.contains(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_FINAL; + } + if !class_info.static_method_impl_classes.contains_key(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + } + flags +} + +/// Returns eval ReflectionMethod bitflags for one interface method entry. +fn eval_reflection_interface_method_flags(is_static: bool) -> u64 { + let mut flags = EVAL_REFLECTION_METHOD_FLAG_PUBLIC | EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + if is_static { + flags |= EVAL_REFLECTION_METHOD_FLAG_STATIC; + } + flags +} + +/// Converts method visibility metadata into eval ReflectionMethod flag bits. +fn eval_reflection_method_visibility_flags(visibility: &Visibility) -> u64 { + match visibility { + Visibility::Public => EVAL_REFLECTION_METHOD_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_METHOD_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_METHOD_FLAG_PRIVATE, + } +} + +/// Emits AOT property flag rows consumed by eval ReflectionProperty metadata probes. +fn emit_eval_reflection_property_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for (slot, (property_name, _)) in class_info.properties.iter().enumerate() { + let flags = eval_reflection_instance_property_flags(class_info, slot, property_name); + let class_label = format!("_eval_reflection_property_class_{}", index); + let property_label = format!("_eval_reflection_property_name_{}", index); + let declaring_class = eval_reflection_instance_property_declaring_class( + class_name, + class_info, + property_name, + ); + let declaring_label = format!("_eval_reflection_property_declaring_class_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + property_label, + escaped_ascii(property_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); + entries.push(( + class_label, + class_name.len(), + property_label, + property_name.len(), + flags, + declaring_label, + declaring_class.len(), + )); + index += 1; + } + for (slot, (property_name, _)) in class_info.static_properties.iter().enumerate() { + let flags = eval_reflection_static_property_flags(class_info, slot, property_name); + let class_label = format!("_eval_reflection_property_class_{}", index); + let property_label = format!("_eval_reflection_property_name_{}", index); + let declaring_class = eval_reflection_static_property_declaring_class( + class_name, + class_info, + property_name, + ); + let declaring_label = format!("_eval_reflection_property_declaring_class_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + property_label, + escaped_ascii(property_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); + entries.push(( + class_label, + class_name.len(), + property_label, + property_name.len(), + flags, + declaring_label, + declaring_class.len(), + )); + index += 1; + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_property_count\n_eval_reflection_property_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_properties\n_eval_reflection_properties:\n"); + for (class_label, class_len, property_label, property_len, flags, declaring_label, declaring_len) in + entries + { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", property_label)); + out.push_str(&format!(" .quad {}\n", property_len)); + out.push_str(&format!(" .quad {}\n", flags)); + out.push_str(&format!(" .quad {}\n", declaring_label)); + out.push_str(&format!(" .quad {}\n", declaring_len)); + } +} + +/// Returns the class name that declares one visible instance property. +fn eval_reflection_instance_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one visible static property. +fn eval_reflection_static_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .static_property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Emits AOT class flag rows consumed by eval ReflectionClass metadata probes. +fn emit_eval_reflection_class_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + sorted_interfaces: &[(&String, &InterfaceInfo)], + declared_trait_source_lines: &HashMap, +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + let flags = eval_reflection_class_flags(class_info); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + entries.push((class_label, class_name.len(), flags)); + index += 1; + } + for (interface_name, interface_info) in sorted_interfaces { + let flags = eval_reflection_interface_flags(interface_info); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(interface_name) + )); + entries.push((class_label, interface_name.len(), flags)); + index += 1; + } + let mut sorted_trait_lines = declared_trait_source_lines.iter().collect::>(); + sorted_trait_lines + .sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + for (trait_name, line) in sorted_trait_lines { + let flags = eval_reflection_trait_flags(*line); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(trait_name) + )); + entries.push((class_label, trait_name.len(), flags)); + index += 1; + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_class_count\n_eval_reflection_class_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_classes\n_eval_reflection_classes:\n"); + for (class_label, class_len, flags) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", flags)); + } +} + +/// Returns eval ReflectionClass flag bits retained for one generated/AOT class. +fn eval_reflection_class_flags(class_info: &ClassInfo) -> u64 { + let mut flags = 0; + if class_info.is_final { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; + } + if class_info.is_abstract { + flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; + } + if class_info.is_readonly_class { + flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + flags |= eval_reflection_source_line_flags(class_info.declaration_span.line); + flags +} + +/// Returns eval ReflectionClass source-location bits retained for one generated/AOT interface. +fn eval_reflection_interface_flags(interface_info: &InterfaceInfo) -> u64 { + eval_reflection_source_line_flags(interface_info.declaration_span.line) +} + +/// Returns eval ReflectionClass source-location bits retained for one generated/AOT trait. +fn eval_reflection_trait_flags(line: u32) -> u64 { + eval_reflection_source_line_flags(line) +} + +/// Encodes declaration line metadata into high ReflectionClass flag bits. +fn eval_reflection_source_line_flags(line: u32) -> u64 { + let start_line = u64::from(line); + if start_line == 0 || start_line > EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK { + return 0; + } + (start_line << EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + | (start_line << EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) +} + +/// Emits class-like/interface-name rows consumed by eval ReflectionClass metadata probes. +fn emit_eval_reflection_class_interface_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + interfaces: &HashMap, +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for interface_name in &class_info.interfaces { + push_eval_reflection_class_interface_row( + out, + &mut entries, + &mut index, + class_name, + interface_name, + ); + } + } + + let mut sorted_interfaces: Vec<&String> = interfaces.keys().collect(); + sorted_interfaces.sort(); + for interface_name in sorted_interfaces { + for parent_name in eval_reflection_interface_parent_names(interface_name, interfaces) { + push_eval_reflection_class_interface_row( + out, + &mut entries, + &mut index, + interface_name, + &parent_name, + ); + } + } + + out.push_str(".p2align 3\n"); + out.push_str( + ".globl _eval_reflection_class_interface_count\n_eval_reflection_class_interface_count:\n", + ); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_class_interfaces\n_eval_reflection_class_interfaces:\n"); + for (class_label, class_len, interface_label, interface_len) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", interface_label)); + out.push_str(&format!(" .quad {}\n", interface_len)); + } +} + +/// Adds one class-like/interface-name row and its backing string labels. +fn push_eval_reflection_class_interface_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + interface_name: &str, +) { + let class_label = format!("_eval_reflection_class_interface_class_{}", *index); + let interface_label = format!("_eval_reflection_class_interface_name_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + interface_label, + escaped_ascii(interface_name) + )); + entries.push(( + class_label, + class_name.len(), + interface_label, + interface_name.len(), + )); + *index += 1; +} + +/// Emits class-like/trait-name rows consumed by eval `class_uses()` metadata probes. +fn emit_eval_reflection_class_trait_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + declared_trait_uses: &HashMap>, +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for trait_name in &class_info.used_traits { + push_eval_reflection_class_trait_row( + out, + &mut entries, + &mut index, + class_name, + trait_name, + ); + } + } + + let mut sorted_traits: Vec<&String> = declared_trait_uses.keys().collect(); + sorted_traits.sort(); + for trait_name in sorted_traits { + if let Some(used_traits) = declared_trait_uses.get(trait_name) { + for used_trait in used_traits { + push_eval_reflection_class_trait_row( + out, + &mut entries, + &mut index, + trait_name, + used_trait, + ); + } + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_class_trait_count\n_eval_reflection_class_trait_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_class_traits\n_eval_reflection_class_traits:\n"); + for (class_label, class_len, trait_label, trait_len) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", trait_label)); + out.push_str(&format!(" .quad {}\n", trait_len)); + } +} + +/// Adds one class-like/trait-name row and its backing string labels. +fn push_eval_reflection_class_trait_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + trait_name: &str, +) { + let class_label = format!("_eval_reflection_class_trait_class_{}", *index); + let trait_label = format!("_eval_reflection_class_trait_name_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + trait_label, + escaped_ascii(trait_name) + )); + entries.push((class_label, class_name.len(), trait_label, trait_name.len())); + *index += 1; +} + +/// Emits class/alias and class/source rows consumed by eval `getTraitAliases()`. +fn emit_eval_reflection_class_trait_alias_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut alias_entries = Vec::new(); + let mut source_entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for (alias, source) in &class_info.trait_aliases { + push_eval_reflection_class_trait_alias_row( + out, + &mut alias_entries, + &mut source_entries, + &mut index, + class_name, + alias, + source, + ); + } + } + + out.push_str(".p2align 3\n"); + out.push_str( + ".globl _eval_reflection_class_trait_alias_count\n_eval_reflection_class_trait_alias_count:\n", + ); + out.push_str(&format!(" .quad {}\n", alias_entries.len())); + out.push_str(".globl _eval_reflection_class_trait_aliases\n_eval_reflection_class_trait_aliases:\n"); + for (class_label, class_len, alias_label, alias_len) in alias_entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", alias_label)); + out.push_str(&format!(" .quad {}\n", alias_len)); + } + out.push_str( + ".globl _eval_reflection_class_trait_alias_sources\n_eval_reflection_class_trait_alias_sources:\n", + ); + for (class_label, class_len, source_label, source_len) in source_entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", source_label)); + out.push_str(&format!(" .quad {}\n", source_len)); + } +} + +/// Adds one class/trait-alias row and its backing string labels. +fn push_eval_reflection_class_trait_alias_row( + out: &mut String, + alias_entries: &mut Vec<(String, usize, String, usize)>, + source_entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + alias: &str, + source: &str, +) { + let class_label = format!("_eval_reflection_class_trait_alias_class_{}", *index); + let alias_label = format!("_eval_reflection_class_trait_alias_name_{}", *index); + let source_label = format!("_eval_reflection_class_trait_alias_source_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + alias_label, + escaped_ascii(alias) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + source_label, + escaped_ascii(source) + )); + alias_entries.push((class_label.clone(), class_name.len(), alias_label, alias.len())); + source_entries.push((class_label, class_name.len(), source_label, source.len())); + *index += 1; +} + +/// Returns direct and inherited parent interface names for one generated interface. +fn eval_reflection_interface_parent_names( + interface_name: &str, + interfaces: &HashMap, +) -> Vec { + let mut names = Vec::new(); + collect_eval_reflection_interface_parent_names(interface_name, interfaces, &mut names); + names +} + +/// Recursively appends interface parents without duplicating case-insensitive names. +fn collect_eval_reflection_interface_parent_names( + interface_name: &str, + interfaces: &HashMap, + names: &mut Vec, +) { + let Some((_, interface_info)) = eval_reflection_interface_entry(interface_name, interfaces) + else { + return; + }; + for parent in &interface_info.parents { + let parent_name = eval_reflection_interface_entry(parent, interfaces) + .map(|(name, _)| name.to_string()) + .unwrap_or_else(|| parent.clone()); + if names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + continue; + } + names.push(parent_name.clone()); + collect_eval_reflection_interface_parent_names(&parent_name, interfaces, names); + } +} + +/// Returns the canonical generated interface entry for a possibly case-varied name. +fn eval_reflection_interface_entry<'a>( + interface_name: &str, + interfaces: &'a HashMap, +) -> Option<(&'a str, &'a InterfaceInfo)> { + if let Some((name, info)) = interfaces.get_key_value(interface_name) { + return Some((name.as_str(), info)); + } + interfaces + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(interface_name)) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Returns eval ReflectionProperty bitflags for one instance property slot. +fn eval_reflection_instance_property_flags( + class_info: &ClassInfo, + slot: usize, + property_name: &str, +) -> u64 { + let visibility = class_info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + let mut flags = eval_reflection_visibility_flags(visibility); + if class_info.final_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_FINAL; + } + if class_info.abstract_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT; + } + if class_info.readonly_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_READONLY; + } + if class_info.promoted_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED; + } + match class_info.property_set_visibilities.get(property_name) { + Some(Visibility::Protected) => flags |= EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED_SET, + Some(Visibility::Private) => flags |= EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE_SET, + Some(Visibility::Public) | None => {} + } + if class_info.defaults.get(slot).is_some_and(Option::is_some) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE; + } + flags +} + +/// Returns eval ReflectionProperty bitflags for one static property slot. +fn eval_reflection_static_property_flags( + class_info: &ClassInfo, + slot: usize, + property_name: &str, +) -> u64 { + let visibility = class_info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + let mut flags = + EVAL_REFLECTION_PROPERTY_FLAG_STATIC | eval_reflection_visibility_flags(visibility); + if class_info.final_static_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_FINAL; + } + if class_info + .static_defaults + .get(slot) + .is_some_and(Option::is_some) + { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE; + } + flags +} + +/// Converts a property visibility into eval ReflectionProperty bitflags. +fn eval_reflection_visibility_flags(visibility: &Visibility) -> u64 { + match visibility { + Visibility::Public => EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE, + } +} + /// Emits the callable-function name table and pointer table for user-defined functions. /// Each function name is emitted as an ASCII label; the pointer table references /// either the active variant symbol for polymorphic functions or zero. @@ -945,13 +1956,7 @@ fn emit_classes_by_name_table( out.push_str(&format!(" .quad {}\n", sorted_classes.len())); out.push_str(".globl _classes_by_name\n_classes_by_name:\n"); for (class_name, class_info) in sorted_classes { - let num_props = class_info.properties.len(); - let dyn_props_slot = if class_info.allow_dynamic_properties { - 8 - } else { - 0 - }; - let obj_size = 8 + num_props * 16 + dyn_props_slot; + let obj_size = class_object_payload_size(class_name, class_info); out.push_str(&format!( " .quad _class_by_name_str_{}\n", class_info.class_id @@ -962,6 +1967,21 @@ fn emit_classes_by_name_table( } } +/// Returns the PHP object payload bytes required by one class layout. +fn class_object_payload_size(class_name: &str, class_info: &ClassInfo) -> usize { + let dyn_props_slot = if class_uses_dynamic_property_tail(class_name, class_info) { + 8 + } else { + 0 + }; + 8 + class_info.properties.len() * 16 + dyn_props_slot +} + +/// Returns whether this class layout stores a dynamic-property hash tail. +fn class_uses_dynamic_property_tail(class_name: &str, class_info: &ClassInfo) -> bool { + class_name == "stdClass" || class_info.allow_dynamic_properties +} + /// The number of fixed-slot stream-wrapper methods recorded per class in /// `_user_wrapper_vtable_`. Slot order matches the runtime fopen /// dispatch (Phase 10): 0 stream_open, 1 stream_close, 2 stream_read, @@ -994,6 +2014,18 @@ pub(crate) const USER_WRAPPER_VTABLE_SLOTS: usize = 23; /// branch with a single load + cmp. pub(crate) const USER_FILTER_VTABLE_SLOTS: usize = 5; +/// Returns true when a method key belongs to the fixed-ABI stream-wrapper +/// vtable surface dispatched by the runtime with raw arguments. +pub(crate) fn is_user_wrapper_contract_method(method_key: &str) -> bool { + USER_WRAPPER_METHOD_NAMES.contains(&method_key) +} + +/// Returns true when a method key belongs to the fixed-ABI user-filter +/// vtable surface dispatched by the runtime with raw arguments. +pub(crate) fn is_user_filter_contract_method(method_key: &str) -> bool { + USER_FILTER_METHOD_NAMES.contains(&method_key) +} + const USER_FILTER_METHOD_NAMES: [&str; 3] = [ "filter", "oncreate", @@ -1331,19 +2363,25 @@ mod tests { ClassInfo { class_id, + declaration_span: crate::span::Span::dummy(), parent: None, is_abstract: false, is_final: false, is_readonly_class: false, allow_dynamic_properties: false, constants: HashMap::new(), + constant_visibilities: HashMap::new(), + final_constants: HashSet::new(), attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), + constant_attribute_names: HashMap::new(), + constant_attribute_args: HashMap::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), property_declaring_classes: HashMap::new(), @@ -1351,10 +2389,13 @@ mod tests { property_visibilities: HashMap::new(), property_set_visibilities: HashMap::new(), declared_properties: HashSet::new(), + property_declared_slots: Vec::new(), final_properties: HashSet::new(), readonly_properties: HashSet::new(), reference_properties: HashSet::new(), owned_reference_properties: HashSet::new(), + promoted_properties: HashSet::new(), + property_reference_slots: Vec::new(), abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), @@ -1407,9 +2448,15 @@ mod tests { &HashMap::new(), &HashSet::new(), &HashMap::new(), + &[], + &[], + &HashMap::new(), + &HashMap::new(), &classes, &HashMap::new(), Some(&allowed_class_names), + false, + None, ); assert!(asm.contains("_class_vtable_1")); @@ -1432,9 +2479,15 @@ mod tests { &HashMap::new(), &HashSet::new(), &HashMap::new(), + &[], + &[], + &HashMap::new(), + &HashMap::new(), &classes, &HashMap::new(), None, + false, + None, ); assert!(asm.contains("_class_gc_desc_count:\n .quad 4\n")); diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index 518ac9ae42..083b72b94c 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -12,6 +12,8 @@ use super::arrays; use super::buffers; use super::callables; use super::diagnostics; +use super::eval_bridge; +use super::eval_scope; use super::exceptions; use super::fibers; use super::generators; @@ -320,6 +322,16 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_mixed_write_stdout(emitter); arrays::emit_object_free_deep(emitter); arrays::emit_refcount(emitter); + if features.eval_bridge { + eval_bridge::emit_eval_bridge_runtime(emitter); + } else if features.eval_scope { + // Scope-only programs run compiled eval fragments natively: they need + // the self-contained value wrappers plus the native scope helpers + // (the magician staticlib supplies the scope symbols only in the full + // bridge configuration). + eval_bridge::emit_eval_bridge_runtime(emitter); + eval_scope::emit_eval_scope_runtime(emitter); + } // SPL runtime-managed containers spl::emit_doubly_linked_list_runtime(emitter); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs new file mode 100644 index 0000000000..ac3b9c2bdd --- /dev/null +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -0,0 +1,4728 @@ +//! Purpose: +//! Emits C-ABI wrappers used by the optional `elephc-magician` bridge crate. +//! Adapts Rust staticlib calls to elephc's internal runtime value helper ABI. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` when `RuntimeFeatures.eval` is enabled. +//! +//! Key details: +//! - Exported wrapper labels use platform C-symbol mangling because they are +//! referenced from Rust object files, while internal `__rt_*` calls keep the +//! existing assembly ABI. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; +const EVAL_RUNTIME_TAG_MIXED: i64 = 7; +const INVOKER_ARG_REF_CELL_TAG: i64 = 11; + +/// Builds the x86_64 instruction that installs the Mixed heap-kind marker. +fn x86_64_mixed_heap_kind_instruction() -> String { + format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5) +} + +/// Emits every eval value wrapper required by `libelephc-magician`. +pub(crate) fn emit_eval_bridge_runtime(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: eval bridge value wrappers ---"); + match emitter.target.arch { + Arch::AArch64 => emit_aarch64_wrappers(emitter), + Arch::X86_64 => emit_x86_64_wrappers(emitter), + } +} + +/// Emits ARM64 C-ABI wrappers around the internal mixed value helpers. +fn emit_aarch64_wrappers(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the null payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_bool"); + emitter.instruction("cmp x0, #0"); // normalize any non-zero C bool payload to PHP true + emitter.instruction("cset x1, ne"); // bool payload is 1 for true and 0 for false + emitter.instruction("mov x0, #3"); // runtime tag 3 = bool + emitter.instruction("mov x2, xzr"); // bool payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the bool payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_new_object"); + emitter.instruction("sub sp, sp, #32"); // allocate a wrapper frame with object and boxed-result slots + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("cmp x1, #8"); // stdClass has an 8-byte class name + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // use the generic factory for non-stdClass lengths + emitter.instruction("ldrb w9, [x0]"); // load candidate byte 0 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 0 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 0 differs + emitter.instruction("ldrb w9, [x0, #1]"); // load candidate byte 1 for stdClass comparison + emitter.instruction("cmp w9, #116"); // byte 1 must be 't' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 1 differs + emitter.instruction("ldrb w9, [x0, #2]"); // load candidate byte 2 for stdClass comparison + emitter.instruction("cmp w9, #100"); // byte 2 must be 'd' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 2 differs + emitter.instruction("ldrb w9, [x0, #3]"); // load candidate byte 3 for stdClass comparison + emitter.instruction("cmp w9, #67"); // byte 3 must be 'C' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 3 differs + emitter.instruction("ldrb w9, [x0, #4]"); // load candidate byte 4 for stdClass comparison + emitter.instruction("cmp w9, #108"); // byte 4 must be 'l' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 4 differs + emitter.instruction("ldrb w9, [x0, #5]"); // load candidate byte 5 for stdClass comparison + emitter.instruction("cmp w9, #97"); // byte 5 must be 'a' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 5 differs + emitter.instruction("ldrb w9, [x0, #6]"); // load candidate byte 6 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 6 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 6 differs + emitter.instruction("ldrb w9, [x0, #7]"); // load candidate byte 7 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 7 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 7 differs + emitter.instruction("bl __rt_stdclass_new"); // allocate stdClass with its dynamic-property hash + emitter.instruction("b __elephc_eval_value_new_object_box"); // box the stdClass object for Rust + emitter.label("__elephc_eval_value_new_object_generic"); + emitter.instruction("mov x2, x1"); // move the C class-name length into new_by_name's string ABI + emitter.instruction("mov x1, x0"); // move the C class-name pointer into new_by_name's string ABI + emitter.instruction("bl __rt_new_by_name"); // allocate the named AOT class object, or return null on miss + emitter.instruction("cbz x0, __elephc_eval_value_new_object_null"); // box PHP null when no runtime class matched the eval name + emitter.label("__elephc_eval_value_new_object_box"); + emitter.instruction("str x0, [sp, #0]"); // save the raw object owner before boxing it for eval + emitter.instruction("mov x1, x0"); // move the allocated object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("str x0, [sp, #8]"); // save the boxed Mixed while consuming the raw object owner + emitter.instruction("ldr x0, [sp, #0]"); // reload the raw object owner created by the allocator + emitter.instruction("ldr w9, [x0, #-12]"); // load the raw object refcount after Mixed boxing retained it + emitter.instruction("sub w9, w9, #1"); // consume the allocator-owned object reference locally + emitter.instruction("str w9, [x0, #-12]"); // leave the boxed Mixed as the sole object owner + emitter.instruction("ldr x0, [sp, #8]"); // restore the boxed object Mixed as the Rust return value + emitter.instruction("b __elephc_eval_value_new_object_done"); // skip the null boxing path after successful allocation + emitter.label("__elephc_eval_value_new_object_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unknown eval class names + emitter.label("__elephc_eval_value_new_object_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the dynamic-object wrapper frame + emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + + emit_aarch64_object_from_raw_wrapper(emitter); + emit_aarch64_install_dynamic_object_destructor_hook(emitter); + emit_aarch64_object_clone_shallow_wrapper(emitter); + + label_c_global(emitter, "__elephc_eval_class_exists"); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class-name lookup state + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across string compares + emitter.instruction("add x29, sp, #48"); // establish a stable class-exists frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_classes_by_name_count"); + emitter.instruction("ldr x9, [x9]"); // load the registered class-name count + emitter.instruction("cbz x9, __elephc_eval_class_exists_miss"); // an empty table cannot contain the requested class + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", "_classes_by_name"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-name table cursor + emitter.instruction("mov x11, #0"); // start scanning at table index zero + emitter.label("__elephc_eval_class_exists_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-name table count + emitter.instruction("cmp x11, x9"); // have all class-name entries been scanned? + emitter.instruction("b.ge __elephc_eval_class_exists_miss"); // no class matched before the end of the table + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_class_exists_skip"); // length mismatch means this entry cannot match + emitter.instruction("str x11, [sp, #32]"); // save the table index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the table index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this entry? + emitter.instruction("b.eq __elephc_eval_class_exists_hit"); // report true on a class-name match + emitter.label("__elephc_eval_class_exists_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("add x10, x10, #32"); // advance to the next class-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the table index + emitter.instruction("b __elephc_eval_class_exists_loop"); // continue scanning the class-name table + emitter.label("__elephc_eval_class_exists_hit"); + emitter.instruction("mov x0, #1"); // return true for a matched class name + emitter.instruction("b __elephc_eval_class_exists_done"); // skip the false result after a match + emitter.label("__elephc_eval_class_exists_miss"); + emitter.instruction("mov x0, #0"); // return false when no class-name entry matched + emitter.label("__elephc_eval_class_exists_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the class-exists helper frame + emitter.instruction("ret"); // return the class-exists flag to Rust + + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_interface_exists", + "_interface_names_count", + "_interface_names", + "__elephc_eval_interface_exists", + ); + + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_trait_exists", + "_trait_names_count", + "_trait_names", + "__elephc_eval_trait_exists", + ); + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_enum_exists", + "_enum_names_count", + "_enum_names", + "__elephc_eval_enum_exists", + ); + + emit_aarch64_eval_reflection_method_names(emitter); + emit_aarch64_eval_reflection_property_names(emitter); + emit_aarch64_eval_reflection_class_interface_names(emitter); + emit_aarch64_eval_reflection_class_trait_names(emitter); + emit_aarch64_eval_reflection_class_trait_alias_names(emitter); + emit_aarch64_eval_reflection_class_trait_alias_sources(emitter); + emit_aarch64_eval_reflection_source_file(emitter); + emit_aarch64_eval_reflection_class_flags(emitter); + emit_aarch64_eval_reflection_method_flags(emitter); + emit_aarch64_eval_reflection_method_declaring_class(emitter); + emit_aarch64_eval_reflection_property_declaring_class(emitter); + emit_aarch64_eval_reflection_property_flags(emitter); + + label_c_global(emitter, "__elephc_eval_value_is_a"); + emitter.instruction("sub sp, sp, #64"); // reserve relation lookup state and preserve the Rust return address + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across runtime match helpers + emitter.instruction("add x29, sp, #48"); // establish a stable is-a relation frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed eval object-or-class cell + emitter.instruction("str x3, [sp, #8]"); // save whether exact class matches should be rejected + emitter.instruction("bl __rt_instanceof_lookup"); // resolve the target class/interface string to matcher metadata + emitter.instruction("cmp x0, #0"); // did the target string resolve to emitted metadata? + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // unresolved targets cannot match eval object values + emitter.instruction("str x1, [sp, #16]"); // save the target class/interface id + emitter.instruction("str x2, [sp, #24]"); // save the target kind: 0 class, 1 interface + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed eval value for unboxing + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the eval value is an object + emitter.instruction("b.eq __elephc_eval_value_is_a_object"); // object values can use their concrete runtime class id + emitter.instruction("cmp x0, #1"); // runtime tag 1 means the eval value is a class string + emitter.instruction("b.eq __elephc_eval_value_is_a_string"); // class-string values need source metadata lookup + emitter.instruction("b __elephc_eval_value_is_a_false"); // other runtime tags cannot satisfy class relations + emitter.label("__elephc_eval_value_is_a_string"); + emitter.instruction("bl __rt_instanceof_lookup"); // resolve the source class string to matcher metadata + emitter.instruction("cmp x0, #0"); // did the source string resolve to emitted metadata? + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // unresolved source strings cannot match relation metadata + emitter.instruction("cmp x2, #0"); // source strings must resolve to concrete classes for this matcher + emitter.instruction("b.ne __elephc_eval_value_is_a_false"); // interface-source strings need a dedicated interface-parent matcher + emitter.instruction("str x1, [sp, #32]"); // build a fake object header containing the source class id + emitter.instruction("ldr x10, [sp, #8]"); // reload the exact-self exclusion flag + emitter.instruction("cbz x10, __elephc_eval_value_is_a_string_match"); // is_a() allows exact class-string matches + emitter.instruction("ldr x11, [sp, #24]"); // reload target kind before exact-class filtering + emitter.instruction("cbnz x11, __elephc_eval_value_is_a_string_match"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("ldr x13, [sp, #16]"); // reload the target concrete class id + emitter.instruction("cmp x1, x13"); // compare source and target class ids for subclass self exclusion + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // is_subclass_of() excludes the exact class string + emitter.label("__elephc_eval_value_is_a_string_match"); + emitter.instruction("add x0, sp, #32"); // pass the fake object header to the metadata matcher + emitter.instruction("ldr x1, [sp, #16]"); // pass the target class/interface id + emitter.instruction("ldr x2, [sp, #24]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("bl __rt_exception_matches"); // test class-string inheritance or implemented interfaces + emitter.instruction("b __elephc_eval_value_is_a_done"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_object"); + emitter.instruction("mov x9, x1"); // keep the unboxed object pointer for matcher input + emitter.instruction("cbz x9, __elephc_eval_value_is_a_false"); // malformed object payloads cannot match class metadata + emitter.instruction("ldr x10, [sp, #8]"); // reload the exact-self exclusion flag + emitter.instruction("cbz x10, __elephc_eval_value_is_a_match"); // is_a() allows exact class matches + emitter.instruction("ldr x11, [sp, #24]"); // reload target kind before exact-class filtering + emitter.instruction("cbnz x11, __elephc_eval_value_is_a_match"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("ldr x12, [x9]"); // load the object's concrete runtime class id + emitter.instruction("ldr x13, [sp, #16]"); // reload the target concrete class id + emitter.instruction("cmp x12, x13"); // compare object and target class ids for subclass self exclusion + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // is_subclass_of() excludes the object's exact class + emitter.label("__elephc_eval_value_is_a_match"); + emitter.instruction("mov x0, x9"); // pass the unboxed object pointer to the metadata matcher + emitter.instruction("ldr x1, [sp, #16]"); // pass the target class/interface id + emitter.instruction("ldr x2, [sp, #24]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("bl __rt_exception_matches"); // test inheritance or implemented-interface metadata + emitter.instruction("b __elephc_eval_value_is_a_done"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_false"); + emitter.instruction("mov x0, #0"); // return false for unresolved, scalar, or exact-self subclass cases + emitter.label("__elephc_eval_value_is_a_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the relation lookup frame + emitter.instruction("ret"); // return the boolean class-relation result to Rust + + label_c_global(emitter, "__elephc_eval_value_object_class_name"); + emitter.instruction("cbz x0, __elephc_eval_value_object_class_name_miss"); // reject null boxed handles before reading their tag + emitter.instruction("ldr x9, [x0]"); // load the boxed eval value runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 is an object payload + emitter.instruction("b.ne __elephc_eval_value_object_class_name_miss"); // non-objects cannot provide a class name + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_class_name_miss"); // reject malformed object payloads + emitter.instruction("ldr x10, [x9]"); // load the object's runtime class id + abi::emit_symbol_address(emitter, "x11", "_class_name_count"); + emitter.instruction("ldr x11, [x11]"); // load the dense class-name table length + emitter.instruction("cmp x10, x11"); // check whether the class id is in table bounds + emitter.instruction("b.hs __elephc_eval_value_object_class_name_miss"); // reject missing or out-of-range class ids + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); + emitter.instruction("lsl x12, x10, #4"); // convert class id to a 16-byte table-entry offset + emitter.instruction("add x11, x11, x12"); // address the class-name entry for this class id + emitter.instruction("ldr x1, [x11]"); // load the class-name string pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the class-name string length + emitter.instruction("cbz x2, __elephc_eval_value_object_class_name_miss"); // reject table holes with empty names + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the class-name string for Rust + emitter.label("__elephc_eval_value_object_class_name_miss"); + emitter.instruction("mov x0, xzr"); // report failure as a null C pointer to Rust + emitter.instruction("ret"); // return the failure sentinel + + label_c_global(emitter, "__elephc_eval_value_parent_class_name"); + emitter.instruction("sub sp, sp, #80"); // reserve lookup state and a call-preserving wrapper frame + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #64"); // establish a stable parent-class lookup frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval value tag and payload words + emitter.instruction("cmp x0, #6"); // tag 6 is an object payload + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_object"); // derive the parent from the object's runtime class id + emitter.instruction("cmp x0, #1"); // tag 1 is a class-name string payload + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_string"); // resolve a class string through generated metadata + emitter.instruction("b __elephc_eval_value_parent_class_name_empty"); // unsupported input types have no parent class name + emitter.label("__elephc_eval_value_parent_class_name_object"); + emitter.instruction("cbz x1, __elephc_eval_value_parent_class_name_empty"); // malformed object payloads have no parent class + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emitter.instruction("b __elephc_eval_value_parent_class_name_from_id"); // convert the class id to its parent class name + emitter.label("__elephc_eval_value_parent_class_name_string"); + emitter.instruction("str x1, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_classes_by_name_count"); + emitter.instruction("ldr x9, [x9]"); // load the registered class-name count + emitter.instruction("cbz x9, __elephc_eval_value_parent_class_name_empty"); // an empty class table cannot resolve a parent name + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", "_classes_by_name"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-name table cursor + emitter.instruction("mov x11, #0"); // start scanning generated class-name entries at index zero + emitter.label("__elephc_eval_value_parent_class_name_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-name table count + emitter.instruction("cmp x11, x9"); // have all generated class names been checked? + emitter.instruction("b.ge __elephc_eval_value_parent_class_name_empty"); // no generated class matched the requested string + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name metadata entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested name lengths first + emitter.instruction("b.ne __elephc_eval_value_parent_class_name_skip"); // length mismatch means this class entry cannot match + emitter.instruction("str x11, [sp, #32]"); // preserve the scan index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the generated class-name pointer + emitter.instruction("mov x4, x12"); // pass the generated class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the scan index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this entry? + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_hit"); // resolve the matched class entry to its parent id + emitter.label("__elephc_eval_value_parent_class_name_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("add x10, x10, #32"); // advance to the next class-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the class-name scan index + emitter.instruction("b __elephc_eval_value_parent_class_name_loop"); // continue scanning generated class names + emitter.label("__elephc_eval_value_parent_class_name_hit"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the matched class-name table entry + emitter.instruction("ldr x9, [x10, #16]"); // load the matched runtime class id + emitter.label("__elephc_eval_value_parent_class_name_from_id"); + abi::emit_symbol_address(emitter, "x10", "_class_name_count"); + emitter.instruction("ldr x10, [x10]"); // load the dense class-name table length + emitter.instruction("cmp x9, x10"); // check that the class id can index parent metadata + emitter.instruction("b.hs __elephc_eval_value_parent_class_name_empty"); // unknown class ids have no parent class name + abi::emit_symbol_address(emitter, "x11", "_class_parent_ids"); + emitter.instruction("lsl x12, x9, #3"); // convert class id to a parent-id table byte offset + emitter.instruction("ldr x9, [x11, x12]"); // load the parent runtime class id + emitter.instruction("mov x13, #-1"); // materialize the parentless class sentinel + emitter.instruction("cmp x9, x13"); // check whether the runtime class has no parent + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_empty"); // parentless runtime classes produce an empty string + emitter.instruction("cmp x9, x10"); // check that the parent class id can index name metadata + emitter.instruction("b.hs __elephc_eval_value_parent_class_name_empty"); // invalid parent ids produce an empty string + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); + emitter.instruction("lsl x12, x9, #4"); // convert parent id to a 16-byte name-entry offset + emitter.instruction("add x11, x11, x12"); // address the parent class-name metadata row + emitter.instruction("ldr x1, [x11]"); // load the parent class-name string pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the parent class-name string length + emitter.instruction("cbz x2, __elephc_eval_value_parent_class_name_empty"); // table holes represent missing parent names + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the parent class-name string + emitter.instruction("b __elephc_eval_value_parent_class_name_done"); // restore the wrapper frame before returning to Rust + emitter.label("__elephc_eval_value_parent_class_name_empty"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("mov x1, xzr"); // missing parent names use an empty string pointer + emitter.instruction("mov x2, xzr"); // missing parent names use an empty string length + emitter.instruction("bl __rt_mixed_from_value"); // box the empty parent class-name string + emitter.label("__elephc_eval_value_parent_class_name_done"); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the parent-class lookup wrapper frame + emitter.instruction("ret"); // return the boxed parent class-name string to Rust + + label_c_global(emitter, "__elephc_eval_value_array_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #4"); // minimum indexed-array capacity for eval literals + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov x1, #8"); // Mixed indexed arrays store boxed-cell pointers + emitter.instruction("bl __rt_array_new"); // allocate indexed-array storage for boxed Mixed slots + emitter.instruction("ldr x10, [x0, #-8]"); // load the packed indexed-array heap kind word + emitter.instruction("mov x12, #0x80ff"); // preserve indexed-array kind and persistent COW metadata + emitter.instruction("and x10, x10, x12"); // clear the default scalar value_type bits + emitter.instruction("mov x11, #7"); // runtime value_type 7 = boxed Mixed + emitter.instruction("lsl x11, x11, #8"); // move the value_type tag into the packed kind word + emitter.instruction("orr x10, x10, x11"); // stamp the array as carrying boxed Mixed slots + emitter.instruction("str x10, [x0, #-8]"); // persist the updated indexed-array metadata + emitter.instruction("str x0, [sp, #0]"); // save the owned array pointer while allocating the Mixed box + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #4"); // runtime tag 4 = indexed array + emitter.instruction("str x10, [x0]"); // store the indexed-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned indexed-array pointer + emitter.instruction("str x11, [x0, #8]"); // store the array pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // indexed arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the array-new wrapper frame + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for string-array allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #4"); // minimum indexed-array capacity for eval metadata lists + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov x1, #16"); // direct string arrays store pointer/length pairs + emitter.instruction("bl __rt_array_new"); // allocate indexed-array storage for direct string slots + emitter.instruction("str x0, [sp, #0]"); // save the owned string-array pointer while boxing it + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #4"); // runtime tag 4 = indexed array + emitter.instruction("str x10, [x0]"); // store the indexed-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned direct-string array pointer + emitter.instruction("str x11, [x0, #8]"); // store the string-array pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // indexed arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-array-new wrapper frame + emitter.instruction("ret"); // return the boxed direct-string array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_push"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame while appending one metadata string + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed string-array owner + emitter.instruction("stp x1, x2, [sp, #8]"); // save the incoming string pointer and length + emitter.instruction("cbz x0, __elephc_eval_value_string_array_push_fail"); // reject malformed null string-array handles + emitter.instruction("bl __rt_mixed_unbox"); // expose the indexed-array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction("b.ne __elephc_eval_value_string_array_push_fail"); // reject non-array metadata containers + emitter.instruction("mov x0, x1"); // pass the unboxed array payload to the string append helper + emitter.instruction("ldp x1, x2, [sp, #8]"); // reload the string payload to append + emitter.instruction("bl __rt_array_push_str"); // persist and append the string, returning the updated array payload + emitter.instruction("ldr x9, [sp, #0]"); // reload the boxed string-array owner + emitter.instruction("str x0, [x9, #8]"); // update the boxed payload in case the array grew + emitter.instruction("mov x0, x9"); // return the boxed string-array owner to Rust + emitter.instruction("b __elephc_eval_value_string_array_push_done"); // skip the malformed-input null result + emitter.label("__elephc_eval_value_string_array_push_fail"); + emitter.instruction("mov x0, xzr"); // report a null pointer so Rust converts it to RuntimeFatal + emitter.label("__elephc_eval_value_string_array_push_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-array-push wrapper frame + emitter.instruction("ret"); // return the updated boxed string-array handle to Rust + + label_c_global(emitter, "__elephc_eval_value_assoc_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for hash allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #16"); // minimum hash capacity for eval associative literals + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum hash capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 16) as the hash allocation capacity + emitter.instruction("mov x1, #7"); // runtime value_type 7 = boxed Mixed hash values + emitter.instruction("bl __rt_hash_new"); // allocate associative-array storage for boxed Mixed entries + emitter.instruction("str x0, [sp, #0]"); // save the owned hash pointer while allocating the Mixed box + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new hash + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #5"); // runtime tag 5 = associative array + emitter.instruction("str x10, [x0]"); // store the associative-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned hash pointer + emitter.instruction("str x11, [x0, #8]"); // store the hash pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // associative arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the assoc-new wrapper frame + emitter.instruction("ret"); // return the boxed associative-array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_get"); + emitter.instruction("sub sp, sp, #32"); // allocate a wrapper frame for key coercion + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver while coercing the key + emitter.instruction("mov x0, x1"); // pass the boxed key to the eval key normalizer + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed array receiver + emitter.instruction("bl __rt_mixed_array_get"); // read the boxed Mixed element or Mixed(null) + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the array-get wrapper frame + emitter.instruction("ret"); // return the boxed element to Rust + + label_c_global(emitter, "__elephc_eval_value_array_key_exists"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for key existence probing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the boxed array receiver while normalizing the key + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("str x1, [sp, #8]"); // save the normalized key low word + emitter.instruction("str x2, [sp, #16]"); // save the normalized key high word + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed array receiver for tag dispatch + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // null handles do not contain array keys + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_key_exists_indexed"); // indexed arrays use bounds-based key existence + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_key_exists_assoc"); // associative arrays use hash existence + emitter.instruction("b __elephc_eval_value_array_key_exists_false"); // scalar values do not contain array keys + emitter.label("__elephc_eval_value_array_key_exists_indexed"); + emitter.instruction("ldr x2, [sp, #16]"); // reload normalized key_hi for integer-key checking + emitter.instruction("cmn x2, #1"); // integer keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_array_key_exists_false"); // non-integer keys never exist in indexed arrays + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed indexed-array receiver + emitter.instruction("ldr x0, [x0, #8]"); // load the indexed-array payload pointer + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // missing payload cannot contain a key + emitter.instruction("ldr x1, [sp, #8]"); // pass normalized integer key to the bounds helper + emitter.instruction("bl __rt_array_key_exists"); // return whether the integer key is in bounds + emitter.instruction("b __elephc_eval_value_array_key_exists_box"); // box the existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_assoc"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed associative-array receiver + emitter.instruction("ldr x0, [x0, #8]"); // load the hash payload pointer + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // missing hash payload cannot contain a key + emitter.instruction("ldr x1, [sp, #8]"); // pass normalized key_lo to the hash lookup + emitter.instruction("ldr x2, [sp, #16]"); // pass normalized key_hi to the hash lookup + emitter.instruction("bl __rt_hash_get"); // return hash found flag in x0 + emitter.instruction("b __elephc_eval_value_array_key_exists_box"); // box the hash existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_false"); + emitter.instruction("mov x0, #0"); // report false for misses and unsupported receivers + emitter.label("__elephc_eval_value_array_key_exists_box"); + emitter.instruction("mov x1, x0"); // move the C bool result into mixed value_lo + emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean + emitter.instruction("mov x2, xzr"); // boolean payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the bool result for Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the key-exists wrapper frame + emitter.instruction("ret"); // return the boxed bool result to Rust + + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for insertion-order key iteration + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable iterator-key frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver while walking the container + emitter.instruction("str x1, [sp, #8]"); // save the requested zero-based foreach position + emitter.instruction("cbz x0, __elephc_eval_value_array_iter_key_null"); // null handles produce a null key + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_indexed"); // indexed arrays expose integer positions as foreach keys + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_assoc"); // associative arrays expose insertion-order hash keys + emitter.instruction("b __elephc_eval_value_array_iter_key_null"); // scalar values have no foreach-visible key + emitter.label("__elephc_eval_value_array_iter_key_indexed"); + emitter.instruction("ldr x1, [sp, #8]"); // use the requested foreach position as the integer key payload + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the indexed foreach key as an owned Mixed cell + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc"); + emitter.instruction("ldr x9, [x0, #8]"); // load the hash payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_array_iter_key_null"); // null hash payloads produce a null key + emitter.instruction("str x9, [sp, #16]"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("str xzr, [sp, #24]"); // start the insertion-order position counter at zero + emitter.instruction("mov x1, xzr"); // cursor 0 starts at the hash head entry + emitter.label("__elephc_eval_value_array_iter_key_assoc_loop"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the hash pointer before advancing the hash iterator + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next insertion-order hash key + emitter.instruction("cmn x0, #1"); // did the iterator report the done sentinel? + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("ldr x10, [sp, #24]"); // load the current insertion-order position + emitter.instruction("ldr x11, [sp, #8]"); // load the requested foreach position + emitter.instruction("cmp x10, x11"); // is this the requested hash entry? + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_assoc_box"); // box the current hash key when the position matches + emitter.instruction("add x10, x10, #1"); // advance the insertion-order position counter + emitter.instruction("str x10, [sp, #24]"); // persist the updated position counter for the next probe + emitter.instruction("mov x1, x0"); // use the returned cursor for the next hash iterator call + emitter.instruction("b __elephc_eval_value_array_iter_key_assoc_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_array_iter_key_assoc_box"); + emitter.instruction("cmn x2, #1"); // integer hash keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_array_iter_key_assoc_string"); // string hash keys need string-tag boxing + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the associative integer key as Mixed + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc_string"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string key + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the associative string key as Mixed + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null keys do not use a low payload word + emitter.instruction("mov x2, xzr"); // null keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for invalid foreach-key requests + emitter.label("__elephc_eval_value_array_iter_key_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the iterator-key wrapper frame + emitter.instruction("ret"); // return the boxed foreach key to Rust + + label_c_global(emitter, "__elephc_eval_value_array_set"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for key coercion and value retention + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver + emitter.instruction("str x2, [sp, #8]"); // save the boxed value being written + emitter.instruction("mov x0, x1"); // pass the boxed key to the eval key normalizer + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("str x1, [sp, #16]"); // save the normalized key low word + emitter.instruction("str x2, [sp, #24]"); // save the normalized key high word + emitter.instruction("ldr x0, [sp, #8]"); // reload the value so the array consumes a retained owner + emitter.instruction("bl __rt_incref"); // retain the boxed value for Mixed array storage + emitter.instruction("ldr x0, [sp, #0]"); // pass the boxed array receiver to the Mixed array setter + emitter.instruction("ldr x1, [sp, #16]"); // pass the normalized key low word to the setter + emitter.instruction("ldr x2, [sp, #24]"); // pass the normalized key high word to the setter + emitter.instruction("ldr x3, [sp, #8]"); // pass the retained boxed value to be consumed by the setter + emitter.instruction("bl __rt_mixed_array_set"); // mutate the boxed Mixed array through the shared runtime helper + emitter.instruction("ldr x0, [sp, #0]"); // return the target boxed array receiver to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the array-set wrapper frame + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_len"); + emitter.instruction("cbz x0, __elephc_eval_value_array_len_zero"); // null handles have no iterable eval elements + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_len_load"); // indexed arrays expose their header element count + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_len_load"); // associative arrays expose their header entry count + emitter.label("__elephc_eval_value_array_len_zero"); + emitter.instruction("mov x0, #0"); // scalar values have zero foreach-visible elements in this subset + emitter.instruction("ret"); // return the empty length to Rust + emitter.label("__elephc_eval_value_array_len_load"); + emitter.instruction("ldr x9, [x0, #8]"); // load the array/hash payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_array_len_zero"); // null payloads are treated as empty containers + emitter.instruction("ldr x0, [x9]"); // load the runtime container element count + emitter.instruction("ret"); // return the element count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_len"); + emitter.instruction("cbz x0, __elephc_eval_value_object_property_len_zero"); // null handles have no JSON-visible object properties + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_property_len_zero"); // non-objects expose no JSON-visible properties here + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_len_zero"); // null object payloads have no visible properties + abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); + emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("cmp x11, x10"); // check whether the object is stdClass + emitter.instruction("b.ne __elephc_eval_value_object_property_len_zero"); // non-stdClass objects expose no bridge-visible properties + emitter.instruction("ldr x9, [x9, #8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_len_zero"); // null property hashes are treated as empty objects + emitter.instruction("ldr x0, [x9]"); // load the hash entry count + emitter.instruction("ret"); // return the public property count to Rust + emitter.label("__elephc_eval_value_object_property_len_zero"); + emitter.instruction("mov x0, #0"); // report zero JSON-visible object properties + emitter.instruction("ret"); // return the empty property count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_iter_key"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for insertion-order property iteration + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable property-iterator frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed object receiver while walking properties + emitter.instruction("str x1, [sp, #8]"); // save the requested zero-based property position + emitter.instruction("cbz x0, __elephc_eval_value_object_property_iter_key_null"); // null handles produce a null property key + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_null"); // non-objects have no JSON-visible property key + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_iter_key_null"); // null object payloads produce a null key + abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); + emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("cmp x11, x10"); // check whether the object is stdClass + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_null"); // non-stdClass objects have no bridge-visible key + emitter.instruction("ldr x9, [x9, #8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_iter_key_null"); // null property hashes produce a null key + emitter.instruction("str x9, [sp, #16]"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("str xzr, [sp, #24]"); // start the insertion-order property counter at zero + emitter.instruction("mov x1, xzr"); // cursor 0 starts at the property hash head entry + emitter.label("__elephc_eval_value_object_property_iter_key_loop"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the hash pointer before advancing the iterator + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next insertion-order property key + emitter.instruction("cmn x0, #1"); // did the iterator report the done sentinel? + emitter.instruction("b.eq __elephc_eval_value_object_property_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("ldr x10, [sp, #24]"); // load the current insertion-order property position + emitter.instruction("ldr x11, [sp, #8]"); // load the requested property position + emitter.instruction("cmp x10, x11"); // is this the requested property entry? + emitter.instruction("b.eq __elephc_eval_value_object_property_iter_key_box"); // box the current property key when the position matches + emitter.instruction("add x10, x10, #1"); // advance the insertion-order property counter + emitter.instruction("str x10, [sp, #24]"); // persist the updated property counter + emitter.instruction("mov x1, x0"); // use the returned cursor for the next iterator call + emitter.instruction("b __elephc_eval_value_object_property_iter_key_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_object_property_iter_key_box"); + emitter.instruction("cmn x2, #1"); // integer hash keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_string"); // string property keys need string-tag boxing + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key fallback + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the integer property key as Mixed + emitter.instruction("b __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_string"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string property key + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property key as Mixed + emitter.instruction("b __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null keys do not use a low payload word + emitter.instruction("mov x2, xzr"); // null keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for invalid property-key requests + emitter.label("__elephc_eval_value_object_property_iter_key_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the property-iterator wrapper frame + emitter.instruction("ret"); // return the boxed property key to Rust + + emitter.label("__elephc_eval_key_normalize"); + emitter.instruction("sub sp, sp, #32"); // allocate a helper frame while classifying the boxed eval key + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #16"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the original boxed key for fallback integer casts + emitter.instruction("bl __rt_mixed_unbox"); // expose key tag plus payload words + emitter.instruction("cmp x0, #1"); // is the eval key a string? + emitter.instruction("b.eq __elephc_eval_key_normalize_string"); // normalize PHP string array keys through hash rules + emitter.instruction("cmp x0, #0"); // is the eval key already an integer? + emitter.instruction("b.eq __elephc_eval_key_normalize_int"); // integer keys only need the sentinel high word + emitter.instruction("cmp x0, #8"); // is the eval key null? + emitter.instruction("b.eq __elephc_eval_key_normalize_null"); // PHP treats null array keys as the empty string + emitter.instruction("ldr x0, [sp, #0]"); // reload the original boxed key for PHP integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-string keys to the current integer-key fallback + emitter.instruction("mov x1, x0"); // publish the coerced integer key low word + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer array key + emitter.instruction("b __elephc_eval_key_normalize_done"); // return the fallback integer key tuple + emitter.label("__elephc_eval_key_normalize_string"); + emitter.instruction("bl __rt_hash_normalize_key"); // normalize numeric strings while preserving true string keys + emitter.instruction("b __elephc_eval_key_normalize_done"); // return the normalized string/int key tuple + emitter.label("__elephc_eval_key_normalize_int"); + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer array key + emitter.instruction("b __elephc_eval_key_normalize_done"); // finish integer key normalization + emitter.label("__elephc_eval_key_normalize_null"); + emitter.instruction("mov x1, xzr"); // null array keys use the empty-string pointer + emitter.instruction("mov x2, xzr"); // null array keys use the empty-string length + emitter.label("__elephc_eval_key_normalize_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the key-normalizer helper frame + emitter.instruction("ret"); // return key_lo/key_hi in x1/x2 + + label_c_global(emitter, "__elephc_eval_value_is_array_like"); + emitter.instruction("cbz x0, __elephc_eval_value_is_array_like_false"); // null handles cannot be indexed as arrays + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // indexed arrays are valid eval array-write receivers + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // associative arrays are valid eval array-write receivers + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // ArrayAccess-capable objects are delegated to runtime set helpers + emitter.label("__elephc_eval_value_is_array_like_false"); + emitter.instruction("mov x0, #0"); // report false for scalar and null values + emitter.instruction("ret"); // return the boolean result to Rust + emitter.label("__elephc_eval_value_is_array_like_true"); + emitter.instruction("mov x0, #1"); // report true for array-like values + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_is_null"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the Mixed cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to a concrete runtime tag + emitter.instruction("cmp x0, #8"); // runtime tag 8 means PHP null + emitter.instruction("cset x0, eq"); // return true when the unboxed tag is null + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the null-check wrapper frame + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_type_tag"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the Mixed cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells and return the concrete runtime tag + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the type-tag wrapper frame + emitter.instruction("ret"); // return the unboxed runtime tag to Rust + + label_c_global(emitter, "__elephc_eval_value_invoker_ref_cell"); + emitter.instruction("mov x1, x0"); // pass the staged Mixed slot address as marker payload + emitter.instruction(&format!("mov x0, #{}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction(&format!("mov x2, #{}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed + emitter.instruction("b __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_invoker_raw_ref_cell"); + emitter.instruction("mov x2, x1"); // pass the staged raw slot source tag as marker metadata + emitter.instruction("mov x1, x0"); // pass the staged raw slot address as marker payload + emitter.instruction(&format!("mov x0, #{}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction("b __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_raw_word"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the scalar cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a frame pointer for the wrapper call + emitter.instruction("bl __rt_mixed_unbox"); // expose the boxed scalar payload words + emitter.instruction("mov x0, x1"); // return the scalar low payload word to Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore caller frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the wrapper frame + emitter.instruction("ret"); // return the raw payload word + + label_c_global(emitter, "__elephc_eval_value_raw_high_word"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the string cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a frame pointer for the wrapper call + emitter.instruction("bl __rt_mixed_unbox"); // expose the boxed payload words + emitter.instruction("mov x0, x2"); // return the high payload word to Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore caller frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the wrapper frame + emitter.instruction("ret"); // return the raw high payload word + + label_c_global(emitter, "__elephc_eval_value_retain_raw_string"); + emitter.instruction("sub sp, sp, #48"); // reserve a wrapper frame while persisting the raw string + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across str_persist + emitter.instruction("add x29, sp, #32"); // establish a stable frame pointer + emitter.instruction("str x2, [sp, #0]"); // save the Rust out-len pointer across string persistence + emitter.instruction("mov x2, x1"); // move the raw string length into str_persist input + emitter.instruction("mov x1, x0"); // move the raw string pointer into str_persist input + emitter.instruction("bl __rt_str_persist"); // duplicate the string for staged by-ref ownership + emitter.instruction("ldr x9, [sp, #0]"); // reload the Rust out-len pointer + emitter.instruction("str x2, [x9]"); // report the persisted string length to Rust + emitter.instruction("mov x0, x1"); // return the persisted string pointer to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string retain wrapper frame + emitter.instruction("ret"); // return the retained raw string pointer + + label_c_global(emitter, "__elephc_eval_value_from_raw_string"); + emitter.instruction("mov x2, x1"); // move the raw string length into the Mixed high word + emitter.instruction("mov x1, x0"); // move the raw string pointer into the Mixed low word + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the raw string payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_string"); + emitter.instruction("b __rt_heap_free_safe"); // release the staged raw string owner + + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); + emitter.instruction("sub sp, sp, #32"); // reserve a wrapper frame while retaining the raw heap word + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across incref + emitter.instruction("add x29, sp, #16"); // establish a stable frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the raw heap word for the C return value + emitter.instruction("bl __rt_incref"); // retain the heap payload for the staged by-ref slot + emitter.instruction("ldr x0, [sp, #0]"); // return the original raw heap word to Rust + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the retain wrapper frame + emitter.instruction("ret"); // return the retained raw heap word + + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); + emitter.instruction("mov x2, xzr"); // raw one-word scalar payloads have no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_from_raw_heap_word"); + emitter.instruction("cbz x0, __elephc_eval_value_from_raw_heap_word_miss"); // null raw heap words cannot be boxed + emitter.instruction("ldr x9, [x0, #-8]"); // load the uniform heap kind word for tag recovery + emitter.instruction("and x9, x9, #0xff"); // isolate the low-byte heap kind tag + emitter.instruction("cmp x9, #2"); // heap kind 2 stores indexed-array payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_array"); // box indexed arrays with runtime tag 4 + emitter.instruction("cmp x9, #3"); // heap kind 3 stores associative hash payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_hash"); // box hashes with runtime tag 5 + emitter.instruction("cmp x9, #4"); // heap kind 4 stores object payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_object"); // box objects with runtime tag 6 + emitter.instruction("cmp x9, #5"); // heap kind 5 stores boxed Mixed payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_mixed"); // box nested Mixed cells with runtime tag 7 + emitter.label("__elephc_eval_value_from_raw_heap_word_miss"); + emitter.instruction("mov x0, xzr"); // report malformed raw heap words as a null Rust handle + emitter.instruction("ret"); // return the failed boxing sentinel + emitter.label("__elephc_eval_value_from_raw_heap_word_array"); + emitter.instruction("mov x1, x0"); // move the indexed-array payload into the boxing low word + emitter.instruction("mov x0, #4"); // runtime tag 4 = indexed array + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_hash"); + emitter.instruction("mov x1, x0"); // move the hash payload into the boxing low word + emitter.instruction("mov x0, #5"); // runtime tag 5 = associative array + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_object"); + emitter.instruction("mov x1, x0"); // move the object payload into the boxing low word + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_mixed"); + emitter.instruction("mov x1, x0"); // move the boxed Mixed payload into the boxing low word + emitter.instruction("mov x0, #7"); // runtime tag 7 = Mixed + emitter.label("__elephc_eval_value_from_raw_heap_word_box"); + emitter.instruction("mov x2, xzr"); // one-word heap payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // retain and box the raw heap payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_heap_word"); + emitter.instruction("b __rt_decref_any"); // release the staged raw heap slot owner + + label_c_global(emitter, "__elephc_eval_value_object_identity"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the object cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable object-identity wrapper frame + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means PHP object + emitter.instruction("csel x0, x1, xzr, eq"); // return the object payload pointer or zero on mismatch + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the object-identity wrapper frame + emitter.instruction("ret"); // return the object identity pointer to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_int"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_int"); // cast the boxed eval value to a PHP integer payload + emitter.instruction("mov x1, x0"); // move the integer cast result into mixed value_lo + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast integer result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed integer cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_float"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval value to a PHP double payload + emitter.instruction("fmov x1, d0"); // move the double cast bits into mixed value_lo + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast double result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed double cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_string"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing and boxing the string result + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete payload tag and value words + emitter.instruction("cmp x0, #0"); // is the eval value an integer? + emitter.instruction("b.eq __elephc_eval_value_cast_string_int"); // integers cast through decimal formatting + emitter.instruction("cmp x0, #1"); // is the eval value already a string? + emitter.instruction("b.eq __elephc_eval_value_cast_string_box"); // strings can be boxed through the normal ownership path + emitter.instruction("cmp x0, #2"); // is the eval value a double? + emitter.instruction("b.eq __elephc_eval_value_cast_string_float"); // doubles cast through decimal formatting + emitter.instruction("cmp x0, #3"); // is the eval value a boolean? + emitter.instruction("b.eq __elephc_eval_value_cast_string_bool"); // booleans cast to "1" or the empty string + emitter.label("__elephc_eval_value_cast_string_empty"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("mov x1, xzr"); // unsupported and falsey payloads use an empty string pointer + emitter.instruction("mov x2, xzr"); // unsupported and falsey payloads use an empty string length + emitter.instruction("bl __rt_mixed_from_value"); // box the empty string result for Rust + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_int"); + emitter.instruction("mov x0, x1"); // pass the integer payload to decimal formatting + emitter.instruction("bl __rt_itoa"); // format the integer cast result as a string pair + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the formatted integer string + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_box"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the existing string payload once + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_float"); + emitter.instruction("fmov d0, x1"); // move the double payload bits into the FP argument register + emitter.instruction("bl __rt_ftoa"); // format the double cast result as a string pair + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the formatted double string + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_bool"); + emitter.instruction("cbz x1, __elephc_eval_value_cast_string_empty"); // false casts to the empty string + emitter.instruction("mov x0, x1"); // pass the true payload to decimal formatting + emitter.instruction("bl __rt_itoa"); // format true as the string "1" + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the true string result + emitter.label("__elephc_eval_value_cast_string_done"); + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the string-cast wrapper frame + emitter.instruction("ret"); // return the boxed string cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_bool"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the boxed eval value to PHP truthiness + emitter.instruction("mov x1, x0"); // move the boolean cast result into mixed value_lo + emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean + emitter.instruction("mov x2, xzr"); // boolean payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast boolean result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed boolean cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_int"); + emitter.instruction("mov x1, x0"); // move the C integer argument into the mixed payload slot + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the integer payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_resource"); + emitter.instruction("mov x1, x0"); // move the C resource id into the mixed payload slot + emitter.instruction("mov x0, #9"); // runtime tag 9 = resource + emitter.instruction("mov x2, xzr"); // resource payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the resource payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_float"); + emitter.instruction("fmov x1, d0"); // move the C double bits into the mixed payload slot + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the double payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string"); + emitter.instruction("mov x2, x1"); // move the C string length into mixed value_hi + emitter.instruction("mov x1, x0"); // move the C string pointer into mixed value_lo + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the string payload for eval + + label_c_global(emitter, "__elephc_eval_value_abs"); + emitter.instruction("b __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + + label_c_global(emitter, "__elephc_eval_value_ceil"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing ceil + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for ceil + emitter.bl_c("ceil"); + emitter.instruction("fmov x1, d0"); // move the ceil result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the ceil result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the ceil wrapper frame + emitter.instruction("ret"); // return the boxed ceil result to Rust + + label_c_global(emitter, "__elephc_eval_value_floor"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing floor + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for floor + emitter.bl_c("floor"); + emitter.instruction("fmov x1, d0"); // move the floor result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the floor result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the floor wrapper frame + emitter.instruction("ret"); // return the boxed floor result to Rust + + label_c_global(emitter, "__elephc_eval_value_sqrt"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing sqrt + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for sqrt + emitter.bl_c("sqrt"); + emitter.instruction("fmov x1, d0"); // move the sqrt result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the sqrt result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the sqrt wrapper frame + emitter.instruction("ret"); // return the boxed sqrt result to Rust + + label_c_global(emitter, "__elephc_eval_value_strrev"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and reversing + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_string"); // cast the boxed eval argument to a PHP string pair + emitter.instruction("bl __rt_strrev"); // reverse the PHP byte string into concat storage + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the reversed string for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the strrev wrapper frame + emitter.instruction("ret"); // return the boxed reversed string to Rust + + label_c_global(emitter, "__elephc_eval_value_fdiv"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d0, d0, d1"); // compute fdiv() with IEEE zero handling + emitter.instruction("fcmp d0, d0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("b.vs __elephc_eval_value_fdiv_nan"); // normalize unordered fdiv results before boxing + emitter.instruction("fmov x1, d0"); // move the fdiv result bits into mixed value_lo + emitter.instruction("b __elephc_eval_value_fdiv_box"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fdiv_nan"); + emitter.instruction("mov x1, xzr"); // start the canonical quiet NaN payload from zero bits + emitter.instruction("movk x1, #0x7ff8, lsl #48"); // install the positive quiet NaN exponent/significand + emitter.label("__elephc_eval_value_fdiv_box"); + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the fdiv result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the fdiv wrapper frame + emitter.instruction("ret"); // return the boxed fdiv result to Rust + + label_c_global(emitter, "__elephc_eval_value_fmod"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d2, d0, d1"); // compute the fmod quotient before truncation + emitter.instruction("frintz d2, d2"); // truncate the quotient toward zero + emitter.instruction("fmsub d0, d2, d1, d0"); // compute dividend minus truncated quotient times divisor + emitter.instruction("fcmp d0, d0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("b.vs __elephc_eval_value_fmod_nan"); // normalize unordered fmod results before boxing + emitter.instruction("fmov x1, d0"); // move the fmod result bits into mixed value_lo + emitter.instruction("b __elephc_eval_value_fmod_box"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fmod_nan"); + emitter.instruction("mov x1, xzr"); // start the canonical quiet NaN payload from zero bits + emitter.instruction("movk x1, #0x7ff8, lsl #48"); // install the positive quiet NaN exponent/significand + emitter.label("__elephc_eval_value_fmod_box"); + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the fmod result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the fmod wrapper frame + emitter.instruction("ret"); // return the boxed fmod result to Rust + + label_c_global(emitter, "__elephc_eval_value_add"); + emitter.instruction("b __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_sub"); + emitter.instruction("b __rt_mixed_numeric_sub"); // subtract two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_mul"); + emitter.instruction("b __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_div"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fcmp d0, #0.0"); // detect division by zero before the hardware divide + emitter.instruction("b.eq __elephc_eval_value_div_null"); // return null until eval has throwable propagation + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d0, d0, d1"); // compute PHP division as a double result + emitter.instruction("fmov x1, d0"); // move the double bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the division result into a Mixed cell + emitter.instruction("b __elephc_eval_value_div_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_div_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for division by zero + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported division-by-zero propagation + emitter.label("__elephc_eval_value_div_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the division wrapper frame + emitter.instruction("ret"); // return the boxed division result to Rust + + label_c_global(emitter, "__elephc_eval_value_mod"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left integer + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("str x0, [sp, #8]"); // save the left integer across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("cbz x0, __elephc_eval_value_mod_null"); // return null until eval has throwable propagation + emitter.instruction("mov x2, x0"); // keep the integer divisor in x2 + emitter.instruction("ldr x1, [sp, #8]"); // reload the integer dividend into x1 + emitter.instruction("sdiv x3, x1, x2"); // compute the signed integer quotient + emitter.instruction("msub x1, x3, x2, x1"); // compute dividend - quotient * divisor + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the modulo result into a Mixed cell + emitter.instruction("b __elephc_eval_value_mod_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_mod_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for modulo by zero + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported modulo-by-zero propagation + emitter.label("__elephc_eval_value_mod_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the modulo wrapper frame + emitter.instruction("ret"); // return the boxed modulo result to Rust + + label_c_global(emitter, "__elephc_eval_value_pow"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the exponentiation base across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // move the exponent into libc pow's second argument + emitter.instruction("ldr d0, [sp, #8]"); // reload the base into libc pow's first argument + emitter.bl_c("pow"); + emitter.instruction("fmov x1, d0"); // move the pow result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the exponentiation result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the exponentiation wrapper frame + emitter.instruction("ret"); // return the boxed exponentiation result to Rust + + label_c_global(emitter, "__elephc_eval_value_round"); + emitter.instruction("sub sp, sp, #48"); // allocate wrapper slots for precision state and saved doubles + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the optional precision cell while casting the value + emitter.instruction("str x2, [sp, #8]"); // save whether the caller supplied a precision argument + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double + emitter.instruction("ldr x2, [sp, #8]"); // reload the precision-presence flag after the value cast + emitter.instruction("cbnz x2, __elephc_eval_value_round_precision"); // use the precision path when a second argument is present + emitter.bl_c("round"); + emitter.instruction("b __elephc_eval_value_round_box"); // box the default-precision round result + emitter.label("__elephc_eval_value_round_precision"); + emitter.instruction("str d0, [sp, #16]"); // save the original value while casting the precision + emitter.instruction("ldr x0, [sp, #0]"); // reload the precision cell for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the optional precision to a PHP integer + emitter.instruction("scvtf d1, x0"); // convert the precision to a floating exponent for pow + emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the precision multiplier base + emitter.bl_c("pow"); + emitter.instruction("ldr d1, [sp, #16]"); // reload the original value after pow returns the multiplier + emitter.instruction("fmul d1, d1, d0"); // scale the value by the precision multiplier + emitter.instruction("str d0, [sp, #24]"); // save the multiplier for rescaling after round + emitter.instruction("fmov d0, d1"); // move the scaled value into the round argument + emitter.bl_c("round"); + emitter.instruction("ldr d1, [sp, #24]"); // reload the precision multiplier for rescaling + emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to requested precision + emitter.label("__elephc_eval_value_round_box"); + emitter.instruction("fmov x1, d0"); // move the round result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the round result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the round wrapper frame + emitter.instruction("ret"); // return the boxed round result to Rust + + label_c_global(emitter, "__elephc_eval_value_bit_not"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame for the cast helper call + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across the cast + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_int"); // cast the boxed operand to a PHP integer + emitter.instruction("mvn x1, x0"); // compute bitwise complement of the integer payload + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the bitwise NOT result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the bitwise NOT wrapper frame + emitter.instruction("ret"); // return the boxed bitwise NOT result to Rust + + label_c_global(emitter, "__elephc_eval_value_bitwise"); + emitter.instruction("sub sp, sp, #48"); // allocate wrapper slots for right operand, opcode, and left integer + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("str x2, [sp, #8]"); // save the eval bitwise opcode across helper calls + emitter.instruction("bl __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("str x0, [sp, #16]"); // save the left integer across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("ldr x1, [sp, #16]"); // reload the left integer into the payload register + emitter.instruction("ldr x2, [sp, #8]"); // reload the eval bitwise opcode for dispatch + emitter.instruction("cmp x2, #0"); // is this integer bitwise AND? + emitter.instruction("b.eq __elephc_eval_value_bitwise_and"); // route opcode 0 to integer AND + emitter.instruction("cmp x2, #1"); // is this integer bitwise OR? + emitter.instruction("b.eq __elephc_eval_value_bitwise_or"); // route opcode 1 to integer OR + emitter.instruction("cmp x2, #2"); // is this integer bitwise XOR? + emitter.instruction("b.eq __elephc_eval_value_bitwise_xor"); // route opcode 2 to integer XOR + emitter.instruction("cmp x2, #3"); // is this integer left shift? + emitter.instruction("b.eq __elephc_eval_value_bitwise_shl"); // route opcode 3 to integer left shift + emitter.instruction("cmp x2, #4"); // is this integer right shift? + emitter.instruction("b.eq __elephc_eval_value_bitwise_shr"); // route opcode 4 to integer right shift + emitter.instruction("b __elephc_eval_value_bitwise_null"); // fail closed for unknown bitwise opcodes + emitter.label("__elephc_eval_value_bitwise_and"); + emitter.instruction("and x1, x1, x0"); // compute integer bitwise AND + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_or"); + emitter.instruction("orr x1, x1, x0"); // compute integer bitwise OR + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_xor"); + emitter.instruction("eor x1, x1, x0"); // compute integer bitwise XOR + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_shl"); + emitter.instruction("cmp x0, #0"); // negative shift counts are runtime errors in PHP + emitter.instruction("b.lt __elephc_eval_value_bitwise_null"); // return null until eval has throwable propagation + emitter.instruction("lsl x1, x1, x0"); // shift the integer payload left + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_shr"); + emitter.instruction("cmp x0, #0"); // negative shift counts are runtime errors in PHP + emitter.instruction("b.lt __elephc_eval_value_bitwise_null"); // return null until eval has throwable propagation + emitter.instruction("asr x1, x1, x0"); // shift the integer payload right arithmetically + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_box"); + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the bitwise result into a Mixed cell + emitter.instruction("b __elephc_eval_value_bitwise_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_bitwise_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for unsupported bitwise errors + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported bitwise error propagation + emitter.label("__elephc_eval_value_bitwise_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the bitwise wrapper frame + emitter.instruction("ret"); // return the boxed bitwise result to Rust + + label_c_global(emitter, "__elephc_eval_value_concat"); + emitter.instruction("sub sp, sp, #64"); // allocate wrapper frame for the right operand and string pairs + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #48"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_string"); // cast the left boxed operand to a PHP string pair + emitter.instruction("stp x1, x2, [sp, #8]"); // save the left string pointer and length + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for string casting + emitter.instruction("bl __rt_mixed_cast_string"); // cast the right boxed operand to a PHP string pair + emitter.instruction("mov x3, x1"); // move the right string pointer into concat's right pointer register + emitter.instruction("mov x4, x2"); // move the right string length into concat's right length register + emitter.instruction("ldp x1, x2, [sp, #8]"); // reload the left string pair for concat + emitter.instruction("bl __rt_concat"); // concatenate the two PHP string pairs + emitter.instruction("mov x0, #1"); // runtime tag 1 = string for boxing the concat result + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the concatenated string + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the concat wrapper frame + emitter.instruction("ret"); // return the boxed concat result to Rust + + label_c_global(emitter, "__elephc_eval_value_compare"); + emitter.instruction("sub sp, sp, #64"); // allocate a wrapper frame for comparison operands and opcode + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across comparison helpers + emitter.instruction("add x29, sp, #48"); // establish a stable comparison wrapper frame + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand for later casts + emitter.instruction("str x2, [sp, #8]"); // save the eval comparison opcode + emitter.instruction("str x0, [sp, #16]"); // save the left boxed operand for equality helper calls + emitter.instruction("cmp x2, #0"); // is this loose equality? + emitter.instruction("b.eq __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper + emitter.instruction("cmp x2, #1"); // is this loose inequality? + emitter.instruction("b.eq __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("cmp x2, #6"); // is this strict equality? + emitter.instruction("b.eq __elephc_eval_value_compare_strict_eq"); // route === through the mixed strict-equality helper + emitter.instruction("cmp x2, #7"); // is this strict inequality? + emitter.instruction("b.eq __elephc_eval_value_compare_strict_ne"); // route !== through the mixed strict-equality helper + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double + emitter.instruction("str d0, [sp, #24]"); // save the normalized left numeric operand + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a numeric comparison double + emitter.instruction("ldr d1, [sp, #24]"); // reload the left numeric operand for the float comparison + emitter.instruction("ldr x9, [sp, #8]"); // reload the eval comparison opcode for dispatch + emitter.instruction("cmp x9, #2"); // is this a less-than comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_lt"); // materialize left < right from float comparison flags + emitter.instruction("cmp x9, #3"); // is this a less-than-or-equal comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_lte"); // materialize left <= right from float comparison flags + emitter.instruction("cmp x9, #4"); // is this a greater-than comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_gt"); // materialize left > right from float comparison flags + emitter.instruction("cmp x9, #5"); // is this a greater-than-or-equal comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_gte"); // materialize left >= right from float comparison flags + emitter.instruction("mov x1, #0"); // unknown comparison opcodes fail closed as false + emitter.instruction("b __elephc_eval_value_compare_box"); // box the fallback false result + emitter.label("__elephc_eval_value_compare_eq"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for loose equality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for loose equality + emitter.instruction("bl __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality + emitter.instruction("mov x1, x0"); // move equality into the bool payload register + emitter.instruction("b __elephc_eval_value_compare_box"); // box the equality result + emitter.label("__elephc_eval_value_compare_ne"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for loose inequality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for loose inequality + emitter.instruction("bl __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion + emitter.instruction("eor x1, x0, #1"); // invert equality for the != operator + emitter.instruction("b __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_strict_eq"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for strict equality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for strict equality + emitter.instruction("bl __rt_mixed_strict_eq"); // compute PHP strict equality + emitter.instruction("mov x1, x0"); // move strict equality into the bool payload register + emitter.instruction("b __elephc_eval_value_compare_box"); // box the strict-equality result + emitter.label("__elephc_eval_value_compare_strict_ne"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for strict inequality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for strict inequality + emitter.instruction("bl __rt_mixed_strict_eq"); // compute PHP strict equality before inversion + emitter.instruction("eor x1, x0, #1"); // invert equality for the !== operator + emitter.instruction("b __elephc_eval_value_compare_box"); // box the strict-inequality result + emitter.label("__elephc_eval_value_compare_lt"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for < + emitter.instruction("cset x1, mi"); // ordered less-than becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the less-than result + emitter.label("__elephc_eval_value_compare_lte"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for <= + emitter.instruction("cset x1, ls"); // ordered less-than-or-equal becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the less-than-or-equal result + emitter.label("__elephc_eval_value_compare_gt"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for > + emitter.instruction("cset x1, gt"); // ordered greater-than becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the greater-than result + emitter.label("__elephc_eval_value_compare_gte"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for >= + emitter.instruction("cset x1, ge"); // ordered greater-than-or-equal becomes boolean true + emitter.label("__elephc_eval_value_compare_box"); + emitter.instruction("mov x0, #3"); // runtime tag 3 = bool + emitter.instruction("mov x2, xzr"); // bool payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the comparison result as a Mixed bool + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the comparison wrapper frame + emitter.instruction("ret"); // return the boxed comparison result to Rust + + emitter.label("__elephc_eval_mixed_loose_eq"); + emitter.instruction("sub sp, sp, #96"); // allocate helper slots for unboxed tags, payloads, and casts + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across mixed helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable loose-equality helper frame + emitter.instruction("stp x0, x1, [sp, #0]"); // save incoming boxed operands for later casts + emitter.instruction("bl __rt_mixed_unbox"); // unbox the left eval operand into tag and payload words + emitter.instruction("str x0, [sp, #16]"); // save the left runtime tag + emitter.instruction("stp x1, x2, [sp, #24]"); // save the left payload words + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for unboxing + emitter.instruction("bl __rt_mixed_unbox"); // unbox the right eval operand into tag and payload words + emitter.instruction("str x0, [sp, #40]"); // save the right runtime tag + emitter.instruction("stp x1, x2, [sp, #48]"); // save the right payload words + emitter.instruction("ldr x9, [sp, #16]"); // reload the left runtime tag for equality dispatch + emitter.instruction("cmp x9, #3"); // does the left operand have PHP bool semantics? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp x0, #3"); // does the right operand have PHP bool semantics? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp x9, x0"); // do the operands have the same runtime tag? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_same_tag"); // same-tag scalars use focused payload comparisons + emitter.instruction("cmp x9, #8"); // is the left operand null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp x0, #8"); // is the right operand null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp x9, #1"); // is a non-matching left operand a string? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string"); // compare numeric strings against numeric scalars + emitter.instruction("cmp x0, #1"); // is a non-matching right operand a string? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string"); // compare numeric strings against numeric scalars + emitter.instruction("b __elephc_eval_mixed_loose_eq_numeric"); // remaining scalar mismatches compare numerically + emitter.label("__elephc_eval_mixed_loose_eq_same_tag"); + emitter.instruction("cmp x9, #8"); // are both operands null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_true"); // null loosely equals null + emitter.instruction("cmp x9, #1"); // are both operands strings? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_strings"); // strings use PHP loose string equality + emitter.instruction("cmp x9, #2"); // are both operands floats? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_floats"); // floats compare with native floating equality + emitter.instruction("ldr x10, [sp, #24]"); // reload the left low payload word + emitter.instruction("ldr x11, [sp, #48]"); // reload the right low payload word + emitter.instruction("cmp x10, x11"); // compare low payload words for int and pointer-like scalars + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_false"); // mismatched low payloads are not equal + emitter.instruction("ldr x10, [sp, #32]"); // reload the left high payload word + emitter.instruction("ldr x11, [sp, #56]"); // reload the right high payload word + emitter.instruction("cmp x10, x11"); // compare high payload words for pointer-like scalars + emitter.instruction("cset x0, eq"); // materialize same-tag payload equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the payload equality result + emitter.label("__elephc_eval_mixed_loose_eq_strings"); + emitter.instruction("ldp x1, x2, [sp, #24]"); // reload the left string pointer and length + emitter.instruction("ldp x3, x4, [sp, #48]"); // reload the right string pointer and length + emitter.instruction("bl __rt_str_loose_eq"); // compare strings with PHP loose numeric-string rules + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_floats"); + emitter.instruction("ldr d1, [sp, #24]"); // reload the left float payload + emitter.instruction("ldr d0, [sp, #48]"); // reload the right float payload + emitter.instruction("fcmp d1, d0"); // compare same-tag float payloads + emitter.instruction("cset x0, eq"); // floats loosely equal only when ordered equal + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the float equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_null"); + emitter.instruction("cmp x0, #1"); // is null being compared with a string? + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("ldr x10, [sp, #56]"); // load the right string length + emitter.instruction("cmp x10, #0"); // null loosely equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the null/string equality result + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the null/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_null"); + emitter.instruction("cmp x9, #1"); // is null being compared with a string? + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("ldr x10, [sp, #32]"); // load the left string length + emitter.instruction("cmp x10, #0"); // null loosely equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the string/null equality result + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string/null equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_string"); + emitter.instruction("cmp x0, #0"); // can the right operand be compared numerically as an int? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("cmp x0, #2"); // can the right operand be compared numerically as a float? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_left_string_numeric"); + emitter.instruction("ldp x1, x2, [sp, #24]"); // reload the left string pointer and length for numeric parsing + emitter.instruction("bl __rt_str_to_number"); // parse the left string under PHP numeric-string rules + emitter.instruction("cbz x0, __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("str d0, [sp, #64]"); // save the parsed left numeric-string value + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the parsed left numeric-string value + emitter.instruction("fcmp d1, d0"); // compare parsed string and numeric scalar values + emitter.instruction("cset x0, eq"); // materialize string/numeric loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string/numeric equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_string"); + emitter.instruction("cmp x9, #0"); // can the left operand be compared numerically as an int? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("cmp x9, #2"); // can the left operand be compared numerically as a float? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_right_string_numeric"); + emitter.instruction("ldp x1, x2, [sp, #48]"); // reload the right string pointer and length for numeric parsing + emitter.instruction("bl __rt_str_to_number"); // parse the right string under PHP numeric-string rules + emitter.instruction("cbz x0, __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("str d0, [sp, #64]"); // save the parsed right numeric-string value + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the parsed right numeric-string value + emitter.instruction("fcmp d0, d1"); // compare numeric scalar and parsed string values + emitter.instruction("cset x0, eq"); // materialize numeric/string loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the numeric/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_bool"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for truthiness + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the left operand to PHP truthiness + emitter.instruction("str x0, [sp, #64]"); // save the left truthiness result + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for truthiness + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the right operand to PHP truthiness + emitter.instruction("ldr x9, [sp, #64]"); // reload the left truthiness result + emitter.instruction("cmp x9, x0"); // compare boolean truthiness for loose equality + emitter.instruction("cset x0, eq"); // materialize bool loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the bool loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_numeric"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for numeric equality + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("str d0, [sp, #64]"); // save the left numeric equality operand + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for numeric equality + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the left numeric equality operand + emitter.instruction("fcmp d1, d0"); // compare numeric operands for loose equality + emitter.instruction("cset x0, eq"); // materialize numeric loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the numeric loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_true"); + emitter.instruction("mov x0, #1"); // materialize true for loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the true result + emitter.label("__elephc_eval_mixed_loose_eq_false"); + emitter.instruction("mov x0, #0"); // materialize false for loose equality + emitter.label("__elephc_eval_mixed_loose_eq_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the loose-equality helper frame + emitter.instruction("ret"); // return the loose-equality boolean in x0 + + label_c_global(emitter, "__elephc_eval_value_spaceship"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left numeric spaceship operand + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("ldr d1, [sp, #8]"); // reload the left numeric spaceship operand + emitter.instruction("fcmp d1, d0"); // compare left and right numeric operands for spaceship + emitter.instruction("b.vs __elephc_eval_value_spaceship_gt"); // PHP treats unordered NaN spaceship comparisons as greater + emitter.instruction("cset x1, gt"); // set result to 1 when left is greater than right + emitter.instruction("csinv x1, x1, xzr, ge"); // keep 1/0 for greater/equal, or produce -1 for less + emitter.instruction("b __elephc_eval_value_spaceship_box"); // box the ordered spaceship result + emitter.label("__elephc_eval_value_spaceship_gt"); + emitter.instruction("mov x1, #1"); // greater or unordered comparisons produce result 1 + emitter.label("__elephc_eval_value_spaceship_box"); + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the spaceship result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the spaceship wrapper frame + emitter.instruction("ret"); // return the boxed spaceship result to Rust + + label_c_global(emitter, "__elephc_eval_value_echo"); + emitter.instruction("b __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string_bytes"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for output pointers + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across string casting + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the caller's out_ptr storage address + emitter.instruction("str x2, [sp, #8]"); // save the caller's out_len storage address + emitter.instruction("bl __rt_mixed_cast_string"); // cast the boxed eval value to a PHP string pair + emitter.instruction("ldr x9, [sp, #0]"); // reload the optional out_ptr storage address + emitter.instruction("cbz x9, __elephc_eval_value_string_bytes_len"); // skip pointer storage when the caller passed null + emitter.instruction("str x1, [x9]"); // store the string pointer for Rust to copy immediately + emitter.label("__elephc_eval_value_string_bytes_len"); + emitter.instruction("ldr x10, [sp, #8]"); // reload the optional out_len storage address + emitter.instruction("cbz x10, __elephc_eval_value_string_bytes_done"); // skip length storage when the caller passed null + emitter.instruction("str x2, [x10]"); // store the string byte length for Rust + emitter.label("__elephc_eval_value_string_bytes_done"); + emitter.instruction("mov x0, #1"); // report successful string conversion to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-bytes wrapper frame + emitter.instruction("ret"); // return the success flag to Rust + + label_c_global(emitter, "__elephc_eval_value_truthy"); + emitter.instruction("b __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + + label_c_global(emitter, "__elephc_eval_value_retain"); + emitter.instruction("b __rt_incref"); // retain one eval-owned boxed Mixed cell + + label_c_global(emitter, "__elephc_eval_value_final_object_identity"); + emitter.instruction("cbz x0, __elephc_eval_value_final_object_none"); // null handles cannot release an object + emitter.label("__elephc_eval_value_final_object_loop"); + emitter.instruction("ldr w9, [x0, #-12]"); // load the current Mixed wrapper refcount + emitter.instruction("cmp w9, #1"); // only the last Mixed owner can free the wrapped payload + emitter.instruction("b.ne __elephc_eval_value_final_object_none"); // non-final wrapper releases cannot run destructors yet + emitter.instruction("ldr x9, [x0]"); // load the current Mixed runtime tag + emitter.instruction("cmp x9, #7"); // tag 7 means the payload is another boxed Mixed + emitter.instruction("b.ne __elephc_eval_value_final_object_check_object"); // concrete payloads can be tested for object ownership + emitter.instruction("ldr x0, [x0, #8]"); // follow the nested Mixed payload pointer + emitter.instruction("cbnz x0, __elephc_eval_value_final_object_loop"); // continue while nested Mixed payloads are present + emitter.instruction("b __elephc_eval_value_final_object_none"); // null nested payloads cannot release an object + emitter.label("__elephc_eval_value_final_object_check_object"); + emitter.instruction("cmp x9, #6"); // tag 6 means the concrete payload is a PHP object + emitter.instruction("b.ne __elephc_eval_value_final_object_none"); // non-object releases have no dynamic destructor + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_final_object_none"); // null object payloads are treated as non-final + emitter.instruction("ldr w10, [x9, #-12]"); // load the object refcount that the Mixed release will decrement + emitter.instruction("cmp w10, #1"); // only the final object owner can run the destructor + emitter.instruction("csel x0, x9, xzr, eq"); // return object identity only for the final release + emitter.instruction("ret"); // return the candidate object identity to Rust + emitter.label("__elephc_eval_value_final_object_none"); + emitter.instruction("mov x0, xzr"); // report that no dynamic destructor should run + emitter.instruction("ret"); // return zero to Rust + + label_c_global(emitter, "__elephc_eval_warning"); + emitter.instruction("mov x2, x1"); // move warning length into the runtime diagnostic length register + emitter.instruction("mov x1, x0"); // move warning pointer into the runtime diagnostic buffer register + emitter.instruction("b __rt_diag_warning"); // emit or suppress one eval runtime warning + + label_c_global(emitter, "__elephc_eval_value_release"); + emitter.instruction("b __rt_decref_mixed"); // release one eval-owned boxed Mixed cell +} + +/// Emits Linux x86_64 C-ABI wrappers around the internal mixed value helpers. +fn emit_x86_64_wrappers(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the null payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_bool"); + emitter.instruction("xor r10d, r10d"); // prepare the normalized PHP bool payload + emitter.instruction("test rdi, rdi"); // treat any non-zero C bool argument as true + emitter.instruction("setne r10b"); // bool payload is 1 for true and 0 for false + emitter.instruction("mov rdi, r10"); // move the normalized bool into mixed value_lo + emitter.instruction("mov eax, 3"); // runtime tag 3 = bool + emitter.instruction("xor esi, esi"); // bool payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the bool payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_new_object"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable dynamic-object wrapper frame + emitter.instruction("sub rsp, 16"); // reserve slots for the raw object and boxed result + emitter.instruction("cmp rsi, 8"); // stdClass has an 8-byte class name + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // use the generic factory for non-stdClass lengths + emitter.instruction("cmp BYTE PTR [rdi], 115"); // byte 0 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 0 differs + emitter.instruction("cmp BYTE PTR [rdi + 1], 116"); // byte 1 must be 't' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 1 differs + emitter.instruction("cmp BYTE PTR [rdi + 2], 100"); // byte 2 must be 'd' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 2 differs + emitter.instruction("cmp BYTE PTR [rdi + 3], 67"); // byte 3 must be 'C' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 3 differs + emitter.instruction("cmp BYTE PTR [rdi + 4], 108"); // byte 4 must be 'l' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 4 differs + emitter.instruction("cmp BYTE PTR [rdi + 5], 97"); // byte 5 must be 'a' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 5 differs + emitter.instruction("cmp BYTE PTR [rdi + 6], 115"); // byte 6 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 6 differs + emitter.instruction("cmp BYTE PTR [rdi + 7], 115"); // byte 7 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 7 differs + emitter.instruction("call __rt_stdclass_new"); // allocate stdClass with its dynamic-property hash + emitter.instruction("jmp __elephc_eval_value_new_object_box_x86"); // box the stdClass object for Rust + emitter.label("__elephc_eval_value_new_object_generic_x86"); + emitter.instruction("mov rax, rdi"); // move the C class-name pointer into new_by_name's string ABI + emitter.instruction("mov rdx, rsi"); // move the C class-name length into new_by_name's string ABI + emitter.instruction("call __rt_new_by_name"); // allocate the named AOT class object, or return null on miss + emitter.instruction("test rax, rax"); // did the runtime class-name lookup allocate an object? + emitter.instruction("jz __elephc_eval_value_new_object_null_x86"); // box PHP null when no runtime class matched the eval name + emitter.label("__elephc_eval_value_new_object_box_x86"); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the raw object owner before boxing it for eval + emitter.instruction("mov rdi, rax"); // move the allocated object pointer into the Mixed payload + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the boxed Mixed while consuming the raw object owner + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the raw object owner created by the allocator + emitter.instruction("mov r10d, DWORD PTR [rax - 12]"); // load the raw object refcount after Mixed boxing retained it + emitter.instruction("sub r10d, 1"); // consume the allocator-owned object reference locally + emitter.instruction("mov DWORD PTR [rax - 12], r10d"); // leave the boxed Mixed as the sole object owner + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // restore the boxed object Mixed as the Rust return value + emitter.instruction("jmp __elephc_eval_value_new_object_done_x86"); // skip the null boxing path after successful allocation + emitter.label("__elephc_eval_value_new_object_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unknown eval class names + emitter.label("__elephc_eval_value_new_object_done_x86"); + emitter.instruction("mov rsp, rbp"); // release the dynamic-object wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + + emit_x86_64_object_from_raw_wrapper(emitter); + emit_x86_64_install_dynamic_object_destructor_hook(emitter); + emit_x86_64_object_clone_shallow_wrapper(emitter); + + label_c_global(emitter, "__elephc_eval_class_exists"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable class-exists frame pointer + emitter.instruction("sub rsp, 48"); // reserve slots for name, count, cursor, and index + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_classes_by_name_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the registered class-name count + emitter.instruction("test r10, r10"); // is the class-name table empty? + emitter.instruction("jz __elephc_eval_class_exists_miss_x86"); // an empty table cannot contain the requested class + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", "_classes_by_name"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning at table index zero + emitter.label("__elephc_eval_class_exists_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-name table count + emitter.instruction("cmp r11, r10"); // have all class-name entries been scanned? + emitter.instruction("jae __elephc_eval_class_exists_miss_x86"); // no class matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_class_exists_skip_x86"); // length mismatch means this entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the table index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the table index after the string compare + emitter.instruction("test rax, rax"); // did the requested class name match this entry? + emitter.instruction("je __elephc_eval_class_exists_hit_x86"); // report true on a class-name match + emitter.label("__elephc_eval_class_exists_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("add r10, 32"); // advance to the next class-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the table index + emitter.instruction("jmp __elephc_eval_class_exists_loop_x86"); // continue scanning the class-name table + emitter.label("__elephc_eval_class_exists_hit_x86"); + emitter.instruction("mov eax, 1"); // return true for a matched class name + emitter.instruction("jmp __elephc_eval_class_exists_done_x86"); // skip the false result after a match + emitter.label("__elephc_eval_class_exists_miss_x86"); + emitter.instruction("xor eax, eax"); // return false when no class-name entry matched + emitter.label("__elephc_eval_class_exists_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the class-exists flag to Rust + + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_interface_exists", + "_interface_names_count", + "_interface_names", + "__elephc_eval_interface_exists_x86", + ); + + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_trait_exists", + "_trait_names_count", + "_trait_names", + "__elephc_eval_trait_exists_x86", + ); + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_enum_exists", + "_enum_names_count", + "_enum_names", + "__elephc_eval_enum_exists_x86", + ); + + emit_x86_64_eval_reflection_method_names(emitter); + emit_x86_64_eval_reflection_property_names(emitter); + emit_x86_64_eval_reflection_class_interface_names(emitter); + emit_x86_64_eval_reflection_class_trait_names(emitter); + emit_x86_64_eval_reflection_class_trait_alias_names(emitter); + emit_x86_64_eval_reflection_class_trait_alias_sources(emitter); + emit_x86_64_eval_reflection_source_file(emitter); + emit_x86_64_eval_reflection_class_flags(emitter); + emit_x86_64_eval_reflection_method_flags(emitter); + emit_x86_64_eval_reflection_method_declaring_class(emitter); + emit_x86_64_eval_reflection_property_declaring_class(emitter); + emit_x86_64_eval_reflection_property_flags(emitter); + + label_c_global(emitter, "__elephc_eval_value_is_a"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime match helpers + emitter.instruction("mov rbp, rsp"); // establish a stable is-a relation frame pointer + emitter.instruction("sub rsp, 48"); // reserve slots for value pointer, flags, and target metadata + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed eval object-or-class cell + emitter.instruction("mov QWORD PTR [rbp - 16], rcx"); // save whether exact class matches should be rejected + emitter.instruction("mov rax, rsi"); // move the target string pointer into the lookup ABI register + emitter.instruction("call __rt_instanceof_lookup"); // resolve the target class/interface string to matcher metadata + emitter.instruction("test rax, rax"); // did the target string resolve to emitted metadata? + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // unresolved targets cannot match eval object values + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the target class/interface id + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the target kind: 0 class, 1 interface + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed eval value for unboxing + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the eval value is an object + emitter.instruction("je __elephc_eval_value_is_a_object_x86"); // object values can use their concrete runtime class id + emitter.instruction("cmp rax, 1"); // runtime tag 1 means the eval value is a class string + emitter.instruction("je __elephc_eval_value_is_a_string_x86"); // class-string values need source metadata lookup + emitter.instruction("jmp __elephc_eval_value_is_a_false_x86"); // other runtime tags cannot satisfy class relations + emitter.label("__elephc_eval_value_is_a_string_x86"); + emitter.instruction("mov rax, rdi"); // pass the source class-string pointer to the metadata lookup + emitter.instruction("call __rt_instanceof_lookup"); // resolve the source class string to matcher metadata + emitter.instruction("test rax, rax"); // did the source string resolve to emitted metadata? + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // unresolved source strings cannot match relation metadata + emitter.instruction("test rdx, rdx"); // source strings must resolve to concrete classes for this matcher + emitter.instruction("jne __elephc_eval_value_is_a_false_x86"); // interface-source strings need a dedicated interface-parent matcher + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // build a fake object header containing the source class id + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // does this call reject exact concrete-class matches? + emitter.instruction("je __elephc_eval_value_is_a_string_match_x86"); // is_a() allows exact class-string matches + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // is the target a concrete class rather than an interface? + emitter.instruction("jne __elephc_eval_value_is_a_string_match_x86"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("cmp rdi, QWORD PTR [rbp - 24]"); // compare source and target class ids for subclass self exclusion + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // is_subclass_of() excludes the exact class string + emitter.label("__elephc_eval_value_is_a_string_match_x86"); + emitter.instruction("lea rdi, [rbp - 40]"); // pass the fake object header to the metadata matcher + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the target class/interface id + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("call __rt_exception_matches"); // test class-string inheritance or implemented interfaces + emitter.instruction("jmp __elephc_eval_value_is_a_done_x86"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_object_x86"); + emitter.instruction("test rdi, rdi"); // check the unboxed object pointer before reading its header + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // malformed object payloads cannot match class metadata + emitter.instruction("mov r8, rdi"); // keep the unboxed object pointer for matcher input + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // does this call reject exact concrete-class matches? + emitter.instruction("je __elephc_eval_value_is_a_match_x86"); // is_a() allows exact class matches + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // is the target a concrete class rather than an interface? + emitter.instruction("jne __elephc_eval_value_is_a_match_x86"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("mov r9, QWORD PTR [r8]"); // load the object's concrete runtime class id + emitter.instruction("cmp r9, QWORD PTR [rbp - 24]"); // compare object and target class ids for subclass self exclusion + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // is_subclass_of() excludes the object's exact class + emitter.label("__elephc_eval_value_is_a_match_x86"); + emitter.instruction("mov rdi, r8"); // pass the unboxed object pointer to the metadata matcher + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the target class/interface id + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("call __rt_exception_matches"); // test inheritance or implemented-interface metadata + emitter.instruction("jmp __elephc_eval_value_is_a_done_x86"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_false_x86"); + emitter.instruction("xor eax, eax"); // return false for unresolved, scalar, or exact-self subclass cases + emitter.label("__elephc_eval_value_is_a_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard relation lookup spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boolean class-relation result to Rust + + label_c_global(emitter, "__elephc_eval_value_object_class_name"); + emitter.instruction("test rdi, rdi"); // reject null boxed handles before reading their tag + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // null handles cannot provide a class name + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed eval value runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 is an object payload + emitter.instruction("jne __elephc_eval_value_object_class_name_miss_x86"); // non-objects cannot provide a class name + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // check the unboxed object pointer before dereferencing it + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // reject malformed object payloads + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the object's runtime class id + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check whether the class id is in table bounds + emitter.instruction("jae __elephc_eval_value_object_class_name_miss_x86"); // reject missing or out-of-range class ids + abi::emit_symbol_address(emitter, "rdx", "_class_name_entries"); + emitter.instruction("shl r11, 4"); // convert class id to a 16-byte table-entry offset + emitter.instruction("add rdx, r11"); // address the class-name entry for this class id + emitter.instruction("mov rdi, QWORD PTR [rdx]"); // load the class-name string pointer + emitter.instruction("mov rsi, QWORD PTR [rdx + 8]"); // load the class-name string length + emitter.instruction("test rsi, rsi"); // table holes use a zero-length name + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // reject table holes with empty names + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the class-name string for Rust + emitter.label("__elephc_eval_value_object_class_name_miss_x86"); + emitter.instruction("xor eax, eax"); // report failure as a null C pointer to Rust + emitter.instruction("ret"); // return the failure sentinel + + label_c_global(emitter, "__elephc_eval_value_parent_class_name"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable parent-class lookup frame pointer + emitter.instruction("sub rsp, 48"); // reserve lookup state while keeping the stack call-aligned + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the eval value tag and payload words + emitter.instruction("cmp rax, 6"); // tag 6 is an object payload + emitter.instruction("je __elephc_eval_value_parent_class_name_object_x86"); // derive the parent from the object's runtime class id + emitter.instruction("cmp rax, 1"); // tag 1 is a class-name string payload + emitter.instruction("je __elephc_eval_value_parent_class_name_string_x86"); // resolve a class string through generated metadata + emitter.instruction("jmp __elephc_eval_value_parent_class_name_empty_x86"); // unsupported input types have no parent class name + emitter.label("__elephc_eval_value_parent_class_name_object_x86"); + emitter.instruction("test rdi, rdi"); // check the unboxed object pointer before reading its header + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // malformed object payloads have no parent class + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emitter.instruction("jmp __elephc_eval_value_parent_class_name_from_id_x86"); // convert the class id to its parent class name + emitter.label("__elephc_eval_value_parent_class_name_string_x86"); + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_classes_by_name_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the registered class-name count + emitter.instruction("test r10, r10"); // is the generated class-name table empty? + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // an empty class table cannot resolve a parent name + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", "_classes_by_name"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning generated class-name entries at index zero + emitter.label("__elephc_eval_value_parent_class_name_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-name table count + emitter.instruction("cmp r11, r10"); // have all generated class names been checked? + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // no generated class matched the requested string + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name metadata entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested name lengths first + emitter.instruction("jne __elephc_eval_value_parent_class_name_skip_x86"); // length mismatch means this class entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // preserve the scan index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the generated class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the scan index after the string compare + emitter.instruction("test rax, rax"); // did the requested class name match this entry? + emitter.instruction("je __elephc_eval_value_parent_class_name_hit_x86"); // resolve the matched class entry to its parent id + emitter.label("__elephc_eval_value_parent_class_name_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("add r10, 32"); // advance to the next class-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the class-name scan index + emitter.instruction("jmp __elephc_eval_value_parent_class_name_loop_x86"); // continue scanning generated class names + emitter.label("__elephc_eval_value_parent_class_name_hit_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the matched class-name table entry + emitter.instruction("mov r11, QWORD PTR [r10 + 16]"); // load the matched runtime class id + emitter.label("__elephc_eval_value_parent_class_name_from_id_x86"); + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check that the class id can index parent metadata + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // unknown class ids have no parent class name + abi::emit_symbol_address(emitter, "rdx", "_class_parent_ids"); + emitter.instruction("mov r11, QWORD PTR [rdx + r11 * 8]"); // load the parent runtime class id + emitter.instruction("cmp r11, -1"); // check whether the runtime class has no parent + emitter.instruction("je __elephc_eval_value_parent_class_name_empty_x86"); // parentless runtime classes produce an empty string + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check that the parent class id can index name metadata + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // invalid parent ids produce an empty string + abi::emit_symbol_address(emitter, "rdx", "_class_name_entries"); + emitter.instruction("shl r11, 4"); // convert parent id to a 16-byte name-entry offset + emitter.instruction("add rdx, r11"); // address the parent class-name metadata row + emitter.instruction("mov rdi, QWORD PTR [rdx]"); // load the parent class-name string pointer + emitter.instruction("mov rsi, QWORD PTR [rdx + 8]"); // load the parent class-name string length + emitter.instruction("test rsi, rsi"); // table holes represent missing parent names + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // missing parent names produce an empty string + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the parent class-name string + emitter.instruction("jmp __elephc_eval_value_parent_class_name_done_x86"); // restore the wrapper frame before returning to Rust + emitter.label("__elephc_eval_value_parent_class_name_empty_x86"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("xor edi, edi"); // missing parent names use an empty string pointer + emitter.instruction("xor esi, esi"); // missing parent names use an empty string length + emitter.instruction("call __rt_mixed_from_value"); // box the empty parent class-name string + emitter.label("__elephc_eval_value_parent_class_name_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed parent class-name string to Rust + + label_c_global(emitter, "__elephc_eval_value_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the array pointer + emitter.instruction("cmp rdi, 4"); // compare requested capacity with the minimum capacity + emitter.instruction("mov r10, 4"); // minimum indexed-array capacity for eval literals + emitter.instruction("cmovb rdi, r10"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov rsi, 8"); // Mixed indexed arrays store boxed-cell pointers + emitter.instruction("call __rt_array_new"); // allocate indexed-array storage for boxed Mixed slots + emitter.instruction("mov r10, QWORD PTR [rax - 8]"); // load the packed indexed-array heap kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // preserve heap magic, indexed-array kind, and COW metadata + emitter.instruction("and r10, r11"); // clear the default scalar value_type bits + emitter.instruction("mov r11, 7"); // runtime value_type 7 = boxed Mixed + emitter.instruction("shl r11, 8"); // move the value_type tag into the packed kind word + emitter.instruction("or r10, r11"); // stamp the array as carrying boxed Mixed slots + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // persist the updated indexed-array metadata + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned array pointer while allocating the Mixed box + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 4"); // runtime tag 4 = indexed array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned indexed-array pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the array pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // indexed arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the array-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the string-array pointer + emitter.instruction("cmp rdi, 4"); // compare requested capacity with the minimum capacity + emitter.instruction("mov r10, 4"); // minimum indexed-array capacity for eval metadata lists + emitter.instruction("cmovb rdi, r10"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov rsi, 16"); // direct string arrays store pointer/length pairs + emitter.instruction("call __rt_array_new"); // allocate indexed-array storage for direct string slots + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned direct-string array pointer while boxing it + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 4"); // runtime tag 4 = indexed array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned direct-string array pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string-array pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // indexed arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the string-array-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed direct-string array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_push"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve local slots for boxed owner and incoming string payload + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed string-array owner + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the incoming string pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the incoming string length + emitter.instruction("test rdi, rdi"); // check whether the boxed string-array handle is null + emitter.instruction("jz __elephc_eval_value_string_array_push_fail_x86"); // reject malformed null string-array handles + emitter.instruction("mov rax, rdi"); // move the boxed owner into mixed_unbox's input register + emitter.instruction("call __rt_mixed_unbox"); // expose the indexed-array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction("jne __elephc_eval_value_string_array_push_fail_x86"); // reject non-array metadata containers + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the string pointer to append + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the string length to append + emitter.instruction("call __rt_array_push_str"); // persist and append the string, returning the updated array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the boxed string-array owner + emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // update the boxed payload in case the array grew + emitter.instruction("mov rax, r10"); // return the boxed string-array owner to Rust + emitter.instruction("jmp __elephc_eval_value_string_array_push_done_x86"); // skip the malformed-input null result + emitter.label("__elephc_eval_value_string_array_push_fail_x86"); + emitter.instruction("xor eax, eax"); // report a null pointer so Rust converts it to RuntimeFatal + emitter.label("__elephc_eval_value_string_array_push_done_x86"); + emitter.instruction("add rsp, 32"); // release the string-array-push wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the updated boxed string-array handle to Rust + + label_c_global(emitter, "__elephc_eval_value_assoc_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the hash pointer + emitter.instruction("cmp rdi, 16"); // compare requested capacity with the minimum hash capacity + emitter.instruction("mov r10, 16"); // minimum hash capacity for eval associative literals + emitter.instruction("cmovb rdi, r10"); // use max(requested, 16) as the hash allocation capacity + emitter.instruction("mov rsi, 7"); // runtime value_type 7 = boxed Mixed hash values + emitter.instruction("call __rt_hash_new"); // allocate associative-array storage for boxed Mixed entries + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned hash pointer while allocating the Mixed box + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new hash + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 5"); // runtime tag 5 = associative array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned hash pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the hash pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // associative arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the assoc-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed associative-array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_get"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the boxed array receiver + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver while coercing the key + emitter.instruction("mov rdi, rsi"); // pass the boxed key to the eval key normalizer + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov rsi, rax"); // pass normalized key_lo to the reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed array receiver + emitter.instruction("call __rt_mixed_array_get"); // read the boxed Mixed element or Mixed(null) + emitter.instruction("add rsp, 16"); // release the array-get wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed element to Rust + + label_c_global(emitter, "__elephc_eval_value_array_key_exists"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver and normalized key words + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the boxed array receiver while normalizing the key + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the normalized key high word + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed array receiver for tag dispatch + emitter.instruction("test rdi, rdi"); // null handles do not contain array keys + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_key_exists_indexed"); // indexed arrays use bounds-based key existence + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_key_exists_assoc"); // associative arrays use hash existence + emitter.instruction("jmp __elephc_eval_value_array_key_exists_false"); // scalar values do not contain array keys + emitter.label("__elephc_eval_value_array_key_exists_indexed"); + emitter.instruction("cmp QWORD PTR [rbp - 24], -1"); // integer keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_array_key_exists_false"); // non-integer keys never exist in indexed arrays + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed indexed-array receiver + emitter.instruction("mov rdi, QWORD PTR [rdi + 8]"); // load the indexed-array payload pointer + emitter.instruction("test rdi, rdi"); // missing payload cannot contain a key + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for missing indexed-array payloads + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass normalized integer key to the bounds helper + emitter.instruction("call __rt_array_key_exists"); // return whether the integer key is in bounds + emitter.instruction("jmp __elephc_eval_value_array_key_exists_box"); // box the existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_assoc"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed associative-array receiver + emitter.instruction("mov rdi, QWORD PTR [rdi + 8]"); // load the hash payload pointer + emitter.instruction("test rdi, rdi"); // missing hash payload cannot contain a key + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for missing associative-array payloads + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass normalized key_lo to the hash lookup + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // pass normalized key_hi to the hash lookup + emitter.instruction("call __rt_hash_get"); // return hash found flag in rax + emitter.instruction("jmp __elephc_eval_value_array_key_exists_box"); // box the hash existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_false"); + emitter.instruction("xor eax, eax"); // report false for misses and unsupported receivers + emitter.label("__elephc_eval_value_array_key_exists_box"); + emitter.instruction("mov rdi, rax"); // move the C bool result into mixed value_lo + emitter.instruction("mov eax, 3"); // runtime tag 3 = boolean + emitter.instruction("xor esi, esi"); // boolean payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the bool result for Rust + emitter.instruction("add rsp, 32"); // release the key-exists wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bool result to Rust + + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable iterator-key wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver, target position, hash pointer, and counter + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver while walking the container + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested zero-based foreach position + emitter.instruction("test rdi, rdi"); // null handles produce a null key + emitter.instruction("jz __elephc_eval_value_array_iter_key_null"); // branch to boxed null for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_iter_key_indexed"); // indexed arrays expose integer positions as foreach keys + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_iter_key_assoc"); // associative arrays expose insertion-order hash keys + emitter.instruction("jmp __elephc_eval_value_array_iter_key_null"); // scalar values have no foreach-visible key + emitter.label("__elephc_eval_value_array_iter_key_indexed"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // use the requested foreach position as the integer key payload + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the indexed foreach key as an owned Mixed cell + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc"); + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the hash payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // null hash payloads produce a null key + emitter.instruction("jz __elephc_eval_value_array_iter_key_null"); // branch to boxed null for missing hash payloads + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the insertion-order position counter at zero + emitter.instruction("xor esi, esi"); // cursor 0 starts at the hash head entry + emitter.label("__elephc_eval_value_array_iter_key_assoc_loop"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the hash pointer before advancing the hash iterator + emitter.instruction("call __rt_hash_iter_next"); // fetch the next insertion-order hash key + emitter.instruction("cmp rax, -1"); // did the iterator report the done sentinel? + emitter.instruction("je __elephc_eval_value_array_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // load the current insertion-order position + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // load the requested foreach position + emitter.instruction("cmp r10, r11"); // is this the requested hash entry? + emitter.instruction("je __elephc_eval_value_array_iter_key_assoc_box"); // box the current hash key when the position matches + emitter.instruction("add r10, 1"); // advance the insertion-order position counter + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the updated position counter for the next probe + emitter.instruction("mov rsi, rax"); // use the returned cursor for the next hash iterator call + emitter.instruction("jmp __elephc_eval_value_array_iter_key_assoc_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_array_iter_key_assoc_box"); + emitter.instruction("cmp rdx, -1"); // integer hash keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_array_iter_key_assoc_string"); // string hash keys need string-tag boxing + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the associative integer key as Mixed + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc_string"); + emitter.instruction("mov rsi, rdx"); // move the string key length into the boxing high word + emitter.instruction("mov eax, 1"); // runtime tag 1 = string key + emitter.instruction("call __rt_mixed_from_value"); // persist and box the associative string key as Mixed + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null keys do not use a low payload word + emitter.instruction("xor esi, esi"); // null keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for invalid foreach-key requests + emitter.label("__elephc_eval_value_array_iter_key_done"); + emitter.instruction("add rsp, 32"); // release the iterator-key wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed foreach key to Rust + + label_c_global(emitter, "__elephc_eval_value_array_set"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve local slots for receiver, value, and key + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the boxed value being written + emitter.instruction("mov rdi, rsi"); // pass the boxed key to the eval key normalizer + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the normalized key high word + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the value so the array consumes a retained owner + emitter.instruction("call __rt_incref"); // retain the boxed value for Mixed array storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the boxed array receiver to the Mixed array setter + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the normalized key low word to the setter + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the normalized key high word to the setter + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // pass the retained boxed value to be consumed by the setter + emitter.instruction("call __rt_mixed_array_set"); // mutate the boxed Mixed array through the shared runtime helper + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the target boxed array receiver to Rust + emitter.instruction("add rsp, 32"); // release the array-set wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_len"); + emitter.instruction("test rdi, rdi"); // null handles have no iterable eval elements + emitter.instruction("jz __elephc_eval_value_array_len_zero"); // report empty length for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_len_load"); // indexed arrays expose their header element count + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_len_load"); // associative arrays expose their header entry count + emitter.label("__elephc_eval_value_array_len_zero"); + emitter.instruction("xor eax, eax"); // scalar values have zero foreach-visible elements in this subset + emitter.instruction("ret"); // return the empty length to Rust + emitter.label("__elephc_eval_value_array_len_load"); + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the array/hash payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // is the container payload null? + emitter.instruction("jz __elephc_eval_value_array_len_zero"); // null payloads are treated as empty containers + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the runtime container element count + emitter.instruction("ret"); // return the element count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_len"); + emitter.instruction("test rdi, rdi"); // null handles have no JSON-visible object properties + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // report zero properties for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_property_len_zero"); // non-objects expose no JSON-visible properties here + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // is the object payload null? + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // null object payloads have no visible properties + abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("cmp rax, r11"); // check whether the object is stdClass + emitter.instruction("jne __elephc_eval_value_object_property_len_zero"); // non-stdClass objects expose no bridge-visible properties + emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("test r10, r10"); // is the property hash null? + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // null property hashes are treated as empty objects + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the hash entry count + emitter.instruction("ret"); // return the public property count to Rust + emitter.label("__elephc_eval_value_object_property_len_zero"); + emitter.instruction("xor eax, eax"); // report zero JSON-visible object properties + emitter.instruction("ret"); // return the empty property count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_iter_key"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable property-iterator wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver, target position, hash pointer, and counter + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed object receiver while walking properties + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested zero-based property position + emitter.instruction("test rdi, rdi"); // null handles produce a null property key + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // branch to boxed null for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_null"); // non-objects have no JSON-visible property key + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // is the object payload null? + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // null object payloads produce a null key + abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("cmp rax, r11"); // check whether the object is stdClass + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_null"); // non-stdClass objects have no bridge-visible key + emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("test r10, r10"); // is the property hash null? + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // null property hashes produce a null key + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the insertion-order property counter at zero + emitter.instruction("xor esi, esi"); // cursor 0 starts at the property hash head entry + emitter.label("__elephc_eval_value_object_property_iter_key_loop"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the hash pointer before advancing the iterator + emitter.instruction("call __rt_hash_iter_next"); // fetch the next insertion-order property key + emitter.instruction("cmp rax, -1"); // did the iterator report the done sentinel? + emitter.instruction("je __elephc_eval_value_object_property_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // load the current insertion-order property position + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // load the requested property position + emitter.instruction("cmp r10, r11"); // is this the requested property entry? + emitter.instruction("je __elephc_eval_value_object_property_iter_key_box"); // box the current property key when the position matches + emitter.instruction("add r10, 1"); // advance the insertion-order property counter + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the updated property counter + emitter.instruction("mov rsi, rax"); // use the returned cursor for the next iterator call + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_object_property_iter_key_box"); + emitter.instruction("cmp rdx, -1"); // integer hash keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_string"); // string property keys need string-tag boxing + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key fallback + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the integer property key as Mixed + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_string"); + emitter.instruction("mov rsi, rdx"); // move the string key length into the boxing high word + emitter.instruction("mov eax, 1"); // runtime tag 1 = string property key + emitter.instruction("call __rt_mixed_from_value"); // persist and box the string property key as Mixed + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null keys do not use a low payload word + emitter.instruction("xor esi, esi"); // null keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for invalid property-key requests + emitter.label("__elephc_eval_value_object_property_iter_key_done"); + emitter.instruction("add rsp, 32"); // release the property-iterator wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed property key to Rust + + emitter.label("__elephc_eval_key_normalize"); + emitter.instruction("push rbp"); // preserve the caller frame pointer while classifying the eval key + emitter.instruction("mov rbp, rsp"); // establish a stable key-normalizer frame + emitter.instruction("sub rsp, 16"); // reserve an aligned slot for the original boxed key + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the original boxed key for fallback integer casts + emitter.instruction("mov rax, rdi"); // pass the boxed key to mixed_unbox's internal input register + emitter.instruction("call __rt_mixed_unbox"); // expose key tag plus payload words + emitter.instruction("cmp rax, 1"); // is the eval key a string? + emitter.instruction("je __elephc_eval_key_normalize_string"); // normalize PHP string array keys through hash rules + emitter.instruction("test rax, rax"); // is the eval key already an integer? + emitter.instruction("jz __elephc_eval_key_normalize_int"); // integer keys only need the sentinel high word + emitter.instruction("cmp rax, 8"); // is the eval key null? + emitter.instruction("je __elephc_eval_key_normalize_null"); // PHP treats null array keys as the empty string + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the original boxed key for PHP integer coercion + emitter.instruction("mov rax, rdi"); // satisfy mixed_cast_int's mixed_unbox input convention + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-string keys to the current integer-key fallback + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer array key + emitter.instruction("jmp __elephc_eval_key_normalize_done"); // return the fallback integer key tuple + emitter.label("__elephc_eval_key_normalize_string"); + emitter.instruction("mov rax, rdi"); // pass the string key pointer to hash normalization + emitter.instruction("call __rt_hash_normalize_key"); // normalize numeric strings while preserving true string keys + emitter.instruction("jmp __elephc_eval_key_normalize_done"); // return the normalized string/int key tuple + emitter.label("__elephc_eval_key_normalize_int"); + emitter.instruction("mov rax, rdi"); // publish the unboxed integer key low word + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer array key + emitter.instruction("jmp __elephc_eval_key_normalize_done"); // finish integer key normalization + emitter.label("__elephc_eval_key_normalize_null"); + emitter.instruction("xor eax, eax"); // null array keys use the empty-string pointer + emitter.instruction("xor edx, edx"); // null array keys use the empty-string length + emitter.label("__elephc_eval_key_normalize_done"); + emitter.instruction("add rsp, 16"); // release the key-normalizer spill slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return key_lo/key_hi in rax/rdx + + label_c_global(emitter, "__elephc_eval_value_is_array_like"); + emitter.instruction("test rdi, rdi"); // null handles cannot be indexed as arrays + emitter.instruction("jz __elephc_eval_value_is_array_like_false"); // report false for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // indexed arrays are valid eval array-write receivers + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // associative arrays are valid eval array-write receivers + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // ArrayAccess-capable objects are delegated to runtime set helpers + emitter.label("__elephc_eval_value_is_array_like_false"); + emitter.instruction("mov rax, 0"); // report false for scalar and null values + emitter.instruction("ret"); // return the boolean result to Rust + emitter.label("__elephc_eval_value_is_array_like_true"); + emitter.instruction("mov rax, 1"); // report true for array-like values + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_is_null"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to a concrete runtime tag + emitter.instruction("cmp rax, 8"); // runtime tag 8 means PHP null + emitter.instruction("sete al"); // set the low byte when the tag is null + emitter.instruction("movzx eax, al"); // widen the C boolean result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_type_tag"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells and return the concrete runtime tag + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the unboxed runtime tag to Rust + + label_c_global(emitter, "__elephc_eval_value_invoker_ref_cell"); + emitter.instruction(&format!("mov rax, {}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction(&format!("mov rsi, {}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed + emitter.instruction("jmp __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_invoker_raw_ref_cell"); + emitter.instruction(&format!("mov rax, {}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction("jmp __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_raw_word"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed scalar cell into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the boxed scalar payload words + emitter.instruction("mov rax, rdi"); // return the scalar low payload word to Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the raw payload word + + label_c_global(emitter, "__elephc_eval_value_raw_high_word"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed cell into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the boxed payload words + emitter.instruction("mov rax, rdx"); // return the high payload word to Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the raw high payload word + + label_c_global(emitter, "__elephc_eval_value_retain_raw_string"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer while persisting a raw string + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve space for the Rust out-len pointer + emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // save the Rust out-len pointer across str_persist + emitter.instruction("mov rax, rdi"); // move the raw string pointer into str_persist input + emitter.instruction("mov rdx, rsi"); // move the raw string length into str_persist input + emitter.instruction("call __rt_str_persist"); // duplicate the string for staged by-ref ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the Rust out-len pointer + emitter.instruction("mov QWORD PTR [r10], rdx"); // report the persisted string length to Rust + emitter.instruction("add rsp, 16"); // release the string retain wrapper spill slot + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the retained raw string pointer + + label_c_global(emitter, "__elephc_eval_value_from_raw_string"); + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("mov rdx, rsi"); // move the raw string length into the Mixed high word + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the raw string payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_string"); + emitter.instruction("mov rax, rdi"); // move the raw string pointer into the runtime release input register + emitter.instruction("jmp __rt_heap_free_safe"); // release the staged raw string owner + + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer while retaining a raw heap word + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve space for the raw heap word return value + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the raw heap word across incref + emitter.instruction("mov rax, rdi"); // move the raw heap word into the runtime incref input register + emitter.instruction("call __rt_incref"); // retain the heap payload for the staged by-ref slot + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the original raw heap word to Rust + emitter.instruction("add rsp, 16"); // release the retain wrapper spill slot + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the retained raw heap word + + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); + emitter.instruction("mov rax, rdi"); // move the runtime tag into the mixed boxing tag register + emitter.instruction("mov rdi, rsi"); // move the raw scalar word into the low payload register + emitter.instruction("xor esi, esi"); // raw one-word scalar payloads have no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_from_raw_heap_word"); + emitter.instruction("test rdi, rdi"); // reject null raw heap words before reading their header + emitter.instruction("jz __elephc_eval_value_from_raw_heap_word_miss_x86"); // null raw heap words cannot be boxed + emitter.instruction("mov r10, QWORD PTR [rdi - 8]"); // load the uniform x86_64 heap kind word for tag recovery + emitter.instruction("and r10, 0xff"); // isolate the low-byte heap kind tag + emitter.instruction("cmp r10, 2"); // heap kind 2 stores indexed-array payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_array_x86"); // box indexed arrays with runtime tag 4 + emitter.instruction("cmp r10, 3"); // heap kind 3 stores associative hash payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_hash_x86"); // box hashes with runtime tag 5 + emitter.instruction("cmp r10, 4"); // heap kind 4 stores object payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_object_x86"); // box objects with runtime tag 6 + emitter.instruction("cmp r10, 5"); // heap kind 5 stores boxed Mixed payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_mixed_x86"); // box nested Mixed cells with runtime tag 7 + emitter.label("__elephc_eval_value_from_raw_heap_word_miss_x86"); + emitter.instruction("xor eax, eax"); // report malformed raw heap words as a null Rust handle + emitter.instruction("ret"); // return the failed boxing sentinel + emitter.label("__elephc_eval_value_from_raw_heap_word_array_x86"); + emitter.instruction("mov rax, 4"); // runtime tag 4 = indexed array + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_hash_x86"); + emitter.instruction("mov rax, 5"); // runtime tag 5 = associative array + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_object_x86"); + emitter.instruction("mov rax, 6"); // runtime tag 6 = object + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_mixed_x86"); + emitter.instruction("mov rax, 7"); // runtime tag 7 = Mixed + emitter.label("__elephc_eval_value_from_raw_heap_word_box_x86"); + emitter.instruction("xor esi, esi"); // one-word heap payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // retain and box the raw heap payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_heap_word"); + emitter.instruction("mov rax, rdi"); // move the raw heap word into the runtime release input register + emitter.instruction("jmp __rt_decref_any"); // release the staged raw heap slot owner + + label_c_global(emitter, "__elephc_eval_value_object_identity"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable object-identity wrapper frame + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means PHP object + emitter.instruction("je __elephc_eval_value_object_identity_object_x86"); // return the payload pointer for object values + emitter.instruction("xor eax, eax"); // return zero for non-object values + emitter.instruction("jmp __elephc_eval_value_object_identity_done_x86"); // skip the object-payload result + emitter.label("__elephc_eval_value_object_identity_object_x86"); + emitter.instruction("mov rax, rdi"); // return the unboxed object payload pointer + emitter.label("__elephc_eval_value_object_identity_done_x86"); + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the object identity pointer to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_int"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the boxed eval value to a PHP integer payload + emitter.instruction("mov rdi, rax"); // move the integer cast result into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the cast integer result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed integer cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_float"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval value to a PHP double payload + emitter.instruction("movq rdi, xmm0"); // move the double cast bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the cast double result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed double cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_string"); + emitter.instruction("push rbp"); // align the stack while unboxing and boxing the string result + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete payload tag and value words + emitter.instruction("cmp rax, 0"); // is the eval value an integer? + emitter.instruction("je __elephc_eval_value_cast_string_int_x86"); // integers cast through decimal formatting + emitter.instruction("cmp rax, 1"); // is the eval value already a string? + emitter.instruction("je __elephc_eval_value_cast_string_box_x86"); // strings can be boxed through the normal ownership path + emitter.instruction("cmp rax, 2"); // is the eval value a double? + emitter.instruction("je __elephc_eval_value_cast_string_float_x86"); // doubles cast through decimal formatting + emitter.instruction("cmp rax, 3"); // is the eval value a boolean? + emitter.instruction("je __elephc_eval_value_cast_string_bool_x86"); // booleans cast to \"1\" or the empty string + emitter.label("__elephc_eval_value_cast_string_empty_x86"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("xor edi, edi"); // unsupported and falsey payloads use an empty string pointer + emitter.instruction("xor esi, esi"); // unsupported and falsey payloads use an empty string length + emitter.instruction("call __rt_mixed_from_value"); // box the empty string result for Rust + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_int_x86"); + emitter.instruction("mov rax, rdi"); // pass the integer payload to decimal formatting + emitter.instruction("call __rt_itoa"); // format the integer cast result as a string pair + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the formatted integer string + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_box_x86"); + emitter.instruction("mov rsi, rdx"); // move the existing string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the existing string payload once + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_float_x86"); + emitter.instruction("movq xmm0, rdi"); // move the double payload bits into the FP argument register + emitter.instruction("call __rt_ftoa"); // format the double cast result as a string pair + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the formatted double string + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_bool_x86"); + emitter.instruction("test rdi, rdi"); // false casts to the empty string + emitter.instruction("je __elephc_eval_value_cast_string_empty_x86"); // route false to the empty string boxer + emitter.instruction("mov rax, rdi"); // pass the true payload to decimal formatting + emitter.instruction("call __rt_itoa"); // format true as the string \"1\" + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the true string result + emitter.label("__elephc_eval_value_cast_string_done_x86"); + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed string cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_bool"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the boxed eval value to PHP truthiness + emitter.instruction("mov rdi, rax"); // move the boolean cast result into mixed value_lo + emitter.instruction("xor esi, esi"); // boolean payloads do not use a high word + emitter.instruction("mov eax, 3"); // runtime tag 3 = boolean + emitter.instruction("call __rt_mixed_from_value"); // box the cast boolean result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed boolean cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_int"); + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the C integer payload in rdi and return + + label_c_global(emitter, "__elephc_eval_value_resource"); + emitter.instruction("mov eax, 9"); // runtime tag 9 = resource, with C id already in rdi + emitter.instruction("xor esi, esi"); // resource payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the resource payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_float"); + emitter.instruction("movq rdi, xmm0"); // move the C double bits into mixed value_lo + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the double payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string, with C ptr/len already in rdi/rsi + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the string payload for eval + + label_c_global(emitter, "__elephc_eval_value_abs"); + emitter.instruction("mov rax, rdi"); // move the boxed eval value into abs_mixed input + emitter.instruction("jmp __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + + label_c_global(emitter, "__elephc_eval_value_ceil"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for ceil + emitter.bl_c("ceil"); + emitter.instruction("movq rdi, xmm0"); // move the ceil result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the ceil result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed ceil result to Rust + + label_c_global(emitter, "__elephc_eval_value_floor"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for floor + emitter.bl_c("floor"); + emitter.instruction("movq rdi, xmm0"); // move the floor result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the floor result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed floor result to Rust + + label_c_global(emitter, "__elephc_eval_value_sqrt"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for sqrt + emitter.bl_c("sqrt"); + emitter.instruction("movq rdi, xmm0"); // move the sqrt result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the sqrt result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed sqrt result to Rust + + label_c_global(emitter, "__elephc_eval_value_strrev"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_string input + emitter.instruction("call __rt_mixed_cast_string"); // cast the boxed eval argument to a PHP string pair + emitter.instruction("call __rt_strrev"); // reverse the PHP byte string into concat storage + emitter.instruction("mov rdi, rax"); // move the reversed string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the reversed string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the reversed string for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reversed string to Rust + + label_c_global(emitter, "__elephc_eval_value_fdiv"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // keep the right divisor in xmm1 + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the left dividend into xmm0 + emitter.instruction("divsd xmm0, xmm1"); // compute fdiv() with IEEE zero handling + emitter.instruction("ucomisd xmm0, xmm0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("jp __elephc_eval_value_fdiv_nan_x86"); // normalize unordered fdiv results before boxing + emitter.instruction("movq rdi, xmm0"); // move the fdiv result bits into mixed value_lo + emitter.instruction("jmp __elephc_eval_value_fdiv_box_x86"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fdiv_nan_x86"); + emitter.instruction("movabs rdi, 0x7ff8000000000000"); // use a positive quiet NaN payload for PHP output + emitter.label("__elephc_eval_value_fdiv_box_x86"); + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the fdiv result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the fdiv wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed fdiv result to Rust + + label_c_global(emitter, "__elephc_eval_value_fmod"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // move the right divisor into the second fmod argument + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // move the left dividend into the first fmod argument + emitter.bl_c("fmod"); + emitter.instruction("ucomisd xmm0, xmm0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("jp __elephc_eval_value_fmod_nan_x86"); // normalize unordered fmod results before boxing + emitter.instruction("movq rdi, xmm0"); // move the fmod result bits into mixed value_lo + emitter.instruction("jmp __elephc_eval_value_fmod_box_x86"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fmod_nan_x86"); + emitter.instruction("movabs rdi, 0x7ff8000000000000"); // use a positive quiet NaN payload for PHP output + emitter.label("__elephc_eval_value_fmod_box_x86"); + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the fmod result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the fmod wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed fmod result to Rust + + label_c_global(emitter, "__elephc_eval_value_add"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_sub"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_sub"); // subtract two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_mul"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_div"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("pxor xmm1, xmm1"); // materialize a zero double for divisor checking + emitter.instruction("ucomisd xmm0, xmm1"); // detect division by zero before the hardware divide + emitter.instruction("je __elephc_eval_value_div_null_x86"); // return null until eval has throwable propagation + emitter.instruction("movapd xmm1, xmm0"); // keep the right divisor in xmm1 + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the left dividend into xmm0 + emitter.instruction("divsd xmm0, xmm1"); // compute PHP division as a double result + emitter.instruction("movq rdi, xmm0"); // move the double bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the division result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_div_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_div_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for division by zero + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported division-by-zero propagation + emitter.label("__elephc_eval_value_div_done_x86"); + emitter.instruction("add rsp, 32"); // release the division wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed division result to Rust + + label_c_global(emitter, "__elephc_eval_value_mod"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left integer + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the left integer across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("test rax, rax"); // detect modulo by zero before the hardware divide + emitter.instruction("jz __elephc_eval_value_mod_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rdi, rax"); // keep the integer divisor in rdi + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the integer dividend into rax + emitter.instruction("cqo"); // sign-extend the dividend for signed division + emitter.instruction("idiv rdi"); // compute quotient in rax and remainder in rdx + emitter.instruction("mov rdi, rdx"); // move the integer remainder into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the modulo result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_mod_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_mod_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for modulo by zero + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported modulo-by-zero propagation + emitter.label("__elephc_eval_value_mod_done_x86"); + emitter.instruction("add rsp, 32"); // release the modulo wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed modulo result to Rust + + label_c_global(emitter, "__elephc_eval_value_pow"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the exponentiation base across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // move the exponent into libc pow's second argument + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the base into libc pow's first argument + emitter.bl_c("pow"); + emitter.instruction("movq rdi, xmm0"); // move the pow result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the exponentiation result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the exponentiation wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed exponentiation result to Rust + + label_c_global(emitter, "__elephc_eval_value_round"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for precision state and saved doubles + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the optional precision cell while casting the value + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save whether the caller supplied a precision argument + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // check whether a precision argument was supplied + emitter.instruction("jne __elephc_eval_value_round_precision_x86"); // use the precision path when a second argument is present + emitter.bl_c("round"); + emitter.instruction("jmp __elephc_eval_value_round_box_x86"); // box the default-precision round result + emitter.label("__elephc_eval_value_round_precision_x86"); + emitter.instruction("movsd QWORD PTR [rbp - 24], xmm0"); // save the original value while casting the precision + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the precision cell for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the optional precision to a PHP integer + emitter.instruction("cvtsi2sd xmm1, rax"); // convert the precision to a floating exponent for pow + emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 payload for 10.0 + emitter.instruction("movq xmm0, rax"); // move 10.0 into the pow base argument + emitter.bl_c("pow"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 24]"); // reload the original value after pow returns the multiplier + emitter.instruction("mulsd xmm1, xmm0"); // scale the value by the precision multiplier + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the multiplier for rescaling after round + emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round argument + emitter.bl_c("round"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the precision multiplier for rescaling + emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to requested precision + emitter.label("__elephc_eval_value_round_box_x86"); + emitter.instruction("movq rdi, xmm0"); // move the round result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the round result into a Mixed cell + emitter.instruction("add rsp, 48"); // release the round wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed round result to Rust + + label_c_global(emitter, "__elephc_eval_value_bit_not"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // keep stack alignment for the cast and boxing calls + emitter.instruction("mov rax, rdi"); // move the boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the boxed operand to a PHP integer + emitter.instruction("not rax"); // compute bitwise complement of the integer payload + emitter.instruction("mov rdi, rax"); // move the complement into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the bitwise NOT result into a Mixed cell + emitter.instruction("add rsp, 16"); // release the bitwise NOT wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bitwise NOT result to Rust + + label_c_global(emitter, "__elephc_eval_value_bitwise"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for right operand, opcode, and left integer + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the eval bitwise opcode across helper calls + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the left integer across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the left integer into the payload register + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the eval bitwise opcode for dispatch + emitter.instruction("cmp r10, 0"); // is this integer bitwise AND? + emitter.instruction("je __elephc_eval_value_bitwise_and_x86"); // route opcode 0 to integer AND + emitter.instruction("cmp r10, 1"); // is this integer bitwise OR? + emitter.instruction("je __elephc_eval_value_bitwise_or_x86"); // route opcode 1 to integer OR + emitter.instruction("cmp r10, 2"); // is this integer bitwise XOR? + emitter.instruction("je __elephc_eval_value_bitwise_xor_x86"); // route opcode 2 to integer XOR + emitter.instruction("cmp r10, 3"); // is this integer left shift? + emitter.instruction("je __elephc_eval_value_bitwise_shl_x86"); // route opcode 3 to integer left shift + emitter.instruction("cmp r10, 4"); // is this integer right shift? + emitter.instruction("je __elephc_eval_value_bitwise_shr_x86"); // route opcode 4 to integer right shift + emitter.instruction("jmp __elephc_eval_value_bitwise_null_x86"); // fail closed for unknown bitwise opcodes + emitter.label("__elephc_eval_value_bitwise_and_x86"); + emitter.instruction("and rdi, rax"); // compute integer bitwise AND + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_or_x86"); + emitter.instruction("or rdi, rax"); // compute integer bitwise OR + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_xor_x86"); + emitter.instruction("xor rdi, rax"); // compute integer bitwise XOR + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_shl_x86"); + emitter.instruction("test rax, rax"); // negative shift counts are runtime errors in PHP + emitter.instruction("js __elephc_eval_value_bitwise_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rcx, rax"); // move the shift count into the x86 shift-count register + emitter.instruction("shl rdi, cl"); // shift the integer payload left + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_shr_x86"); + emitter.instruction("test rax, rax"); // negative shift counts are runtime errors in PHP + emitter.instruction("js __elephc_eval_value_bitwise_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rcx, rax"); // move the shift count into the x86 shift-count register + emitter.instruction("sar rdi, cl"); // shift the integer payload right arithmetically + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_box_x86"); + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the bitwise result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_bitwise_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_bitwise_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for unsupported bitwise errors + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported bitwise error propagation + emitter.label("__elephc_eval_value_bitwise_done_x86"); + emitter.instruction("add rsp, 32"); // release the bitwise wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bitwise result to Rust + + label_c_global(emitter, "__elephc_eval_value_concat"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for right operand and left string pair + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_string input + emitter.instruction("call __rt_mixed_cast_string"); // cast the left boxed operand to a PHP string pair + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the left string pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the left string length + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for string casting + emitter.instruction("call __rt_mixed_cast_string"); // cast the right boxed operand to a PHP string pair + emitter.instruction("mov rdi, rax"); // move the right string pointer into concat's right pointer register + emitter.instruction("mov rsi, rdx"); // move the right string length into concat's right length register + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the left string pointer for concat + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the left string length for concat + emitter.instruction("call __rt_concat"); // concatenate the two PHP string pairs + emitter.instruction("mov rdi, rax"); // move the concat string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the concat string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string for boxing the concat result + emitter.instruction("call __rt_mixed_from_value"); // persist and box the concatenated string + emitter.instruction("add rsp, 32"); // release the concat wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed concat result to Rust + + label_c_global(emitter, "__elephc_eval_value_compare"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across comparison helpers + emitter.instruction("mov rbp, rsp"); // establish a stable comparison wrapper frame + emitter.instruction("sub rsp, 32"); // reserve slots for operands, opcode, and numeric casts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed operand + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed operand + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the eval comparison opcode + emitter.instruction("cmp rdx, 0"); // is this loose equality? + emitter.instruction("je __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper + emitter.instruction("cmp rdx, 1"); // is this loose inequality? + emitter.instruction("je __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("cmp rdx, 6"); // is this strict equality? + emitter.instruction("je __elephc_eval_value_compare_strict_eq"); // route === through the mixed strict-equality helper + emitter.instruction("cmp rdx, 7"); // is this strict inequality? + emitter.instruction("je __elephc_eval_value_compare_strict_ne"); // route !== through the mixed strict-equality helper + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the normalized left numeric operand + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a numeric comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the left numeric operand for the float comparison + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the eval comparison opcode for dispatch + emitter.instruction("cmp r10, 2"); // is this a less-than comparison? + emitter.instruction("je __elephc_eval_value_compare_lt"); // materialize left < right from float comparison flags + emitter.instruction("cmp r10, 3"); // is this a less-than-or-equal comparison? + emitter.instruction("je __elephc_eval_value_compare_lte"); // materialize left <= right from float comparison flags + emitter.instruction("cmp r10, 4"); // is this a greater-than comparison? + emitter.instruction("je __elephc_eval_value_compare_gt"); // materialize left > right from float comparison flags + emitter.instruction("cmp r10, 5"); // is this a greater-than-or-equal comparison? + emitter.instruction("je __elephc_eval_value_compare_gte"); // materialize left >= right from float comparison flags + emitter.instruction("xor eax, eax"); // unknown comparison opcodes fail closed as false + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the fallback false result + emitter.label("__elephc_eval_value_compare_eq"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for loose equality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for loose equality + emitter.instruction("call __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the equality result + emitter.label("__elephc_eval_value_compare_ne"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for loose inequality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for loose inequality + emitter.instruction("call __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion + emitter.instruction("xor rax, 1"); // invert equality for the != operator + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_strict_eq"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for strict equality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for strict equality + emitter.instruction("call __rt_mixed_strict_eq"); // compute PHP strict equality + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the strict-equality result + emitter.label("__elephc_eval_value_compare_strict_ne"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for strict inequality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for strict inequality + emitter.instruction("call __rt_mixed_strict_eq"); // compute PHP strict equality before inversion + emitter.instruction("xor rax, 1"); // invert equality for the !== operator + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the strict-inequality result + emitter.label("__elephc_eval_value_compare_lt"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for < + emitter.instruction("setb al"); // set true when left is below right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN less-than results + emitter.instruction("movzx rax, al"); // widen the less-than boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the less-than result + emitter.label("__elephc_eval_value_compare_lte"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for <= + emitter.instruction("setbe al"); // set true when left is below or equal to right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN less-than-or-equal results + emitter.instruction("movzx rax, al"); // widen the less-than-or-equal boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the less-than-or-equal result + emitter.label("__elephc_eval_value_compare_gt"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for > + emitter.instruction("seta al"); // set true when left is above right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN greater-than results + emitter.instruction("movzx rax, al"); // widen the greater-than boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the greater-than result + emitter.label("__elephc_eval_value_compare_gte"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for >= + emitter.instruction("setae al"); // set true when left is above or equal to right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN greater-than-or-equal results + emitter.instruction("movzx rax, al"); // widen the greater-than-or-equal boolean result + emitter.label("__elephc_eval_value_compare_box"); + emitter.instruction("mov rdi, rax"); // move the comparison boolean into the Mixed payload register + emitter.instruction("mov eax, 3"); // runtime tag 3 = bool + emitter.instruction("xor esi, esi"); // bool payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the comparison result as a Mixed bool + emitter.instruction("add rsp, 32"); // release the comparison wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed comparison result to Rust + + emitter.label("__elephc_eval_mixed_loose_eq"); + emitter.instruction("push rbp"); // preserve the caller frame pointer across mixed helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable loose-equality helper frame + emitter.instruction("sub rsp, 96"); // allocate helper slots for unboxed tags, payloads, and casts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed operand for later casts + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed operand for later casts + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // unbox the left eval operand into tag and payload words + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the left runtime tag + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the left low payload word + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the left high payload word + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand for unboxing + emitter.instruction("call __rt_mixed_unbox"); // unbox the right eval operand into tag and payload words + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the right runtime tag + emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the right low payload word + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the right high payload word + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the left runtime tag for equality dispatch + emitter.instruction("cmp r10, 3"); // does the left operand have PHP bool semantics? + emitter.instruction("je __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp rax, 3"); // does the right operand have PHP bool semantics? + emitter.instruction("je __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp r10, rax"); // do the operands have the same runtime tag? + emitter.instruction("je __elephc_eval_mixed_loose_eq_same_tag"); // same-tag scalars use focused payload comparisons + emitter.instruction("cmp r10, 8"); // is the left operand null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp rax, 8"); // is the right operand null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp r10, 1"); // is a non-matching left operand a string? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string"); // compare numeric strings against numeric scalars + emitter.instruction("cmp rax, 1"); // is a non-matching right operand a string? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string"); // compare numeric strings against numeric scalars + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_numeric"); // remaining scalar mismatches compare numerically + emitter.label("__elephc_eval_mixed_loose_eq_same_tag"); + emitter.instruction("cmp r10, 8"); // are both operands null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_true"); // null loosely equals null + emitter.instruction("cmp r10, 1"); // are both operands strings? + emitter.instruction("je __elephc_eval_mixed_loose_eq_strings"); // strings use PHP loose string equality + emitter.instruction("cmp r10, 2"); // are both operands floats? + emitter.instruction("je __elephc_eval_mixed_loose_eq_floats"); // floats compare with native floating equality + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the left low payload word + emitter.instruction("cmp r11, QWORD PTR [rbp - 56]"); // compare low payload words for int and pointer-like scalars + emitter.instruction("jne __elephc_eval_mixed_loose_eq_false"); // mismatched low payloads are not equal + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the left high payload word + emitter.instruction("cmp r11, QWORD PTR [rbp - 64]"); // compare high payload words for pointer-like scalars + emitter.instruction("sete al"); // materialize same-tag payload equality + emitter.instruction("movzx rax, al"); // widen the payload equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the payload equality result + emitter.label("__elephc_eval_mixed_loose_eq_strings"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the left string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reload the left string length + emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // reload the right string pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the right string length + emitter.instruction("call __rt_str_loose_eq"); // compare strings with PHP loose numeric-string rules + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_floats"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the left float payload + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 56]"); // reload the right float payload + emitter.instruction("ucomisd xmm1, xmm0"); // compare same-tag float payloads + emitter.instruction("sete al"); // set true for ordered float equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the float equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the float equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_null"); + emitter.instruction("cmp rax, 1"); // is null being compared with a string? + emitter.instruction("jne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // null loosely equals only the empty string + emitter.instruction("sete al"); // materialize the null/string equality result + emitter.instruction("movzx rax, al"); // widen the null/string equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the null/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_null"); + emitter.instruction("cmp r10, 1"); // is null being compared with a string? + emitter.instruction("jne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // null loosely equals only the empty string + emitter.instruction("sete al"); // materialize the string/null equality result + emitter.instruction("movzx rax, al"); // widen the string/null equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string/null equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_string"); + emitter.instruction("cmp rax, 0"); // can the right operand be compared numerically as an int? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("cmp rax, 2"); // can the right operand be compared numerically as a float? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_left_string_numeric"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the left string pointer for numeric parsing + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // reload the left string length for numeric parsing + emitter.instruction("call __rt_str_to_number"); // parse the left string under PHP numeric-string rules + emitter.instruction("test rax, rax"); // did the left string parse as numeric? + emitter.instruction("je __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the parsed left numeric-string value + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the parsed left numeric-string value + emitter.instruction("ucomisd xmm1, xmm0"); // compare parsed string and numeric scalar values + emitter.instruction("sete al"); // set true for ordered string/numeric equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the string/numeric equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string/numeric equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_string"); + emitter.instruction("cmp r10, 0"); // can the left operand be compared numerically as an int? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("cmp r10, 2"); // can the left operand be compared numerically as a float? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_right_string_numeric"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the right string pointer for numeric parsing + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // reload the right string length for numeric parsing + emitter.instruction("call __rt_str_to_number"); // parse the right string under PHP numeric-string rules + emitter.instruction("test rax, rax"); // did the right string parse as numeric? + emitter.instruction("je __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the parsed right numeric-string value + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the parsed right numeric-string value + emitter.instruction("ucomisd xmm0, xmm1"); // compare numeric scalar and parsed string values + emitter.instruction("sete al"); // set true for ordered numeric/string equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the numeric/string equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the numeric/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_bool"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for truthiness + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the left operand to PHP truthiness + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the left truthiness result + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for truthiness + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the right operand to PHP truthiness + emitter.instruction("cmp QWORD PTR [rbp - 72], rax"); // compare boolean truthiness for loose equality + emitter.instruction("sete al"); // materialize bool loose equality + emitter.instruction("movzx rax, al"); // widen the bool equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the bool loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_numeric"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for numeric equality + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the left numeric equality operand + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric equality + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the left numeric equality operand + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric operands for loose equality + emitter.instruction("sete al"); // set true for ordered numeric equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the numeric equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the numeric loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_true"); + emitter.instruction("mov rax, 1"); // materialize true for loose equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the true result + emitter.label("__elephc_eval_mixed_loose_eq_false"); + emitter.instruction("xor eax, eax"); // materialize false for loose equality + emitter.label("__elephc_eval_mixed_loose_eq_done"); + emitter.instruction("add rsp, 96"); // release the loose-equality helper slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the loose-equality boolean in rax + + label_c_global(emitter, "__elephc_eval_value_spaceship"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left numeric spaceship operand + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 16]"); // reload the left numeric spaceship operand + emitter.instruction("ucomisd xmm1, xmm0"); // compare left and right numeric operands for spaceship + emitter.instruction("jp __elephc_eval_value_spaceship_gt_x86"); // PHP treats unordered NaN spaceship comparisons as greater + emitter.instruction("ja __elephc_eval_value_spaceship_gt_x86"); // route left > right to result 1 + emitter.instruction("jb __elephc_eval_value_spaceship_lt_x86"); // route left < right to result -1 + emitter.instruction("xor edi, edi"); // equal operands produce spaceship result 0 + emitter.instruction("jmp __elephc_eval_value_spaceship_box_x86"); // box the equal spaceship result + emitter.label("__elephc_eval_value_spaceship_gt_x86"); + emitter.instruction("mov rdi, 1"); // greater or unordered comparisons produce result 1 + emitter.instruction("jmp __elephc_eval_value_spaceship_box_x86"); // box the greater spaceship result + emitter.label("__elephc_eval_value_spaceship_lt_x86"); + emitter.instruction("mov rdi, -1"); // lesser comparisons produce result -1 + emitter.label("__elephc_eval_value_spaceship_box_x86"); + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the spaceship result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the spaceship wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed spaceship result to Rust + + label_c_global(emitter, "__elephc_eval_value_echo"); + emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed echo input + emitter.instruction("jmp __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string_bytes"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across string casting + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve slots for the caller's output pointers + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the caller's out_ptr storage address + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the caller's out_len storage address + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_string input + emitter.instruction("call __rt_mixed_cast_string"); // cast the boxed eval value to a PHP string pair + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the optional out_ptr storage address + emitter.instruction("test r10, r10"); // did the caller request the string pointer? + emitter.instruction("jz __elephc_eval_value_string_bytes_len"); // skip pointer storage when the caller passed null + emitter.instruction("mov QWORD PTR [r10], rax"); // store the string pointer for Rust to copy immediately + emitter.label("__elephc_eval_value_string_bytes_len"); + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the optional out_len storage address + emitter.instruction("test r10, r10"); // did the caller request the string length? + emitter.instruction("jz __elephc_eval_value_string_bytes_done"); // skip length storage when the caller passed null + emitter.instruction("mov QWORD PTR [r10], rdx"); // store the string byte length for Rust + emitter.label("__elephc_eval_value_string_bytes_done"); + emitter.instruction("mov rax, 1"); // report successful string conversion to Rust + emitter.instruction("add rsp, 16"); // release the string-bytes wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the success flag to Rust + + label_c_global(emitter, "__elephc_eval_value_truthy"); + emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed truthiness input + emitter.instruction("jmp __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + + label_c_global(emitter, "__elephc_eval_value_retain"); + emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal retain register + emitter.instruction("jmp __rt_incref"); // retain one eval-owned boxed Mixed cell + + label_c_global(emitter, "__elephc_eval_value_final_object_identity"); + emitter.instruction("mov rax, rdi"); // inspect the C boxed Mixed argument without changing refcounts + emitter.instruction("test rax, rax"); // null handles cannot release an object + emitter.instruction("jz __elephc_eval_value_final_object_none_x86"); // report no final object for null handles + emitter.label("__elephc_eval_value_final_object_loop_x86"); + emitter.instruction("mov r10d, DWORD PTR [rax - 12]"); // load the current Mixed wrapper refcount + emitter.instruction("cmp r10d, 1"); // only the last Mixed owner can free the wrapped payload + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // non-final wrapper releases cannot run destructors yet + emitter.instruction("mov r10, QWORD PTR [rax]"); // load the current Mixed runtime tag + emitter.instruction("cmp r10, 7"); // tag 7 means the payload is another boxed Mixed + emitter.instruction("jne __elephc_eval_value_final_object_check_object_x86"); // concrete payloads can be tested for object ownership + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // follow the nested Mixed payload pointer + emitter.instruction("test rax, rax"); // null nested payloads cannot release an object + emitter.instruction("jnz __elephc_eval_value_final_object_loop_x86"); // continue while nested Mixed payloads are present + emitter.instruction("jmp __elephc_eval_value_final_object_none_x86"); // return zero for null nested payloads + emitter.label("__elephc_eval_value_final_object_check_object_x86"); + emitter.instruction("cmp r10, 6"); // tag 6 means the concrete payload is a PHP object + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // non-object releases have no dynamic destructor + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // load the object payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // null object payloads are treated as non-final + emitter.instruction("jz __elephc_eval_value_final_object_none_x86"); // return zero for null object payloads + emitter.instruction("mov r11d, DWORD PTR [r10 - 12]"); // load the object refcount that the Mixed release will decrement + emitter.instruction("cmp r11d, 1"); // only the final object owner can run the destructor + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // defer destructor execution until object refcount is final + emitter.instruction("mov rax, r10"); // return the object identity pointer to Rust + emitter.instruction("ret"); // finish the final-object query + emitter.label("__elephc_eval_value_final_object_none_x86"); + emitter.instruction("xor eax, eax"); // report that no dynamic destructor should run + emitter.instruction("ret"); // return zero to Rust + + label_c_global(emitter, "__elephc_eval_warning"); + emitter.instruction("jmp __rt_diag_warning"); // emit or suppress one eval runtime warning + + label_c_global(emitter, "__elephc_eval_value_release"); + emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal release register + emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell +} + +/// Emits the ARM64 wrapper that boxes a borrowed raw object pointer for Rust eval. +fn emit_aarch64_object_from_raw_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_from_raw"); + emitter.instruction("cbz x0, __elephc_eval_value_object_from_raw_null"); // null raw object pointers become PHP null cells + emitter.instruction("mov x1, x0"); // move the raw object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box and retain the borrowed object for eval + emitter.label("__elephc_eval_value_object_from_raw_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the null payload and return to Rust +} + +/// Emits the ARM64 wrapper that installs the dynamic object destructor callback. +fn emit_aarch64_install_dynamic_object_destructor_hook(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_install_dynamic_object_destructor_hook"); + abi::emit_symbol_address(emitter, "x9", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("str x0, [x9]"); // store the Rust callback pointer for object destruction + emitter.instruction("ret"); // return after installing the optional eval hook +} + +/// Emits the x86_64 wrapper that boxes a borrowed raw object pointer for Rust eval. +fn emit_x86_64_object_from_raw_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_from_raw"); + emitter.instruction("test rdi, rdi"); // null raw object pointers become PHP null cells + emitter.instruction("jz __elephc_eval_value_object_from_raw_null_x86"); // branch to null boxing for missing object payloads + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box and retain the borrowed object for eval + emitter.label("__elephc_eval_value_object_from_raw_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the null payload and return to Rust +} + +/// Emits the x86_64 wrapper that installs the dynamic object destructor callback. +fn emit_x86_64_install_dynamic_object_destructor_hook(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_install_dynamic_object_destructor_hook"); + abi::emit_symbol_address(emitter, "r10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("mov QWORD PTR [r10], rdi"); // store the Rust callback pointer for object destruction + emitter.instruction("ret"); // return after installing the optional eval hook +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionMethod names. +fn emit_aarch64_eval_reflection_method_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_method_names", + "_eval_reflection_method_count", + "_eval_reflection_methods", + "__elephc_eval_reflection_method_names", + "method", + 56, + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionProperty names. +fn emit_aarch64_eval_reflection_property_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_property_names", + "_eval_reflection_property_count", + "_eval_reflection_properties", + "__elephc_eval_reflection_property_names", + "property", + 56, + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionClass interface names. +fn emit_aarch64_eval_reflection_class_interface_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_interface_names", + "_eval_reflection_class_interface_count", + "_eval_reflection_class_interfaces", + "__elephc_eval_reflection_class_interface_names", + "class interface", + 32, + ); +} + +/// Emits the ARM64 eval hook that returns AOT `class_uses()` trait names. +fn emit_aarch64_eval_reflection_class_trait_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_names", + "_eval_reflection_class_trait_count", + "_eval_reflection_class_traits", + "__elephc_eval_reflection_class_trait_names", + "class trait", + 32, + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionClass trait alias names. +fn emit_aarch64_eval_reflection_class_trait_alias_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_names", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_aliases", + "__elephc_eval_reflection_class_trait_alias_names", + "class trait alias", + 32, + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionClass trait alias sources. +fn emit_aarch64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_sources", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_alias_sources", + "__elephc_eval_reflection_class_trait_alias_sources", + "class trait alias source", + 32, + ); +} + +/// Emits the ARM64 eval hook that returns the AOT reflection source file. +fn emit_aarch64_eval_reflection_source_file(emitter: &mut Emitter) { + let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + label_c_global(emitter, "__elephc_eval_reflection_source_file"); + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_source_file_len"); + emitter.instruction("ldr x1, [x9]"); // load the generated source-file length + emitter.instruction("cbz x1, __elephc_eval_reflection_source_file_miss"); // report no source file when EIR metadata is absent + abi::emit_symbol_address(emitter, "x0", "_eval_reflection_source_file"); + emitter.instruction(&format!("b {string_symbol}")); // box the generated source-file path for Rust + emitter.label("__elephc_eval_reflection_source_file_miss"); + emitter.instruction("mov x0, xzr"); // return null when no source file is available + emitter.instruction("ret"); // finish the source-file metadata lookup +} + +/// Emits an ARM64 class-filtered AOT reflection member-name scanner. +fn emit_aarch64_eval_reflection_member_names( + emitter: &mut Emitter, + symbol: &str, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + member_kind: &str, + row_stride: u64, +) { + let loop_label = format!("{label_prefix}_loop"); + let skip_label = format!("{label_prefix}_skip"); + let miss_label = format!("{label_prefix}_miss"); + let done_label = format!("{label_prefix}_done"); + let string_array_new_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + let string_array_push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + label_c_global(emitter, symbol); + emitter.instruction("sub sp, sp, #112"); // reserve scan state across allocation and string comparisons + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #96"); // establish a stable member-name scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", count_symbol); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection member row count + emitter.instruction("str x9, [sp, #16]"); // save the table count across helper calls + emitter.instruction("mov x0, x9"); // use the full table count as a safe result-array capacity + emitter.instruction(&format!("bl {string_array_new_symbol}")); // allocate the boxed result string array + emitter.instruction(&format!("cbz x0, {miss_label}")); // allocation failure reports a null pointer to Rust + emitter.instruction("str x0, [sp, #24]"); // save the boxed result string array + abi::emit_symbol_address(emitter, "x10", table_symbol); + emitter.instruction("str x10, [sp, #32]"); // save the current member metadata row + emitter.instruction("mov x11, #0"); // start scanning at member metadata row zero + emitter.label(&loop_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the member metadata row count + emitter.instruction("cmp x11, x9"); // have all member metadata rows been scanned? + emitter.instruction(&format!("b.ge {done_label}")); // return the accumulated names after the final row + emitter.instruction("ldr x10, [sp, #32]"); // reload the current member metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction(&format!("b.ne {skip_label}")); // length mismatch means this row belongs to another class + emitter.instruction("str x11, [sp, #40]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #40]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction(&format!("b.ne {skip_label}")); // class mismatch means scanning must continue + emitter.instruction("str x11, [sp, #40]"); // save the row index across appending the member name + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed result string array + emitter.instruction("ldr x10, [sp, #32]"); // reload the matched member metadata row + emitter.instruction("ldr x1, [x10, #16]"); // pass the stored member-name pointer + emitter.instruction("ldr x2, [x10, #24]"); // pass the stored member-name length + emitter.instruction(&format!("bl {string_array_push_symbol}")); // append the matched member name to the result array + emitter.instruction(&format!("cbz x0, {miss_label}")); // malformed append state reports a null pointer to Rust + emitter.instruction("str x0, [sp, #24]"); // save the updated boxed result string array + emitter.instruction("ldr x11, [sp, #40]"); // restore the row index after appending the member name + emitter.label(&skip_label); + emitter.instruction("ldr x10, [sp, #32]"); // reload the current member metadata row + emitter.instruction(&format!("add x10, x10, #{row_stride}")); // advance to the next reflection metadata row + emitter.instruction("str x10, [sp, #32]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction(&format!("b {loop_label}")); // continue scanning member metadata rows + emitter.label(&done_label); + emitter.instruction("ldr x0, [sp, #24]"); // return the boxed result string array + emitter.instruction(&format!("b {label_prefix}_ret")); // share the frame teardown path + emitter.label(&miss_label); + emitter.instruction("mov x0, xzr"); // return a null pointer when allocation or append failed + emitter.label(&format!("{label_prefix}_ret")); + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the member-name scan frame + emitter.instruction("ret"); // return the boxed name array, or null on failure, to Rust + emitter.comment(&format!("--- end eval reflection {member_kind} names ---")); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionMethod predicate flags. +fn emit_aarch64_eval_reflection_method_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_flags"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested method-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_method_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-method row count + emitter.instruction("cbz x9, __elephc_eval_reflection_method_flags_miss"); // an empty table cannot contain the requested method + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_methods"); + emitter.instruction("str x10, [sp, #40]"); // save the current method metadata row + emitter.instruction("mov x11, #0"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_flags_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the method metadata row count + emitter.instruction("cmp x11, x9"); // have all method metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_method_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the method-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored method-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested method-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested method-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // length mismatch means the method cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the method-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested method-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested method-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored method-name pointer + emitter.instruction("mov x4, x12"); // pass the stored method-name length + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the method-name compare + emitter.instruction("cmp x0, #0"); // did the requested method name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // method mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched method metadata row + emitter.instruction("ldr x0, [x10, #32]"); // return the row's ReflectionMethod predicate flags + emitter.instruction("b __elephc_eval_reflection_method_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_flags_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte method metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_method_flags_loop"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the method metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the ARM64 eval hook that returns a matched AOT ReflectionMethod declaring class. +fn emit_aarch64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_declaring_class"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested method-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_method_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-method row count + emitter.instruction("cbz x9, __elephc_eval_reflection_method_declaring_class_miss"); // an empty table cannot contain the requested method + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_methods"); + emitter.instruction("str x10, [sp, #40]"); // save the current method metadata row + emitter.instruction("mov x11, #0"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_declaring_class_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the method metadata row count + emitter.instruction("cmp x11, x9"); // have all method metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_method_declaring_class_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the method-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored method-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested method-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested method-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // length mismatch means the method cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the method-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested method-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested method-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored method-name pointer + emitter.instruction("mov x4, x12"); // pass the stored method-name length + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the method-name compare + emitter.instruction("cmp x0, #0"); // did the requested method name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // method mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched method metadata row + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("ldr x1, [x10, #40]"); // load the declaring class-name pointer + emitter.instruction("ldr x2, [x10, #48]"); // load the declaring class-name length + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("b __elephc_eval_reflection_method_declaring_class_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_declaring_class_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte method metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_method_declaring_class_loop"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_declaring_class_miss"); + emitter.instruction("mov x0, xzr"); // return null when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_declaring_class_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the method metadata scan frame + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + +/// Emits the ARM64 eval hook that returns a matched AOT ReflectionProperty declaring class. +fn emit_aarch64_eval_reflection_property_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_declaring_class"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_property_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-property row count + emitter.instruction("cbz x9, __elephc_eval_reflection_property_declaring_class_miss"); // an empty table cannot contain the requested property + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_properties"); + emitter.instruction("str x10, [sp, #40]"); // save the current property metadata row + emitter.instruction("mov x11, #0"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_declaring_class_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the property metadata row count + emitter.instruction("cmp x11, x9"); // have all property metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_property_declaring_class_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the property-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored property-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested property-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested property-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // length mismatch means the property cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the property-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested property-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored property-name pointer + emitter.instruction("mov x4, x12"); // pass the stored property-name length + emitter.instruction("bl __rt_strcasecmp"); // compare property names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the property-name compare + emitter.instruction("cmp x0, #0"); // did the requested property name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // property mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched property metadata row + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("ldr x1, [x10, #40]"); // load the declaring class-name pointer + emitter.instruction("ldr x2, [x10, #48]"); // load the declaring class-name length + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("b __elephc_eval_reflection_property_declaring_class_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_declaring_class_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte property metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_property_declaring_class_loop"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_declaring_class_miss"); + emitter.instruction("mov x0, xzr"); // return null when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_declaring_class_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the property metadata scan frame + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionProperty predicate flags. +fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_flags"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_property_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-property row count + emitter.instruction("cbz x9, __elephc_eval_reflection_property_flags_miss"); // an empty table cannot contain the requested property + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_properties"); + emitter.instruction("str x10, [sp, #40]"); // save the current property metadata row + emitter.instruction("mov x11, #0"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_flags_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the property metadata row count + emitter.instruction("cmp x11, x9"); // have all property metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_property_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the property-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored property-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested property-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested property-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // length mismatch means the property cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the property-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested property-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored property-name pointer + emitter.instruction("mov x4, x12"); // pass the stored property-name length + emitter.instruction("bl __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the property-name compare + emitter.instruction("cmp x0, #0"); // did the requested property name match this row? + emitter.instruction("b.eq __elephc_eval_reflection_property_flags_skip"); // property mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched property metadata row + emitter.instruction("ldr x0, [x10, #32]"); // return the row's ReflectionProperty predicate flags + emitter.instruction("b __elephc_eval_reflection_property_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_flags_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte property metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_property_flags_loop"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the property metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionMethod names. +fn emit_x86_64_eval_reflection_method_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_method_names", + "_eval_reflection_method_count", + "_eval_reflection_methods", + "__elephc_eval_reflection_method_names_x86", + "method", + 56, + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionProperty names. +fn emit_x86_64_eval_reflection_property_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_property_names", + "_eval_reflection_property_count", + "_eval_reflection_properties", + "__elephc_eval_reflection_property_names_x86", + "property", + 56, + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass interface names. +fn emit_x86_64_eval_reflection_class_interface_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_interface_names", + "_eval_reflection_class_interface_count", + "_eval_reflection_class_interfaces", + "__elephc_eval_reflection_class_interface_names_x86", + "class interface", + 32, + ); +} + +/// Emits the x86_64 eval hook that returns AOT `class_uses()` trait names. +fn emit_x86_64_eval_reflection_class_trait_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_names", + "_eval_reflection_class_trait_count", + "_eval_reflection_class_traits", + "__elephc_eval_reflection_class_trait_names_x86", + "class trait", + 32, + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass trait alias names. +fn emit_x86_64_eval_reflection_class_trait_alias_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_names", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_aliases", + "__elephc_eval_reflection_class_trait_alias_names_x86", + "class trait alias", + 32, + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass trait alias sources. +fn emit_x86_64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_sources", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_alias_sources", + "__elephc_eval_reflection_class_trait_alias_sources_x86", + "class trait alias source", + 32, + ); +} + +/// Emits the x86_64 eval hook that returns the AOT reflection source file. +fn emit_x86_64_eval_reflection_source_file(emitter: &mut Emitter) { + let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + label_c_global(emitter, "__elephc_eval_reflection_source_file"); + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_source_file_len"); + emitter.instruction("mov rsi, QWORD PTR [r10]"); // load the generated source-file length + emitter.instruction("test rsi, rsi"); // is source-file metadata available? + emitter.instruction("jz __elephc_eval_reflection_source_file_miss_x86"); // report no source file when EIR metadata is absent + abi::emit_symbol_address(emitter, "rdi", "_eval_reflection_source_file"); + emitter.instruction(&format!("jmp {string_symbol}")); // box the generated source-file path for Rust + emitter.label("__elephc_eval_reflection_source_file_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no source file is available + emitter.instruction("ret"); // finish the source-file metadata lookup +} + +/// Emits an x86_64 class-filtered AOT reflection member-name scanner. +fn emit_x86_64_eval_reflection_member_names( + emitter: &mut Emitter, + symbol: &str, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + member_kind: &str, + row_stride: u64, +) { + let loop_label = format!("{label_prefix}_loop"); + let skip_label = format!("{label_prefix}_skip"); + let miss_label = format!("{label_prefix}_miss"); + let done_label = format!("{label_prefix}_done"); + let string_array_new_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + let string_array_push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + label_c_global(emitter, symbol); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable member-name scan frame pointer + emitter.instruction("sub rsp, 80"); // reserve scan state across allocation and string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", count_symbol); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection member row count + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across helper calls + emitter.instruction("mov rdi, r10"); // use the full table count as a safe result-array capacity + emitter.instruction(&format!("call {string_array_new_symbol}")); // allocate the boxed result string array + emitter.instruction(&format!("test rax, rax")); // did allocation return a usable boxed array? + emitter.instruction(&format!("jz {miss_label}")); // allocation failure reports a null pointer to Rust + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the boxed result string array + abi::emit_symbol_address(emitter, "r11", table_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the current member metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at member metadata row zero + emitter.label(&loop_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the member metadata row count + emitter.instruction("cmp r11, r10"); // have all member metadata rows been scanned? + emitter.instruction(&format!("jae {done_label}")); // return the accumulated names after the final row + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current member metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction(&format!("jne {skip_label}")); // length mismatch means this row belongs to another class + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction(&format!("jne {skip_label}")); // class mismatch means scanning must continue + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the row index across appending the member name + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the matched member metadata row + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed result string array + emitter.instruction("mov rsi, QWORD PTR [r10 + 16]"); // pass the stored member-name pointer + emitter.instruction("mov rdx, QWORD PTR [r10 + 24]"); // pass the stored member-name length + emitter.instruction(&format!("call {string_array_push_symbol}")); // append the matched member name to the result array + emitter.instruction("test rax, rax"); // did append return a usable boxed array? + emitter.instruction(&format!("jz {miss_label}")); // malformed append state reports a null pointer to Rust + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the updated boxed result string array + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // restore the row index after appending the member name + emitter.label(&skip_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current member metadata row + emitter.instruction(&format!("add r10, {row_stride}")); // advance to the next reflection metadata row + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction(&format!("jmp {loop_label}")); // continue scanning member metadata rows + emitter.label(&done_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the boxed result string array + emitter.instruction(&format!("jmp {label_prefix}_ret")); // share the frame teardown path + emitter.label(&miss_label); + emitter.instruction("xor eax, eax"); // return a null pointer when allocation or append failed + emitter.label(&format!("{label_prefix}_ret")); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed name array, or null on failure, to Rust + emitter.comment(&format!("--- end eval reflection {member_kind} names ---")); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionMethod predicate flags. +fn emit_x86_64_eval_reflection_method_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested method-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_method_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-method row count + emitter.instruction("test r10, r10"); // is the method metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_method_flags_miss_x86"); // an empty table cannot contain the requested method + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_methods"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current method metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the method metadata row count + emitter.instruction("cmp r11, r10"); // have all method metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_method_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the method-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored method-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested method-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // length mismatch means the method cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the method-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested method-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored method-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the method-name compare + emitter.instruction("test rax, rax"); // did the requested method name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // method mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched method metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 32]"); // return the row's ReflectionMethod predicate flags + emitter.instruction("jmp __elephc_eval_reflection_method_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte method metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_method_flags_loop_x86"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns a matched AOT ReflectionMethod declaring class. +fn emit_x86_64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_declaring_class"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested method-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_method_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-method row count + emitter.instruction("test r10, r10"); // is the method metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_method_declaring_class_miss_x86"); // an empty table cannot contain the requested method + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_methods"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current method metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_declaring_class_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the method metadata row count + emitter.instruction("cmp r11, r10"); // have all method metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_method_declaring_class_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the method-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored method-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested method-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // length mismatch means the method cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the method-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested method-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored method-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the method-name compare + emitter.instruction("test rax, rax"); // did the requested method name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // method mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched method metadata row + emitter.instruction("mov rdi, QWORD PTR [r10 + 40]"); // load the declaring class-name pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 48]"); // load the declaring class-name length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("jmp __elephc_eval_reflection_method_declaring_class_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_declaring_class_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte method metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_method_declaring_class_loop_x86"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_declaring_class_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_declaring_class_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns a matched AOT ReflectionProperty declaring class. +fn emit_x86_64_eval_reflection_property_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_declaring_class"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_property_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-property row count + emitter.instruction("test r10, r10"); // is the property metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_property_declaring_class_miss_x86"); // an empty table cannot contain the requested property + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_properties"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current property metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_declaring_class_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the property metadata row count + emitter.instruction("cmp r11, r10"); // have all property metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_property_declaring_class_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the property-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored property-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested property-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // length mismatch means the property cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the property-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested property-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored property-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare property names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the property-name compare + emitter.instruction("test rax, rax"); // did the requested property name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // property mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched property metadata row + emitter.instruction("mov rdi, QWORD PTR [r10 + 40]"); // load the declaring class-name pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 48]"); // load the declaring class-name length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("jmp __elephc_eval_reflection_property_declaring_class_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_declaring_class_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte property metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_property_declaring_class_loop_x86"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_declaring_class_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_declaring_class_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionProperty predicate flags. +fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_property_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-property row count + emitter.instruction("test r10, r10"); // is the property metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_property_flags_miss_x86"); // an empty table cannot contain the requested property + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_properties"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current property metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the property metadata row count + emitter.instruction("cmp r11, r10"); // have all property metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_property_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the property-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored property-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested property-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // length mismatch means the property cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the property-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested property-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored property-name pointer + emitter.instruction("call __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the property-name compare + emitter.instruction("test rax, rax"); // did the requested property name match this row? + emitter.instruction("jz __elephc_eval_reflection_property_flags_skip_x86"); // property mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched property metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 32]"); // return the row's ReflectionProperty predicate flags + emitter.instruction("jmp __elephc_eval_reflection_property_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte property metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_property_flags_loop_x86"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits a class-like name membership scanner for ARM64 eval bridge hooks. +fn emit_aarch64_eval_name_table_exists( + emitter: &mut Emitter, + exported_label: &str, + count_symbol: &str, + table_symbol: &str, + local_stem: &str, +) { + label_c_global(emitter, exported_label); + emitter.instruction("sub sp, sp, #64"); // reserve lookup state while comparing metadata names + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across string compares + emitter.instruction("add x29, sp, #48"); // establish a stable name-table lookup frame + emitter.instruction("str x0, [sp, #0]"); // save the requested name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested name length + abi::emit_symbol_address(emitter, "x9", count_symbol); + emitter.instruction("ldr x9, [x9]"); // load the metadata-name table count + emitter.instruction(&format!("cbz x9, {local_stem}_miss")); // an empty table cannot contain the requested name + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", table_symbol); + emitter.instruction("str x10, [sp, #24]"); // save the current metadata-name table cursor + emitter.instruction("mov x11, #0"); // start scanning at table index zero + emitter.label(&format!("{local_stem}_loop")); + emitter.instruction("ldr x9, [sp, #16]"); // reload the metadata-name table count + emitter.instruction("cmp x11, x9"); // have all metadata-name entries been scanned? + emitter.instruction(&format!("b.ge {local_stem}_miss")); // no metadata name matched before the end + emitter.instruction("ldr x10, [sp, #24]"); // reload the current metadata-name table entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored metadata-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested name length + emitter.instruction("cmp x12, x2"); // compare stored and requested name lengths + emitter.instruction(&format!("b.ne {local_stem}_skip")); // length mismatch means this entry cannot match + emitter.instruction("str x11, [sp, #32]"); // save the table index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested name length + emitter.instruction("ldr x3, [x10]"); // pass the stored metadata-name pointer + emitter.instruction("mov x4, x12"); // pass the stored metadata-name length + emitter.instruction("bl __rt_strcasecmp"); // compare names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the table index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested name match this entry? + emitter.instruction(&format!("b.eq {local_stem}_hit")); // report true on a metadata-name match + emitter.label(&format!("{local_stem}_skip")); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current metadata-name table entry + emitter.instruction("add x10, x10, #16"); // advance to the next metadata-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the table index + emitter.instruction(&format!("b {local_stem}_loop")); // continue scanning the metadata-name table + emitter.label(&format!("{local_stem}_hit")); + emitter.instruction("mov x0, #1"); // return true for a matched metadata name + emitter.instruction(&format!("b {local_stem}_done")); // skip the false result after a match + emitter.label(&format!("{local_stem}_miss")); + emitter.instruction("mov x0, #0"); // return false when no metadata name matched + emitter.label(&format!("{local_stem}_done")); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the name-table lookup frame + emitter.instruction("ret"); // return the metadata-name existence flag to Rust +} + +/// Emits an x86_64 eval wrapper that scans one `(name_ptr, name_len)` metadata table. +fn emit_x86_64_eval_name_table_exists( + emitter: &mut Emitter, + exported_label: &str, + count_symbol: &str, + table_symbol: &str, + local_stem: &str, +) { + label_c_global(emitter, exported_label); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable name-table lookup frame + emitter.instruction("sub rsp, 48"); // reserve slots for name, count, cursor, and index + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested name length + abi::emit_symbol_address(emitter, "r10", count_symbol); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the metadata-name table count + emitter.instruction("test r10, r10"); // is the metadata-name table empty? + emitter.instruction(&format!("jz {local_stem}_miss")); // an empty table cannot contain the requested name + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", table_symbol); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current metadata-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning at table index zero + emitter.label(&format!("{local_stem}_loop")); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the metadata-name table count + emitter.instruction("cmp r11, r10"); // have all metadata-name entries been scanned? + emitter.instruction(&format!("jae {local_stem}_miss")); // no metadata name matched before the end + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current metadata-name table entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored metadata-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested name lengths + emitter.instruction(&format!("jne {local_stem}_skip")); // length mismatch means this entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the table index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored metadata-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the table index after the string compare + emitter.instruction("test rax, rax"); // did the requested name match this entry? + emitter.instruction(&format!("je {local_stem}_hit")); // report true on a metadata-name match + emitter.label(&format!("{local_stem}_skip")); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current metadata-name table entry + emitter.instruction("add r10, 16"); // advance to the next metadata-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the table index + emitter.instruction(&format!("jmp {local_stem}_loop")); // continue scanning the metadata-name table + emitter.label(&format!("{local_stem}_hit")); + emitter.instruction("mov eax, 1"); // return true for a matched metadata name + emitter.instruction(&format!("jmp {local_stem}_done")); // skip the false result after a match + emitter.label(&format!("{local_stem}_miss")); + emitter.instruction("xor eax, eax"); // return false when no metadata name matched + emitter.label(&format!("{local_stem}_done")); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the metadata-name existence flag to Rust +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionClass flags. +fn emit_aarch64_eval_reflection_class_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_class_flags"); + emitter.instruction("sub sp, sp, #64"); // reserve class-flag scan state across string comparisons + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #48"); // establish a stable class-flag scan frame + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_class_count"); + emitter.instruction("ldr x9, [x9]"); // load the class-flag metadata row count + emitter.instruction("cbz x9, __elephc_eval_reflection_class_flags_miss"); // an empty table cannot contain class flags + emitter.instruction("str x9, [sp, #16]"); // save the class-flag row count + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_classes"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-flag metadata row + emitter.instruction("mov x11, #0"); // start scanning at class-flag row zero + emitter.label("__elephc_eval_reflection_class_flags_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-flag metadata row count + emitter.instruction("cmp x11, x9"); // have all class-flag rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_class_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-flag metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_class_flags_skip"); // length mismatch means this row belongs to another class + emitter.instruction("str x11, [sp, #32]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_class_flags_skip"); // class mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #24]"); // reload the matched class-flag metadata row + emitter.instruction("ldr x0, [x10, #16]"); // return the row's ReflectionClass predicate flags + emitter.instruction("b __elephc_eval_reflection_class_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_class_flags_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-flag metadata row + emitter.instruction("add x10, x10, #24"); // advance to the next 24-byte class-flag row + emitter.instruction("str x10, [sp, #24]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_class_flags_loop"); // continue scanning class-flag metadata rows + emitter.label("__elephc_eval_reflection_class_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT class flags matched + emitter.label("__elephc_eval_reflection_class_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the class-flag metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass flags. +fn emit_x86_64_eval_reflection_class_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_class_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable class-flag scan frame + emitter.instruction("sub rsp, 48"); // reserve class-name, count, cursor, and index slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_class_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the class-flag metadata row count + emitter.instruction("test r10, r10"); // is the class-flag metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_class_flags_miss_x86"); // an empty table cannot contain class flags + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the class-flag row count + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_classes"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-flag metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at class-flag row zero + emitter.label("__elephc_eval_reflection_class_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-flag metadata row count + emitter.instruction("cmp r11, r10"); // have all class-flag rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_class_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-flag metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_class_flags_skip_x86"); // length mismatch means this row belongs to another class + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_class_flags_skip_x86"); // class mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the matched class-flag metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 16]"); // return the row's ReflectionClass predicate flags + emitter.instruction("jmp __elephc_eval_reflection_class_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_class_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-flag metadata row + emitter.instruction("add r10, 24"); // advance to the next 24-byte class-flag row + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_class_flags_loop_x86"); // continue scanning class-flag metadata rows + emitter.label("__elephc_eval_reflection_class_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT class flags matched + emitter.label("__elephc_eval_reflection_class_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the ARM64 eval bridge wrapper for cloning boxed object cells. +fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("sub sp, sp, #80"); // reserve source, clone, descriptor, counters, and wrapper frame slots + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address across clone helper calls + emitter.instruction("add x29, sp, #64"); // establish a stable clone wrapper frame pointer + emitter.instruction("cbz x0, __elephc_eval_value_object_clone_shallow_null"); // null handles cannot be cloned as objects + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-object values cannot be cloned by this bridge + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_clone_shallow_null"); // malformed object payloads cannot be cloned + emitter.instruction("str x9, [sp, #0]"); // save the source object payload pointer + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("str x11, [sp, #56]"); // save class id across allocation and ownership calls + emit_aarch64_reject_runtime_managed_clone_classes(emitter, "x11", "__elephc_eval_value_object_clone_shallow_null"); + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); + emitter.instruction("ldr x10, [x10]"); // load the number of emitted class descriptors + emitter.instruction("cmp x11, x10"); // is this class id inside the descriptor table? + emitter.instruction("b.hs __elephc_eval_value_object_clone_shallow_null"); // unknown class layouts cannot be cloned by the eval bridge + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_ptrs"); + emitter.instruction("lsl x12, x11, #3"); // scale class id to an 8-byte descriptor pointer slot + emitter.instruction("ldr x10, [x10, x12]"); // load the class property-tag descriptor pointer + emitter.instruction("str x10, [sp, #16]"); // save descriptor pointer for the property-copy loop + abi::emit_symbol_address(emitter, "x10", "_class_object_payload_sizes"); + emitter.instruction("ldr x12, [x10, x12]"); // load the class-declared object payload size + emitter.instruction("str x12, [sp, #48]"); // save payload size for allocation and dyn-prop offsets + emitter.instruction("mov x0, x12"); // pass the source payload size to the heap allocator + emitter.instruction("bl __rt_heap_alloc"); // allocate a clone object payload with the same byte size + emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers + emitter.instruction("str x9, [x0, #-8]"); // stamp the uniform object heap header + emitter.instruction("ldr x11, [sp, #56]"); // reload the source class id + emitter.instruction("str x11, [x0]"); // store the class id at the clone payload head + emitter.instruction("str x0, [sp, #8]"); // save the clone object payload pointer + emitter.instruction("ldr x12, [sp, #48]"); // reload the payload size + emitter.instruction("sub x12, x12, #8"); // remove the leading class id field from the clone layout + emitter.instruction("ldr x13, [sp, #56]"); // reload the source class id for dynamic-property metadata + emitter.instruction("lsl x13, x13, #3"); // scale class id to an 8-byte dynamic-flag slot + abi::emit_symbol_address(emitter, "x10", "_class_object_dynamic_prop_flags"); + emitter.instruction("ldr x13, [x10, x13]"); // load whether this class layout has a dyn-props tail + emitter.instruction("cbz x13, __elephc_eval_value_object_clone_shallow_count_ready"); // no dyn-props tail contributes to property count + emitter.instruction("sub x12, x12, #8"); // remove the dyn-props tail before counting declared slots + emitter.label("__elephc_eval_value_object_clone_shallow_count_ready"); + emitter.instruction("lsr x12, x12, #4"); // derive declared-property slot count from the payload size + emitter.instruction("str x12, [sp, #24]"); // save property count for the copy loop + emitter.instruction("str xzr, [sp, #32]"); // initialize property-copy index to zero + + emitter.label("__elephc_eval_value_object_clone_shallow_prop_loop"); + emitter.instruction("ldr x12, [sp, #32]"); // reload the current property index + emitter.instruction("ldr x13, [sp, #24]"); // reload the declared-property slot count + emitter.instruction("cmp x12, x13"); // has every declared property slot been copied? + emitter.instruction("b.ge __elephc_eval_value_object_clone_shallow_dyn"); // move on to the optional dynamic-property hash + emitter.instruction("mov x10, #16"); // each declared-property slot is two 8-byte words + emitter.instruction("mul x10, x12, x10"); // compute the byte offset inside the property region + emitter.instruction("add x10, x10, #8"); // skip the leading class id to reach this slot + emitter.instruction("ldr x9, [sp, #0]"); // reload the source object pointer + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer + emitter.instruction("ldr x15, [x9, x10]"); // copy the source property low word and keep it for retains + emitter.instruction("str x15, [x11, x10]"); // store the property low word on the clone + emitter.instruction("add x10, x10, #8"); // advance to the high word of the property slot + emitter.instruction("ldr x14, [x9, x10]"); // copy the source property high word + emitter.instruction("str x14, [x11, x10]"); // store the property high word on the clone + emitter.instruction("ldr x11, [sp, #16]"); // reload the property-tag descriptor pointer + emitter.instruction("ldrb w14, [x11, x12]"); // load the compile-time ownership tag for this slot + emitter.instruction("cmp x14, #1"); // does the slot hold an owned string payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_string"); // string slots need an independent payload copy + emitter.instruction("cmp x14, #4"); // does the slot hold a retained indexed-array payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained array slots need an extra owner reference + emitter.instruction("cmp x14, #5"); // does the slot hold a retained associative-array payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained hash slots need an extra owner reference + emitter.instruction("cmp x14, #6"); // does the slot hold a retained object payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained object slots need an extra owner reference + emitter.instruction("cmp x14, #7"); // does the slot hold a retained boxed Mixed payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained Mixed slots need an extra owner reference + emitter.instruction("b __elephc_eval_value_object_clone_shallow_next"); // scalar slots are copied without ownership changes + + emitter.label("__elephc_eval_value_object_clone_shallow_string"); + emitter.instruction("str x12, [sp, #32]"); // preserve property index across string persistence + emitter.instruction("str x10, [sp, #40]"); // preserve the high-word slot offset across the helper + emitter.instruction("mov x1, x15"); // pass the source string pointer to the persistence helper + emitter.instruction("ldr x2, [x9, x10]"); // pass the source string length to the persistence helper + emitter.instruction("bl __rt_str_persist"); // duplicate the string payload for clone ownership + emitter.instruction("ldr x10, [sp, #40]"); // restore the high-word slot offset after persistence + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer after persistence + emitter.instruction("sub x10, x10, #8"); // move back to the low-word string pointer slot + emitter.instruction("str x1, [x11, x10]"); // install the persisted string pointer on the clone + emitter.instruction("add x10, x10, #8"); // move to the high-word string length slot + emitter.instruction("str x2, [x11, x10]"); // install the persisted string length on the clone + emitter.instruction("ldr x12, [sp, #32]"); // restore property index after string persistence + emitter.instruction("b __elephc_eval_value_object_clone_shallow_next"); // continue with the next declared property + + emitter.label("__elephc_eval_value_object_clone_shallow_retain"); + emitter.instruction("str x12, [sp, #32]"); // preserve property index across the retain helper + emitter.instruction("mov x0, x15"); // pass the copied heap payload to the retain helper + emitter.instruction("bl __rt_incref"); // retain the shared property payload for the cloned object + emitter.instruction("ldr x12, [sp, #32]"); // restore property index after the retain helper + + emitter.label("__elephc_eval_value_object_clone_shallow_next"); + emitter.instruction("add x12, x12, #1"); // advance to the next declared-property slot + emitter.instruction("str x12, [sp, #32]"); // save the advanced property-copy index + emitter.instruction("b __elephc_eval_value_object_clone_shallow_prop_loop"); // continue copying declared properties + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn"); + emitter.instruction("ldr x12, [sp, #56]"); // reload the source class id for dynamic-property metadata + emitter.instruction("lsl x12, x12, #3"); // scale class id to an 8-byte dynamic-flag slot + abi::emit_symbol_address(emitter, "x10", "_class_object_dynamic_prop_flags"); + emitter.instruction("ldr x12, [x10, x12]"); // load whether this class layout has a dyn-props tail + emitter.instruction("cbz x12, __elephc_eval_value_object_clone_shallow_box"); // no dynamic hash slot: box the copied clone + emitter.instruction("ldr x13, [sp, #48]"); // reload the class-declared payload size + emitter.instruction("sub x13, x13, #8"); // compute dyn-props slot offset as payload_size - 8 + emitter.instruction("str x13, [sp, #40]"); // save dyn-props slot offset across hash cloning + emitter.instruction("ldr x9, [sp, #0]"); // reload the source object pointer + emitter.instruction("ldr x10, [x9, x13]"); // load the source dynamic-property hash pointer + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer + emitter.instruction("cbz x10, __elephc_eval_value_object_clone_shallow_dyn_null"); // null source hash stays null on the clone + emitter.instruction("mov x0, x10"); // pass the source dynamic hash to the clone helper + emitter.instruction("bl __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("ldr x13, [sp, #40]"); // restore the dynamic-property slot offset + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer after hash cloning + emitter.instruction("str x0, [x11, x13]"); // install the cloned dynamic-property hash + emitter.instruction("b __elephc_eval_value_object_clone_shallow_box"); // box the clone after dynamic properties are installed + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_null"); + emitter.instruction("str xzr, [x11, x13]"); // clear the clone's dynamic-property hash slot + + emitter.label("__elephc_eval_value_object_clone_shallow_box"); + emitter.instruction("ldr x1, [sp, #8]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("b __elephc_eval_value_object_clone_shallow_done"); // skip the null sentinel after a successful clone + + emitter.label("__elephc_eval_value_object_clone_shallow_null"); + emitter.instruction("mov x0, xzr"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done"); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the clone wrapper frame + emitter.instruction("ret"); // return the boxed clone or null failure sentinel +} + +/// Emits the x86_64 eval bridge wrapper for cloning boxed object cells. +fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across clone calls + emitter.instruction("mov rbp, rsp"); // establish a stable clone wrapper frame pointer + emitter.instruction("sub rsp, 64"); // reserve source, clone, descriptor, counters, and payload slots + emitter.instruction("test rdi, rdi"); // null handles cannot be cloned as objects + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for null handles + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-object values cannot be cloned by this bridge + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // malformed object payloads cannot be cloned + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for missing payloads + emitter.instruction("mov QWORD PTR [rbp - 8], r10"); // save the source object payload pointer + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save class id across allocation and ownership calls + emit_x86_64_reject_runtime_managed_clone_classes(emitter, "rax", "__elephc_eval_value_object_clone_shallow_null_x86"); + abi::emit_load_symbol_to_reg(emitter, "r11", "_class_gc_desc_count", 0); + emitter.instruction("cmp rax, r11"); // is this class id inside the descriptor table? + emitter.instruction("jae __elephc_eval_value_object_clone_shallow_null_x86"); // unknown class layouts cannot be cloned by the eval bridge + abi::emit_symbol_address(emitter, "r11", "_class_gc_desc_ptrs"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load the class property-tag descriptor pointer + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // save descriptor pointer for the property-copy loop + abi::emit_symbol_address(emitter, "r11", "_class_object_payload_sizes"); + emitter.instruction("mov rcx, QWORD PTR [r11 + rax * 8]"); // load the class-declared object payload size + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save payload size for allocation and dyn-prop detection + emitter.instruction("mov rax, rcx"); // pass the source payload size to the heap allocator + emitter.instruction("call __rt_heap_alloc"); // allocate a clone object payload with the same byte size + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the uniform object heap header + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // reload the source class id + emitter.instruction("mov QWORD PTR [rax], rcx"); // store the class id at the clone payload head + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the clone object payload pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the payload size + emitter.instruction("sub r10, 8"); // remove the leading class id field from the clone layout + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the source class id for dynamic-property metadata + abi::emit_symbol_address(emitter, "r11", "_class_object_dynamic_prop_flags"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load whether this class layout has a dyn-props tail + emitter.instruction("test r11, r11"); // check whether dyn-props bytes should be excluded + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_count_ready_x86"); // no dyn-props tail contributes to property count + emitter.instruction("sub r10, 8"); // remove the dyn-props tail before counting declared slots + emitter.label("__elephc_eval_value_object_clone_shallow_count_ready_x86"); + emitter.instruction("shr r10, 4"); // derive declared-property slot count from the payload size + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save property count for the copy loop + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // initialize property-copy index to zero + + emitter.label("__elephc_eval_value_object_clone_shallow_prop_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current property index + emitter.instruction("cmp r10, QWORD PTR [rbp - 32]"); // has every declared property slot been copied? + emitter.instruction("jae __elephc_eval_value_object_clone_shallow_dyn_x86"); // move on to the optional dynamic-property hash + emitter.instruction("mov rcx, r10"); // copy property index before scaling it into a byte offset + emitter.instruction("shl rcx, 4"); // each declared-property slot is two 8-byte words + emitter.instruction("add rcx, 8"); // skip the leading class id to reach this slot + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the source object pointer + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the clone object pointer + emitter.instruction("mov rax, QWORD PTR [r11 + rcx]"); // copy the source property low word + emitter.instruction("mov rdx, QWORD PTR [r11 + rcx + 8]"); // copy the source property high word + emitter.instruction("mov QWORD PTR [r8 + rcx], rax"); // store the property low word on the clone + emitter.instruction("mov QWORD PTR [r8 + rcx + 8], rdx"); // store the property high word on the clone + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the property-tag descriptor pointer + emitter.instruction("movzx r11, BYTE PTR [r9 + r10]"); // load the compile-time ownership tag for this slot + emitter.instruction("cmp r11, 1"); // does the slot hold an owned string payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_string_x86"); // string slots need an independent payload copy + emitter.instruction("cmp r11, 4"); // does the slot hold a retained indexed-array payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained array slots need an extra owner reference + emitter.instruction("cmp r11, 5"); // does the slot hold a retained associative-array payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained hash slots need an extra owner reference + emitter.instruction("cmp r11, 6"); // does the slot hold a retained object payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained object slots need an extra owner reference + emitter.instruction("cmp r11, 7"); // does the slot hold a retained boxed Mixed payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained Mixed slots need an extra owner reference + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_next_x86"); // scalar slots are copied without ownership changes + + emitter.label("__elephc_eval_value_object_clone_shallow_string_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve property index across string persistence + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // preserve the low-word slot offset across the helper + emitter.instruction("call __rt_str_persist"); // duplicate the string payload for clone ownership + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // restore the low-word slot offset after persistence + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the clone object pointer after persistence + emitter.instruction("mov QWORD PTR [r8 + rcx], rax"); // install the persisted string pointer on the clone + emitter.instruction("mov QWORD PTR [r8 + rcx + 8], rdx"); // install the persisted string length on the clone + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_next_x86"); // continue with the next declared property + + emitter.label("__elephc_eval_value_object_clone_shallow_retain_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve property index across the retain helper + emitter.instruction("mov rdi, rax"); // pass the copied heap payload to the retain helper + emitter.instruction("call __rt_incref"); // retain the shared property payload for the cloned object + + emitter.label("__elephc_eval_value_object_clone_shallow_next_x86"); + emitter.instruction("add QWORD PTR [rbp - 40], 1"); // advance to the next declared-property slot + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_prop_loop_x86"); // continue copying declared properties + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the source class id for dynamic-property metadata + abi::emit_symbol_address(emitter, "r11", "_class_object_dynamic_prop_flags"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load whether this class layout has a dyn-props tail + emitter.instruction("test r11, r11"); // check whether a dyn-props hash slot exists + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_box_x86"); // no dynamic hash slot: box the copied clone + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the class-declared payload size + emitter.instruction("sub r10, 8"); // compute dyn-props slot offset as payload_size - 8 + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save dyn-props slot offset across hash cloning + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the source object pointer + emitter.instruction("mov rax, QWORD PTR [r11 + r10]"); // load the source dynamic-property hash pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the clone object pointer + emitter.instruction("test rax, rax"); // is the source dynamic-property hash present? + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_dyn_null_x86"); // null source hash stays null on the clone + emitter.instruction("mov rdi, rax"); // pass the source dynamic hash to the clone helper + emitter.instruction("call __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // restore the dynamic-property slot offset + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the clone object pointer after hash cloning + emitter.instruction("mov QWORD PTR [r11 + r10], rax"); // install the cloned dynamic-property hash + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_box_x86"); // box the clone after dynamic properties are installed + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_null_x86"); + emitter.instruction("mov QWORD PTR [r11 + r10], 0"); // clear the clone's dynamic-property hash slot + + emitter.label("__elephc_eval_value_object_clone_shallow_box_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_done_x86"); // skip the null sentinel after a successful clone + + emitter.label("__elephc_eval_value_object_clone_shallow_null_x86"); + emitter.instruction("xor eax, eax"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done_x86"); + emitter.instruction("add rsp, 64"); // release clone wrapper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed clone or null failure sentinel +} + +/// Emits ARM64 comparisons that keep runtime-managed object payloads out of the generic clone path. +fn emit_aarch64_reject_runtime_managed_clone_classes( + emitter: &mut Emitter, + class_id_reg: &str, + reject_label: &str, +) { + for symbol in [ + "_fiber_class_id", + "_generator_class_id", + "_spl_dll_class_id", + "_spl_stack_class_id", + "_spl_queue_class_id", + "_spl_fixed_array_class_id", + ] { + abi::emit_symbol_address(emitter, "x10", symbol); + emitter.instruction("ldr x10, [x10]"); // load one runtime-managed class id sentinel + emitter.instruction(&format!("cmp {}, x10", class_id_reg)); // compare source class id with the unsupported payload sentinel + emitter.instruction(&format!("b.eq {}", reject_label)); // reject custom runtime payload layouts + } +} + +/// Emits x86_64 comparisons that keep runtime-managed object payloads out of the generic clone path. +fn emit_x86_64_reject_runtime_managed_clone_classes( + emitter: &mut Emitter, + class_id_reg: &str, + reject_label: &str, +) { + for symbol in [ + "_fiber_class_id", + "_generator_class_id", + "_spl_dll_class_id", + "_spl_stack_class_id", + "_spl_queue_class_id", + "_spl_fixed_array_class_id", + ] { + abi::emit_load_symbol_to_reg(emitter, "r11", symbol, 0); + emitter.instruction(&format!("cmp {}, r11", class_id_reg)); // compare source class id with the unsupported payload sentinel + emitter.instruction(&format!("je {}", reject_label)); // reject custom runtime payload layouts + } +} + +/// Emits a global label with platform C-symbol mangling. +fn label_c_global(emitter: &mut Emitter, name: &str) { + let symbol = emitter.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen_support/runtime/eval_scope.rs b/src/codegen_support/runtime/eval_scope.rs new file mode 100644 index 0000000000..dfe2c6f8bb --- /dev/null +++ b/src/codegen_support/runtime/eval_scope.rs @@ -0,0 +1,434 @@ +//! Purpose: +//! Emits core runtime helpers for scope-only literal eval AOT fragments. +//! Provides a small materialized name-to-Mixed-cell map without linking the +//! optional Rust eval interpreter bridge. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` when `eval_scope` is +//! needed without `eval_bridge`. +//! +//! Key details: +//! - Entry names are generated static data labels, so the scope stores borrowed +//! name pointers instead of copying name bytes. +//! - Owned Mixed cells are released on overwrite and scope free. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +const EVAL_STATUS_OK: i64 = 0; +const EVAL_STATUS_RUNTIME_FATAL: i64 = 2; +const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; +const EVAL_SCOPE_FLAG_DIRTY: i64 = 1 << 2; +const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; + +/// Emits the target-specific eval-scope core helper surface. +pub(crate) fn emit_eval_scope_runtime(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: eval scope core helpers ---"); + match emitter.target.arch { + Arch::AArch64 => emit_aarch64_eval_scope_runtime(emitter), + Arch::X86_64 => emit_x86_64_eval_scope_runtime(emitter), + } +} + +/// Emits ARM64 eval-scope helpers. `__elephc_eval_value_null` comes from the +/// eval bridge value wrappers, which scope-only programs also emit. +fn emit_aarch64_eval_scope_runtime(emitter: &mut Emitter) { + emit_aarch64_eval_scope_new(emitter); + emit_aarch64_eval_scope_free(emitter); + emit_aarch64_eval_scope_set(emitter); + emit_aarch64_eval_scope_get(emitter); +} + +/// Emits the ARM64 scope allocator. +fn emit_aarch64_eval_scope_new(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_new"); + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the caller frame while allocating the scope header + emitter.instruction("mov x29, sp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov x0, #8"); // scope header stores one head pointer + emitter.instruction("bl __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("str xzr, [x0]"); // initialize the entry-list head to null + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the caller frame after allocation + emitter.instruction("ret"); // return the scope handle in x0 +} + +/// Emits the ARM64 scope destructor. +fn emit_aarch64_eval_scope_free(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_free"); + emitter.instruction("cbz x0, __elephc_eval_scope_free_done"); // null scope handles are already free + emitter.instruction("sub sp, sp, #48"); // reserve a frame for callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across release calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and current-entry registers + emitter.instruction("str x21, [sp, #32]"); // preserve the next-entry register + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // keep the scope header pointer across entry releases + emitter.instruction("ldr x20, [x19]"); // load the first entry in the scope list + emitter.label("__elephc_eval_scope_free_loop"); + emitter.instruction("cbz x20, __elephc_eval_scope_free_header"); // stop when every entry has been released + emitter.instruction("ldr x21, [x20]"); // save entry->next before freeing the current entry + emitter.instruction("ldr x9, [x20, #32]"); // load the entry ABI flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("b.eq __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("ldr x0, [x20, #24]"); // load the owned Mixed cell pointer + emitter.instruction("cbz x0, __elephc_eval_scope_free_entry"); // tolerate null owned cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.label("__elephc_eval_scope_free_entry"); + emitter.instruction("mov x0, x20"); // pass the current entry allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the entry record itself + emitter.instruction("mov x20, x21"); // advance to the saved next entry + emitter.instruction("b __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.label("__elephc_eval_scope_free_header"); + emitter.instruction("mov x0, x19"); // pass the scope header allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the scope header after all entries + emitter.instruction("ldr x21, [sp, #32]"); // restore the saved next-entry register + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scan registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the scope-free frame + emitter.label("__elephc_eval_scope_free_done"); + emitter.instruction("ret"); // return after the scope is fully released +} + +/// Emits the ARM64 scope setter. +fn emit_aarch64_eval_scope_set(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_set"); + emitter.instruction("cbz x0, __elephc_eval_scope_set_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_set_frame"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_set_fatal"); // non-empty names must provide bytes + emitter.label("__elephc_eval_scope_set_frame"); + emitter.instruction("sub sp, sp, #96"); // reserve a frame for inputs and callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across runtime calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and name pointer + emitter.instruction("stp x21, x22, [sp, #32]"); // preserve name length and cell pointer + emitter.instruction("stp x23, x24, [sp, #48]"); // preserve flags and previous-next pointer + emitter.instruction("stp x25, x26, [sp, #64]"); // preserve current entry and allocated entry pointer + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // save scope header pointer + emitter.instruction("mov x20, x1"); // save name byte pointer + emitter.instruction("mov x21, x2"); // save name byte length + emitter.instruction("mov x22, x3"); // save Mixed cell pointer + emitter.instruction("mov x23, x4"); // save caller-provided ABI flags + emitter.instruction("mov x24, x19"); // previous-next pointer initially addresses scope->head + emitter.instruction("ldr x25, [x24]"); // load the first candidate entry + emitter.label("__elephc_eval_scope_set_probe"); + emitter.instruction("cbz x25, __elephc_eval_scope_set_insert"); // insert when the name is absent + emitter.instruction("ldr x9, [x25, #16]"); // load candidate name length + emitter.instruction("cmp x9, x21"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x25, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_set_cmp"); + emitter.instruction("cmp x11, x21"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x20, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_set_next"); + emitter.instruction("mov x24, x25"); // previous-next pointer now addresses current->next + emitter.instruction("ldr x25, [x25]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.label("__elephc_eval_scope_set_update"); + emitter.instruction("ldr x9, [x25, #32]"); // load old entry flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("ldr x0, [x25, #24]"); // load the old Mixed cell pointer + emitter.instruction("cmp x0, x22"); // compare old and replacement cells + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("cbz x0, __elephc_eval_scope_set_store"); // tolerate null old cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.label("__elephc_eval_scope_set_store"); + emitter.instruction("str x22, [x25, #24]"); // store the replacement Mixed cell + emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x25, #32]"); // publish the updated ABI flags + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("b __elephc_eval_scope_set_done"); // restore the frame and return + emitter.label("__elephc_eval_scope_set_insert"); + emitter.instruction("mov x0, #40"); // entry records store next, name, length, cell, and flags + emitter.instruction("bl __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov x26, x0"); // keep the new entry pointer while initializing fields + emitter.instruction("str x25, [x26]"); // new entry next points at the old successor, normally null + emitter.instruction("str x20, [x26, #8]"); // store the borrowed static name pointer + emitter.instruction("str x21, [x26, #16]"); // store the name byte length + emitter.instruction("str x22, [x26, #24]"); // store the Mixed cell pointer + emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x26, #32]"); // store the new entry flags + emitter.instruction("str x26, [x24]"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.label("__elephc_eval_scope_set_done"); + emitter.instruction("ldp x25, x26, [sp, #64]"); // restore callee-saved entry registers + emitter.instruction("ldp x23, x24, [sp, #48]"); // restore callee-saved flag and link registers + emitter.instruction("ldp x21, x22, [sp, #32]"); // restore callee-saved name length and cell registers + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scope and name registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the setter frame + emitter.instruction("ret"); // return the eval status in x0 + emitter.label("__elephc_eval_scope_set_fatal"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope +} + +/// Emits the ARM64 scope getter. +fn emit_aarch64_eval_scope_get(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_get"); + emitter.instruction("cbz x0, __elephc_eval_scope_get_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_get_search"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_get_fatal"); // non-empty names must provide bytes + emitter.label("__elephc_eval_scope_get_search"); + emitter.instruction("ldr x9, [x0]"); // load the first entry from scope->head + emitter.label("__elephc_eval_scope_get_probe"); + emitter.instruction("cbz x9, __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("ldr x10, [x9, #16]"); // load candidate name length + emitter.instruction("cmp x10, x2"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x9, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_get_cmp"); + emitter.instruction("cmp x11, x2"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x1, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_get_next"); + emitter.instruction("ldr x9, [x9]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.label("__elephc_eval_scope_get_found"); + emitter.instruction("cbz x3, __elephc_eval_scope_get_found_flags"); // skip cell output when caller passed null + emitter.instruction("ldr x10, [x9, #24]"); // load the visible Mixed cell pointer + emitter.instruction("str x10, [x3]"); // write the output Mixed cell pointer + emitter.label("__elephc_eval_scope_get_found_flags"); + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("ldr w10, [x9, #32]"); // load the low ABI flag bits + emitter.instruction("str w10, [x4]"); // write the output ABI flags + emitter.instruction("b __elephc_eval_scope_get_ok"); // finish with success + emitter.label("__elephc_eval_scope_get_missing"); + emitter.instruction("cbz x3, __elephc_eval_scope_get_missing_flags"); // skip cell output when caller passed null + emitter.instruction("str xzr, [x3]"); // missing variables have no cell pointer + emitter.label("__elephc_eval_scope_get_missing_flags"); + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("str wzr, [x4]"); // missing variables have zero ABI flags + emitter.label("__elephc_eval_scope_get_ok"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in x0 + emitter.label("__elephc_eval_scope_get_fatal"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status +} + +/// Emits x86_64 eval-scope helpers. `__elephc_eval_value_null` comes from the +/// eval bridge value wrappers, which scope-only programs also emit. +fn emit_x86_64_eval_scope_runtime(emitter: &mut Emitter) { + emit_x86_64_eval_scope_new(emitter); + emit_x86_64_eval_scope_free(emitter); + emit_x86_64_eval_scope_set(emitter); + emit_x86_64_eval_scope_get(emitter); +} + +/// Emits the x86_64 scope allocator. +fn emit_x86_64_eval_scope_new(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_new"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before allocating the scope header + emitter.instruction("mov rbp, rsp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov rax, 8"); // scope header stores one head pointer + emitter.instruction("call __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("mov QWORD PTR [rax], 0"); // initialize the entry-list head to null + emitter.instruction("pop rbp"); // restore the caller frame pointer after allocation + emitter.instruction("ret"); // return the scope handle in rax +} + +/// Emits the x86_64 scope destructor. +fn emit_x86_64_eval_scope_free(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_free"); + emitter.instruction("test rdi, rdi"); // null scope handles are already free + emitter.instruction("jz __elephc_eval_scope_free_done"); // skip all cleanup for null handles + emitter.instruction("push rbp"); // preserve the caller frame pointer before release work + emitter.instruction("mov rbp, rsp"); // establish a stable frame for local scan state + emitter.instruction("sub rsp, 32"); // reserve scope, current-entry, and next-entry slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the scope header pointer + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first entry in the scope list + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the current entry pointer + emitter.label("__elephc_eval_scope_free_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the current entry pointer + emitter.instruction("test r10, r10"); // have all entries been released? + emitter.instruction("jz __elephc_eval_scope_free_header"); // stop when the entry list is exhausted + emitter.instruction("mov r11, QWORD PTR [r10]"); // save entry->next before freeing the current entry + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // keep the next entry across release calls + emitter.instruction("mov r11, QWORD PTR [r10 + 32]"); // load the entry ABI flags + emitter.instruction(&format!("test r11, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("jz __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("mov rax, QWORD PTR [r10 + 24]"); // load the owned Mixed cell pointer + emitter.instruction("test rax, rax"); // tolerate null owned cells defensively + emitter.instruction("jz __elephc_eval_scope_free_entry"); // skip release when the owned cell is null + emitter.instruction("call __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.label("__elephc_eval_scope_free_entry"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // pass the current entry allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the entry record itself + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the saved next entry + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // make the saved next entry current + emitter.instruction("jmp __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.label("__elephc_eval_scope_free_header"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // pass the scope header allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the scope header after all entries + emitter.instruction("add rsp, 32"); // release local scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.label("__elephc_eval_scope_free_done"); + emitter.instruction("ret"); // return after the scope is fully released +} + +/// Emits the x86_64 scope setter. +fn emit_x86_64_eval_scope_set(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_set"); + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_set_frame"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for invalid names + emitter.label("__elephc_eval_scope_set_frame"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before scope mutation + emitter.instruction("mov rbp, rsp"); // establish a stable frame for saved inputs and scan state + emitter.instruction("sub rsp, 64"); // reserve scope, name, length, cell, flags, previous-next, current, and index slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save scope header pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save name byte pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save name byte length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save Mixed cell pointer + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save caller-provided ABI flags + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // previous-next pointer initially addresses scope->head + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first candidate entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the current candidate entry + emitter.label("__elephc_eval_scope_set_probe"); + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate entry + emitter.instruction("test r9, r9"); // is the requested name absent? + emitter.instruction("jz __elephc_eval_scope_set_insert"); // insert when the scan reaches the end + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, QWORD PTR [rbp - 24]"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_set_cmp"); + emitter.instruction("cmp r11, QWORD PTR [rbp - 24]"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload requested name pointer + emitter.instruction("cmp al, BYTE PTR [r8 + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_set_next"); + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // previous-next pointer now addresses current->next + emitter.instruction("mov r10, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the next candidate as current + emitter.instruction("jmp __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.label("__elephc_eval_scope_set_update"); + emitter.instruction("mov r10, QWORD PTR [r9 + 32]"); // load old entry flags + emitter.instruction(&format!("test r10, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("jz __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("mov rax, QWORD PTR [r9 + 24]"); // load the old Mixed cell pointer + emitter.instruction("cmp rax, QWORD PTR [rbp - 32]"); // compare old and replacement cells + emitter.instruction("je __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("test rax, rax"); // tolerate null old cells defensively + emitter.instruction("jz __elephc_eval_scope_set_store"); // skip release when the old cell is null + emitter.instruction("call __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload current entry after the runtime call + emitter.label("__elephc_eval_scope_set_store"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the replacement Mixed cell + emitter.instruction("mov QWORD PTR [r9 + 24], r10"); // store the replacement Mixed cell + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the entry visible and dirty + emitter.instruction("mov QWORD PTR [r9 + 32], r10"); // publish the updated ABI flags + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("jmp __elephc_eval_scope_set_done"); // restore the frame and return + emitter.label("__elephc_eval_scope_set_insert"); + emitter.instruction("mov rax, 40"); // entry records store next, name, length, cell, and flags + emitter.instruction("call __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload old successor, normally null + emitter.instruction("mov QWORD PTR [rax], r9"); // new entry next points at the old successor + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload borrowed static name pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the borrowed static name pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload name byte length + emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the name byte length + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload Mixed cell pointer + emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the Mixed cell pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the new entry visible and dirty + emitter.instruction("mov QWORD PTR [rax + 32], r10"); // store the new entry flags + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the previous-next pointer + emitter.instruction("mov QWORD PTR [r11], rax"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.label("__elephc_eval_scope_set_done"); + emitter.instruction("add rsp, 64"); // release saved input and scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the eval status in rax + emitter.label("__elephc_eval_scope_set_fatal"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope +} + +/// Emits the x86_64 scope getter. +fn emit_x86_64_eval_scope_get(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_get"); + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_get_search"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for invalid names + emitter.label("__elephc_eval_scope_get_search"); + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the first entry from scope->head + emitter.label("__elephc_eval_scope_get_probe"); + emitter.instruction("test r9, r9"); // has the scan reached the end of the entry list? + emitter.instruction("jz __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, rdx"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_get_cmp"); + emitter.instruction("cmp r11, rdx"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("cmp al, BYTE PTR [rsi + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_get_next"); + emitter.instruction("mov r9, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("jmp __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.label("__elephc_eval_scope_get_found"); + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_found_flags"); // skip cell output for null out_cell + emitter.instruction("mov r10, QWORD PTR [r9 + 24]"); // load the visible Mixed cell pointer + emitter.instruction("mov QWORD PTR [rcx], r10"); // write the output Mixed cell pointer + emitter.label("__elephc_eval_scope_get_found_flags"); + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov r10d, DWORD PTR [r9 + 32]"); // load the low ABI flag bits + emitter.instruction("mov DWORD PTR [r8], r10d"); // write the output ABI flags + emitter.instruction("jmp __elephc_eval_scope_get_ok"); // finish with success + emitter.label("__elephc_eval_scope_get_missing"); + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_missing_flags"); // skip cell output for null out_cell + emitter.instruction("mov QWORD PTR [rcx], 0"); // missing variables have no cell pointer + emitter.label("__elephc_eval_scope_get_missing_flags"); + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov DWORD PTR [r8], 0"); // missing variables have zero ABI flags + emitter.label("__elephc_eval_scope_get_ok"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in rax + emitter.label("__elephc_eval_scope_get_fatal"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status +} + +/// Emits a global label with platform C-symbol mangling. +fn label_c_global(emitter: &mut Emitter, name: &str) { + let symbol = emitter.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index b21b4dca2c..cb79493943 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -15,6 +15,8 @@ mod callables; mod data; mod diagnostics; mod emitters; +mod eval_bridge; +mod eval_scope; mod exceptions; mod fibers; /// Runtime helpers for generator state management (yield, resume, stack frames). @@ -22,9 +24,9 @@ pub(crate) mod generators; mod io; mod objects; mod pointers; -mod strings; /// Standard PHP library constants, functions, and classes. pub(crate) mod spl; +mod strings; mod system; /// zval pack/unpack bridge helpers (elephc values ↔ PHP zval structs). mod zval; @@ -32,13 +34,14 @@ mod zval; pub(crate) use data::emit_runtime_data_fixed; /// Emit fixed runtime data section (symbols, constants, type metadata). pub(crate) use data::emit_runtime_data_user; +pub(crate) use data::{is_user_filter_contract_method, is_user_wrapper_contract_method}; /// Emit user-program-specific runtime data section. pub(crate) use emitters::emit_runtime; /// Emit full runtime helpers (orchestrates all runtime sections). pub(crate) use fibers::{ FIBER_CALLABLE_OFFSET, FIBER_PENDING_THROW_OFFSET, FIBER_STACK_BASE_OFFSET, - FIBER_STACK_SIZE_OFFSET, FIBER_START_ARG_COUNT_OFFSET, FIBER_START_ARGS_MAX, - FIBER_START_ARGS_OFFSET, FIBER_STATE_NOT_STARTED, FIBER_STATE_RUNNING, + FIBER_STACK_SIZE_OFFSET, FIBER_START_ARGS_MAX, FIBER_START_ARGS_OFFSET, + FIBER_START_ARG_COUNT_OFFSET, FIBER_STATE_NOT_STARTED, FIBER_STATE_RUNNING, FIBER_STATE_SUSPENDED, FIBER_STATE_TERMINATED, FIBER_TRANSFER_VALUE_OFFSET, FIBER_USER_ARG_MAX_OFFSET, }; diff --git a/src/codegen_support/runtime/objects/call_destructor.rs b/src/codegen_support/runtime/objects/call_destructor.rs index bf3823e9cb..cd2049399a 100644 --- a/src/codegen_support/runtime/objects/call_destructor.rs +++ b/src/codegen_support/runtime/objects/call_destructor.rs @@ -14,6 +14,9 @@ //! - `$this` is passed in the first integer argument register and is borrowed by //! the callee (no incref/decref around the call), matching normal method ABI, so //! the call cannot double-free the receiver. +//! - An optional eval callback can claim runtime-generic objects that actually +//! belong to eval-declared classes; when no callback is installed, the helper +//! follows the original static destructor table path. //! - Re-entrancy guard: before calling the destructor, bit 31 of the 32-bit //! refcount is set. A balanced `$tmp = $this;`/scope-exit inside the body then //! decrements from `0x8000_0001` back to `0x8000_0000` instead of reaching zero, @@ -44,6 +47,29 @@ fn emit_call_object_destructor_aarch64(emitter: &mut Emitter) { emitter.label_global("__rt_call_object_destructor"); emitter.instruction("cbz x0, __rt_call_object_destructor_ret"); // null receiver → nothing to destruct + emitter.instruction("ldr w9, [x0, #-12]"); // w9 = object refcount (header offset -12) + emitter.instruction("tbnz w9, #31, __rt_call_object_destructor_ret"); // destruction already in progress → never run twice + abi::emit_symbol_address(emitter, "x10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("ldr x10, [x10]"); // x10 = optional eval dynamic destructor callback + emitter.instruction("cbz x10, __rt_call_object_destructor_static"); // no eval callback installed → use static class table + emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = 0x80000000, the destruction-in-progress flag bit + emitter.instruction("orr w9, w9, w12"); // mark destruction in progress before boxing borrowed $this + emitter.instruction("str w9, [x0, #-12]"); // persist the guard flag in the refcount field + emitter.instruction("sub sp, sp, #32"); // allocate an aligned frame for the eval callback + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address before the Rust call + emitter.instruction("add x29, sp, #16"); // establish the helper frame + emitter.instruction("str x0, [sp, #0]"); // save the object pointer across the callback + emitter.instruction("blr x10"); // ask eval whether it owns and destructed this object + emitter.instruction("mov x12, x0"); // preserve the eval callback handled flag + emitter.instruction("ldr x0, [sp, #0]"); // restore the object pointer after the callback + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the eval callback frame + emitter.instruction("cbnz x12, __rt_call_object_destructor_ret"); // eval handled the dynamic object → skip static lookup + emitter.instruction("ldr w9, [x0, #-12]"); // reload the refcount after an eval miss + emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = destruction-in-progress flag bit + emitter.instruction("bic w9, w9, w12"); // clear the temporary eval guard before static lookup + emitter.instruction("str w9, [x0, #-12]"); // persist the restored refcount guard state + emitter.label("__rt_call_object_destructor_static"); emitter.instruction("ldr x11, [x0]"); // x11 = runtime class_id (object payload offset 0) // emit_load_symbol_to_reg uses x9 as scratch, so class_id is kept in x11. crate::codegen_support::abi::emit_load_symbol_to_reg(emitter, "x10", "_class_destruct_count", 0); @@ -53,7 +79,6 @@ fn emit_call_object_destructor_aarch64(emitter: &mut Emitter) { emitter.instruction("ldr x10, [x10, x11, lsl #3]"); // x10 = destructor symbol for this class (or 0) emitter.instruction("cbz x10, __rt_call_object_destructor_ret"); // class defines no __destruct → done emitter.instruction("ldr w9, [x0, #-12]"); // w9 = object refcount (header offset -12) - emitter.instruction("tbnz w9, #31, __rt_call_object_destructor_ret"); // destruction already in progress → never run twice emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = 0x80000000, the destruction-in-progress flag bit emitter.instruction("orr w9, w9, w12"); // mark destruction in progress so a balanced self-ref cannot re-enter the free path emitter.instruction("str w9, [x0, #-12]"); // persist the guard flag in the refcount field @@ -74,6 +99,29 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("test rdi, rdi"); // null receiver → nothing to destruct emitter.instruction("jz __rt_call_object_destructor_ret"); // skip the lookup for a null object + emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // eax = object refcount (header offset -12) + emitter.instruction("test eax, 0x80000000"); // is destruction already in progress? + emitter.instruction("jnz __rt_call_object_destructor_ret"); // never run a destructor twice + abi::emit_symbol_address(emitter, "r10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // r10 = optional eval dynamic destructor callback + emitter.instruction("test r10, r10"); // is the eval callback installed? + emitter.instruction("jz __rt_call_object_destructor_static_x86"); // no eval callback installed → use static class table + emitter.instruction("or eax, 0x80000000"); // mark destruction in progress before boxing borrowed $this + emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field + emitter.instruction("push rbp"); // align the stack and save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the eval callback frame + emitter.instruction("sub rsp, 16"); // reserve a spill slot for the object pointer + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the object pointer across the callback + emitter.instruction("call r10"); // ask eval whether it owns and destructed this object + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the object pointer after the callback + emitter.instruction("add rsp, 16"); // release the eval callback spill slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("test rax, rax"); // did eval handle this dynamic object? + emitter.instruction("jnz __rt_call_object_destructor_ret"); // eval handled the dynamic object → skip static lookup + emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // reload the refcount after an eval miss + emitter.instruction("and eax, 0x7fffffff"); // clear the temporary eval guard before static lookup + emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the restored refcount guard state + emitter.label("__rt_call_object_destructor_static_x86"); emitter.instruction("mov rax, QWORD PTR [rdi]"); // rax = runtime class_id (object payload offset 0) abi::emit_cmp_reg_to_symbol(emitter, "rax", "_class_destruct_count"); // is class_id within the destructor table? emitter.instruction("jae __rt_call_object_destructor_ret"); // out-of-range class ids have no destructor @@ -82,8 +130,6 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // class defines no __destruct? emitter.instruction("jz __rt_call_object_destructor_ret"); // nothing to call → done emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // eax = object refcount (header offset -12) - emitter.instruction("test eax, 0x80000000"); // is destruction already in progress? - emitter.instruction("jnz __rt_call_object_destructor_ret"); // never run a destructor twice emitter.instruction("or eax, 0x80000000"); // mark destruction in progress so a balanced self-ref cannot re-enter the free path emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field emitter.instruction("push rbp"); // align the stack and save the caller frame pointer diff --git a/src/codegen_support/runtime/objects/new_by_name.rs b/src/codegen_support/runtime/objects/new_by_name.rs index 3f0dbcb38d..f1e688d283 100644 --- a/src/codegen_support/runtime/objects/new_by_name.rs +++ b/src/codegen_support/runtime/objects/new_by_name.rs @@ -90,8 +90,10 @@ pub fn emit_new_by_name(emitter: &mut Emitter) { emitter.instruction("ldr x13, [x10, #24]"); // obj_size emitter.instruction("str x12, [sp, #32]"); // save class_id across the heap call emitter.instruction("str x13, [sp, #40]"); // save obj_size across the heap call + emit_runtime_managed_match_aarch64(emitter); // -- allocate the object payload -- + emitter.label("__rt_nbn_generic_alloc"); emitter.instruction("mov x0, x13"); // allocation size emitter.instruction("bl __rt_heap_alloc"); // x0 = object pointer emitter.instruction("mov x9, #4"); // heap kind 4 = object instance @@ -119,6 +121,7 @@ pub fn emit_new_by_name(emitter: &mut Emitter) { emitter.instruction("blr x10"); // _class_propinit_(this = object in x0) emitter.instruction("ldr x0, [sp, #40]"); // restore the object pointer (the thunk may clobber x0) emitter.label("__rt_nbn_no_propinit"); + emitter.label("__rt_nbn_return_allocated"); emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // release the frame emitter.instruction("ret"); // return the object pointer @@ -184,8 +187,10 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [r10 + 24]"); // obj_size emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // stash class_id emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // stash obj_size + emit_runtime_managed_match_x86_64(emitter); // -- allocate the object payload -- + emitter.label("__rt_nbn_generic_alloc_x86"); emitter.instruction("mov rax, rdx"); // allocation size emitter.instruction("call __rt_heap_alloc"); // rax = object pointer emitter.instruction(&format!( @@ -218,6 +223,7 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call r10"); // _class_propinit_(this) emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // restore the object pointer (the thunk may clobber rax) emitter.label("__rt_nbn_no_propinit_x86"); + emitter.label("__rt_nbn_return_allocated_x86"); emitter.instruction("add rsp, 48"); // release the frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the object pointer @@ -228,3 +234,60 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return null } + +/// Emits ARM64 class-id checks for runtime-managed builtin payload layouts. +fn emit_runtime_managed_match_aarch64(emitter: &mut Emitter) { + emitter.instruction("ldr x12, [sp, #32]"); // reload matched class id for runtime-managed allocation checks + abi::emit_symbol_address(emitter, "x10", "_spl_dll_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplDoublyLinkedList class id + emitter.instruction("cmp x12, x10"); // is the requested class SplDoublyLinkedList? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the SPL list payload for SplDoublyLinkedList + abi::emit_symbol_address(emitter, "x10", "_spl_stack_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplStack class id + emitter.instruction("cmp x12, x10"); // is the requested class SplStack? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the shared SPL list payload for SplStack + abi::emit_symbol_address(emitter, "x10", "_spl_queue_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplQueue class id + emitter.instruction("cmp x12, x10"); // is the requested class SplQueue? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the shared SPL list payload for SplQueue + abi::emit_symbol_address(emitter, "x10", "_spl_fixed_array_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplFixedArray class id + emitter.instruction("cmp x12, x10"); // is the requested class SplFixedArray? + emitter.instruction("b.eq __rt_nbn_alloc_spl_fixed"); // allocate the SPL fixed-array payload with size zero + emitter.instruction("b __rt_nbn_generic_alloc"); // continue with the generic property-object allocator + emitter.label("__rt_nbn_alloc_spl_dll"); + emitter.instruction("mov x0, x12"); // pass the matched concrete SPL list class id + emitter.instruction("bl __rt_spl_dll_new"); // allocate the runtime-managed SPL list object layout + emitter.instruction("b __rt_nbn_return_allocated"); // return the initialized runtime-managed object + emitter.label("__rt_nbn_alloc_spl_fixed"); + emitter.instruction("mov x0, x12"); // pass the matched SplFixedArray class id + emitter.instruction("mov x1, xzr"); // default dynamic size is zero before constructor dispatch + emitter.instruction("bl __rt_spl_fixed_new"); // allocate the runtime-managed fixed-array object layout + emitter.instruction("b __rt_nbn_return_allocated"); // return the initialized runtime-managed object +} + +/// Emits x86_64 class-id checks for runtime-managed builtin payload layouts. +fn emit_runtime_managed_match_x86_64(emitter: &mut Emitter) { + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_dll_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplDoublyLinkedList? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the SPL list payload for SplDoublyLinkedList + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_stack_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplStack? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the shared SPL list payload for SplStack + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_queue_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplQueue? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the shared SPL list payload for SplQueue + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_fixed_array_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplFixedArray? + emitter.instruction("je __rt_nbn_alloc_spl_fixed_x86"); // allocate the SPL fixed-array payload with size zero + emitter.instruction("jmp __rt_nbn_generic_alloc_x86"); // continue with the generic property-object allocator + emitter.label("__rt_nbn_alloc_spl_dll_x86"); + emitter.instruction("mov rdi, rcx"); // pass the matched concrete SPL list class id + emitter.instruction("call __rt_spl_dll_new"); // allocate the runtime-managed SPL list object layout + emitter.instruction("jmp __rt_nbn_return_allocated_x86"); // return the initialized runtime-managed object + emitter.label("__rt_nbn_alloc_spl_fixed_x86"); + emitter.instruction("mov rdi, rcx"); // pass the matched SplFixedArray class id + emitter.instruction("xor esi, esi"); // default dynamic size is zero before constructor dispatch + emitter.instruction("call __rt_spl_fixed_new"); // allocate the runtime-managed fixed-array object layout + emitter.instruction("jmp __rt_nbn_return_allocated_x86"); // return the initialized runtime-managed object +} diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 63ab6ad9c5..eb54a3cd0d 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -14,6 +14,8 @@ //! - The dynamic builtin dispatcher (descriptor invoker) emits per-builtin //! wrappers — including md5/sha1/hash — that reference the `elephc_crypto` //! staticlib, so its detection forces that crate to link. +//! - `eval()` keeps the dynamic bridge feature separate from scope-only helpers +//! so AOT fragments can shed bridge-only runtime/link dependencies incrementally. use std::collections::HashMap; @@ -33,6 +35,12 @@ pub struct RuntimeFeatures { /// True when codegen can emit the runtime callable dispatcher (descriptor /// invoker) that builds per-builtin wrappers referencing `elephc_crypto`. pub descriptor_invoker: bool, + /// True when codegen can call the optional eval interpreter bridge staticlib. + pub eval_bridge: bool, + /// True when codegen can use materialized eval-scope helper symbols without + /// the interpreter bridge. Scope-only helpers are emitted by the core runtime + /// when the dynamic eval bridge is not otherwise required. + pub eval_scope: bool, /// True when compiling a `--web` program. Selects the output-capture variant /// of `__rt_stdout_write`, which checks the `_elephc_web_capture` flag and may /// tail-call `elephc_web_write` (a symbol only linked into `--web` binaries). @@ -47,6 +55,8 @@ impl RuntimeFeatures { regex: false, phar_archive: false, descriptor_invoker: false, + eval_bridge: false, + eval_scope: false, web: false, } } @@ -58,6 +68,8 @@ impl RuntimeFeatures { regex: true, phar_archive: true, descriptor_invoker: true, + eval_bridge: true, + eval_scope: true, web: true, } } @@ -81,8 +93,8 @@ pub fn runtime_features_for_program_and_classes( pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec { let mut libs = Vec::new(); if features.regex { - libs.push("pcre2-posix".to_string()); - libs.push("pcre2-8".to_string()); + push_required_library(&mut libs, "pcre2-posix"); + push_required_library(&mut libs, "pcre2-8"); } if features.phar_archive { libs.push("elephc_phar".to_string()); @@ -94,9 +106,23 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec // reference `elephc_crypto_hash`; force the crate to link on all targets. libs.push("elephc_crypto".to_string()); } + if features.eval_bridge { + // Eval code can call preg_* from dynamically parsed source, so PCRE2 is + // required even when no static preg call appears in the AOT AST. + push_required_library(&mut libs, "pcre2-posix"); + push_required_library(&mut libs, "pcre2-8"); + push_required_library(&mut libs, "elephc_magician"); + } libs } +/// Appends a required link library once while preserving feature order. +fn push_required_library(libs: &mut Vec, library: &str) { + if !libs.iter().any(|existing| existing == library) { + libs.push(library.to_string()); + } +} + /// Builds the optional runtime feature set, using class metadata when codegen has it. fn runtime_features_for_program_and_classes_opt( program: &Program, @@ -170,7 +196,9 @@ fn emitted_classes_include_phar_archive_helpers( classes: &HashMap, ) -> bool { if program_has_dynamic_instanceof(program) { - return classes.keys().any(|name| is_phar_archive_helper_class_name(name)); + return classes + .keys() + .any(|name| is_phar_archive_helper_class_name(name)); } super::collect_emitted_class_names(program, classes) .iter() @@ -231,9 +259,9 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { } => { expr_has_regex_call(condition) || body_has_regex_call(then_body) - || elseif_clauses - .iter() - .any(|(condition, body)| expr_has_regex_call(condition) || body_has_regex_call(body)) + || elseif_clauses.iter().any(|(condition, body)| { + expr_has_regex_call(condition) || body_has_regex_call(body) + }) || else_body.as_deref().is_some_and(body_has_regex_call) } StmtKind::IfDef { @@ -241,8 +269,7 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { else_body, .. } => { - body_has_regex_call(then_body) - || else_body.as_deref().is_some_and(body_has_regex_call) + body_has_regex_call(then_body) || else_body.as_deref().is_some_and(body_has_regex_call) } StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { expr_has_regex_call(condition) || body_has_regex_call(body) @@ -278,9 +305,9 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { | StmtKind::FunctionDecl { body, .. } => body_has_regex_call(body), StmtKind::ClassDecl { methods, .. } | StmtKind::TraitDecl { methods, .. } - | StmtKind::InterfaceDecl { methods, .. } => { - methods.iter().any(|method| body_has_regex_call(&method.body)) - } + | StmtKind::InterfaceDecl { methods, .. } => methods + .iter() + .any(|method| body_has_regex_call(&method.body)), StmtKind::Try { try_body, catches, @@ -313,9 +340,9 @@ fn expr_has_regex_call(expr: &Expr) -> bool { match &expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } ExprKind::FunctionCall { name, args } => { is_regex_builtin_name(name.as_str()) || regex_callback_dispatch_call(name.as_str(), args) @@ -337,6 +364,7 @@ fn expr_has_regex_call(expr: &Expr) -> bool { | ExprKind::Not(expr) | ExprKind::BitNot(expr) | ExprKind::Throw(expr) + | ExprKind::Clone(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) | ExprKind::Spread(expr) @@ -418,6 +446,15 @@ fn expr_has_regex_call(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_regex_call(object) || args.iter().any(expr_has_regex_call) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_regex_call(object) + || expr_has_regex_call(method) + || args.iter().any(expr_has_regex_call) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_has_regex_call(object) } @@ -477,7 +514,10 @@ fn expr_is_regex_callback_string(expr: &Expr) -> bool { /// Returns true when a static receiver expression can contain a regex call. fn static_receiver_has_regex_call(receiver: &StaticReceiver) -> bool { match receiver { - StaticReceiver::Named(_) | StaticReceiver::Self_ | StaticReceiver::Static | StaticReceiver::Parent => false, + StaticReceiver::Named(_) + | StaticReceiver::Self_ + | StaticReceiver::Static + | StaticReceiver::Parent => false, } } @@ -544,7 +584,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { || elseif_clauses.iter().any(|(condition, body)| { expr_needs_descriptor_invoker(condition) || body_needs_descriptor_invoker(body) }) - || else_body.as_deref().is_some_and(body_needs_descriptor_invoker) + || else_body + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::IfDef { then_body, @@ -552,7 +594,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { .. } => { body_needs_descriptor_invoker(then_body) - || else_body.as_deref().is_some_and(body_needs_descriptor_invoker) + || else_body + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { expr_needs_descriptor_invoker(condition) || body_needs_descriptor_invoker(body) @@ -564,7 +608,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { body, } => { init.as_deref().is_some_and(stmt_needs_descriptor_invoker) - || condition.as_ref().is_some_and(expr_needs_descriptor_invoker) + || condition + .as_ref() + .is_some_and(expr_needs_descriptor_invoker) || update.as_deref().is_some_and(stmt_needs_descriptor_invoker) || body_needs_descriptor_invoker(body) } @@ -581,7 +627,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { patterns.iter().any(expr_needs_descriptor_invoker) || body_needs_descriptor_invoker(body) }) - || default.as_deref().is_some_and(body_needs_descriptor_invoker) + || default + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::Synthetic(body) | StmtKind::NamespaceBlock { body, .. } @@ -631,9 +679,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { match &expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } // A direct dynamic call on an arbitrary callee (e.g. `$callback(...)`) lowers // through runtime callable dispatch when the callee resolves to a string name. ExprKind::ExprCall { callee, args } => { @@ -648,6 +696,7 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::Not(expr) | ExprKind::BitNot(expr) | ExprKind::Throw(expr) + | ExprKind::Clone(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) | ExprKind::Spread(expr) @@ -675,11 +724,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { .is_some_and(expr_needs_descriptor_invoker) } ExprKind::ArrayLiteral(items) => items.iter().any(expr_needs_descriptor_invoker), - ExprKind::ArrayLiteralAssoc(items) => items - .iter() - .any(|(key, value)| { - expr_needs_descriptor_invoker(key) || expr_needs_descriptor_invoker(value) - }), + ExprKind::ArrayLiteralAssoc(items) => items.iter().any(|(key, value)| { + expr_needs_descriptor_invoker(key) || expr_needs_descriptor_invoker(value) + }), ExprKind::Match { subject, arms, @@ -690,7 +737,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { patterns.iter().any(expr_needs_descriptor_invoker) || expr_needs_descriptor_invoker(value) }) - || default.as_deref().is_some_and(expr_needs_descriptor_invoker) + || default + .as_deref() + .is_some_and(expr_needs_descriptor_invoker) } ExprKind::ArrayAccess { array, index } => { expr_needs_descriptor_invoker(array) || expr_needs_descriptor_invoker(index) @@ -712,7 +761,10 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_needs_descriptor_invoker), ExprKind::NewDynamicObject { class_name, args, .. - } => expr_needs_descriptor_invoker(class_name) || args.iter().any(expr_needs_descriptor_invoker), + } => { + expr_needs_descriptor_invoker(class_name) + || args.iter().any(expr_needs_descriptor_invoker) + } ExprKind::PropertyAccess { object, .. } | ExprKind::NullsafePropertyAccess { object, .. } => expr_needs_descriptor_invoker(object), ExprKind::DynamicPropertyAccess { object, property } @@ -723,6 +775,15 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_needs_descriptor_invoker(object) || args.iter().any(expr_needs_descriptor_invoker) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_needs_descriptor_invoker(object) + || expr_needs_descriptor_invoker(method) + || args.iter().any(expr_needs_descriptor_invoker) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_needs_descriptor_invoker(object) } @@ -778,7 +839,9 @@ fn expr_is_descriptor_invoker_trigger(expr: &Expr) -> bool { // string literals — as runtime dispatch, so this uses the broader predicate. ExprKind::NewObject { class_name, args } => { is_fiber_class_name(class_name.as_str()) - && args.first().is_some_and(fiber_callback_may_be_runtime_dispatch) + && args + .first() + .is_some_and(fiber_callback_may_be_runtime_dispatch) } _ => false, } @@ -870,7 +933,10 @@ mod tests { /// Verifies ordinary programs do not require the optional regex runtime helpers. #[test] fn test_runtime_features_omit_regex_for_plain_program() { - assert_eq!(features_for(") -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -138,6 +139,7 @@ pub(super) fn rewrite_expr(expr: Expr, defines: &HashSet) -> Expr { }) .collect(), variadic, + variadic_by_ref, variadic_type, return_type, body: apply_stmts(body, defines), @@ -215,6 +217,18 @@ pub(super) fn rewrite_expr(expr: Expr, defines: &HashSet) -> Expr { .map(|arg| rewrite_expr(arg, defines)) .collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(rewrite_expr(*object, defines)), + method: Box::new(rewrite_expr(*method, defines)), + args: args + .into_iter() + .map(|arg| rewrite_expr(arg, defines)) + .collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index b62690f491..e29cb4c3fb 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -235,7 +235,9 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -248,7 +250,9 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { (name, type_ann, default.map(|expr| rewrite_expr(expr, defines)), is_ref) }) .collect(), + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: apply_stmts(body, defines), @@ -315,12 +319,14 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods, constants, cases: cases diff --git a/src/eval_aot.rs b/src/eval_aot.rs new file mode 100644 index 0000000000..7dab17efe2 --- /dev/null +++ b/src/eval_aot.rs @@ -0,0 +1,5276 @@ +//! Purpose: +//! Shared compile-time analysis helpers for literal `eval` AOT eligibility. +//! Keeps parser/classifier decisions out of target assembly lowering where possible. +//! +//! Called from: +//! - `crate::ir_lower::program` while deriving runtime feature requirements. +//! - `crate::codegen_ir::lower_inst::builtins::eval` while lowering AOT fragments. +//! +//! Key details: +//! - Only exposes semantics that are fully target-independent. +//! - Plans keep fallback and scope metadata alongside any fully static lowering. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; + +use crate::names::{php_symbol_key, Name}; +use crate::parser::ast::{ + BinOp, CallableTarget, CastType, Expr, ExprKind, Program, StaticReceiver, Stmt, StmtKind, +}; +use crate::span::Span; +use crate::types::call_args::{has_named_args, plan_call_args}; +use crate::types::{builtin_call_sig, is_php_integer_array_key, FunctionSig, PhpType}; + +const EIR_AOT_FUNCTION_PREFIX: &str = "__eir@evalaot"; +const MAX_STATIC_STRING_FOLD_BYTES: usize = 1_048_576; + +/// Static call support available while classifying eval fragments for EIR AOT. +trait EirStaticCallSupport { + /// Returns true when a function call can be lowered inside an EIR AOT fragment. + fn function_supported(&self, name: &str, args: &[Expr]) -> bool; + + /// Returns true when a static method call can be lowered inside an EIR AOT fragment. + fn static_method_supported( + &self, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], + ) -> bool; +} + +/// Pair of caller-provided support predicates for eval EIR AOT static calls. +struct EirStaticCallPredicates<'a, F, M> { + function: &'a F, + static_method: &'a M, +} + +impl EirStaticCallSupport for EirStaticCallPredicates<'_, F, M> +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + /// Delegates function-call eligibility to the caller-provided predicate. + fn function_supported(&self, name: &str, args: &[Expr]) -> bool { + (self.function)(name, args) + } + + /// Delegates static-method eligibility to the caller-provided predicate. + fn static_method_supported( + &self, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], + ) -> bool { + (self.static_method)(receiver, method, args) + } +} + +/// Compile-time plan for one literal eval fragment. +pub(crate) struct EvalAotPlan { + function_name: Option, + eir_program: Option, + scope_read_function_name: Option, + scope_read_eir_program: Option, + reads: BTreeSet, + array_read_constraints: BTreeSet, + assoc_array_read_constraints: BTreeSet, + float_predicate_read_constraints: BTreeSet, + writes: BTreeSet, + scope_direct_writes: BTreeSet, + scope_flush_writes: BTreeSet, + creates_unknown_vars: bool, + needs_eval_context: bool, + needs_global_scope: bool, + fallback_reason: Option, + native_only_scalar: bool, +} + +impl EvalAotPlan { + /// Returns true when this fragment is fully native and cannot call the eval bridge. + pub(crate) fn is_fully_static_no_bridge(&self) -> bool { + !self.needs_eval_context + && self.fallback_reason.is_none() + && self.scope_read_eir_program.is_none() + && (self.eir_program.is_some() || self.native_only_scalar) + } + + /// Returns true when the scope-read EIR body can receive reads as direct parameters. + pub(crate) fn uses_scope_read_params(&self) -> bool { + self.scope_read_eir_program.is_some() + && !self.reads.is_empty() + && self.writes.is_empty() + && self.scope_direct_writes.is_empty() + && self.scope_flush_writes.is_empty() + } + + /// Returns true when the fragment still requires the magician eval bridge. + pub(crate) fn requires_runtime_eval_bridge(&self) -> bool { + if self.is_fully_static_no_bridge() { + return false; + } + if self.scope_read_eir_program.is_some() { + return false; + } + self.needs_eval_context + || self.needs_global_scope + || self.creates_unknown_vars + || !self.reads.is_empty() + || !self.writes.is_empty() + || self.fallback_reason.is_some() + } + + /// Returns true when the fragment needs only core eval-scope runtime state. + pub(crate) fn requires_runtime_eval_scope(&self) -> bool { + self.scope_read_eir_program.is_some() && !self.uses_scope_read_params() + } + + /// Takes the deterministic internal EIR function name, when one exists. + pub(crate) fn take_function_name(&mut self) -> Option { + self.function_name.take() + } + + /// Takes the parsed and folded EIR AOT body, when one exists. + pub(crate) fn take_eir_program(&mut self) -> Option { + self.eir_program.take() + } + + /// Takes the deterministic EIR function name for a scope-read AOT body. + pub(crate) fn take_scope_read_function_name(&mut self) -> Option { + self.scope_read_function_name.take() + } + + /// Takes the parsed and folded body for a scope-read EIR AOT function. + pub(crate) fn take_scope_read_eir_program(&mut self) -> Option { + self.scope_read_eir_program.take() + } + + /// Returns the statically known eval-scope reads for this fragment. + pub(crate) fn reads(&self) -> &BTreeSet { + &self.reads + } + + /// Returns scope reads that must be caller-side arrays for direct-param AOT. + pub(crate) fn array_read_constraints(&self) -> &BTreeSet { + &self.array_read_constraints + } + + /// Returns scope reads that must be caller-side associative arrays. + pub(crate) fn assoc_array_read_constraints(&self) -> &BTreeSet { + &self.assoc_array_read_constraints + } + + /// Returns scope reads that must be caller-side int/float values. + pub(crate) fn float_predicate_read_constraints(&self) -> &BTreeSet { + &self.float_predicate_read_constraints + } + + /// Returns the statically known eval-scope writes for this fragment. + pub(crate) fn writes(&self) -> &BTreeSet { + &self.writes + } + + /// Returns eval-scope writes that are stored immediately during EIR lowering. + pub(crate) fn direct_writes(&self) -> &BTreeSet { + &self.scope_direct_writes + } + + /// Returns local writes that are flushed to eval scope by the EIR finalizer. + pub(crate) fn flush_writes(&self) -> &BTreeSet { + &self.scope_flush_writes + } + + /// Returns the conservative bridge fallback reason, when this plan has one. + pub(crate) fn fallback_reason(&self) -> Option { + self.fallback_reason + } +} + +/// Conservative reason a literal eval fragment cannot be fully static today. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum EvalAotFallbackReason { + ParseError, + IncludeOrRequire, + Declaration, + GlobalOrStatic, + ReferenceOrByRef, + DynamicCall, + DynamicClassOrMember, + ObjectOrMemberAccess, + ArrayOrIterable, + TryOrThrow, + UnsupportedControlFlow, + UnsupportedScope, + UnsupportedStaticCall, + UnsupportedConstruct, +} + +/// Static scalar category for a direct eval local-store candidate. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DirectLocalStoreScalarKind { + Null, + Bool, + Int, + Float, + String, +} + +impl EvalAotFallbackReason { + /// Returns a stable assembly-marker description for this fallback reason. + pub(crate) fn description(self) -> &'static str { + match self { + EvalAotFallbackReason::ParseError => "parse error", + EvalAotFallbackReason::IncludeOrRequire => "include/require needs bridge semantics", + EvalAotFallbackReason::Declaration => "runtime declarations need bridge semantics", + EvalAotFallbackReason::GlobalOrStatic => "global/static scope needs bridge semantics", + EvalAotFallbackReason::ReferenceOrByRef => "references/by-ref need bridge semantics", + EvalAotFallbackReason::DynamicCall => "dynamic call needs bridge semantics", + EvalAotFallbackReason::DynamicClassOrMember => { + "dynamic class/member access needs bridge semantics" + } + EvalAotFallbackReason::ObjectOrMemberAccess => { + "object/member access needs bridge semantics" + } + EvalAotFallbackReason::ArrayOrIterable => { + "array/iterable semantics need bridge fallback" + } + EvalAotFallbackReason::TryOrThrow => "try/throw needs bridge semantics", + EvalAotFallbackReason::UnsupportedControlFlow => "unsupported control flow", + EvalAotFallbackReason::UnsupportedScope => "unsupported scope synchronization", + EvalAotFallbackReason::UnsupportedStaticCall => "unsupported static call", + EvalAotFallbackReason::UnsupportedConstruct => "unsupported construct", + } + } +} + +/// Parses a literal eval fragment as a PHP statement fragment. +pub(crate) fn parse_literal_fragment(fragment: &str) -> Option { + let source = format!(", +) -> Option { + let program = parse_literal_fragment(fragment)?; + Some(match source_path { + Some(source_path) => crate::magic_constants::substitute_file_and_scope_constants( + program, + Path::new(source_path), + ), + None => program, + }) +} + +/// Returns the ordered static writes in a direct local-store eval candidate. +pub(crate) fn literal_fragment_direct_local_store_writes( + fragment: &str, +) -> Option> { + let program = parse_literal_fragment(fragment)?; + direct_local_store_candidate_writes(&program) +} + +/// Returns scalar self-read writes that can avoid eval scope with direct caller locals. +pub(crate) fn literal_fragment_direct_local_read_write_writes( + fragment: &str, +) -> Option> { + let program = parse_literal_fragment(fragment)?; + direct_local_read_write_candidate_writes(&program) +} + +/// Checks the narrow read/write shape `target = f(target, literals)`. +fn direct_local_read_write_candidate_writes( + program: &[Stmt], +) -> Option> { + let mut writes = BTreeMap::new(); + let mut saw_store = false; + let mut terminated = false; + for stmt in program { + if terminated { + return None; + } + match &stmt.kind { + StmtKind::Assign { name, value } => { + let kind = direct_local_read_write_expr(value, name)?; + writes.insert(name.clone(), kind); + saw_store = true; + } + StmtKind::Return(Some(expr)) => { + direct_local_store_static_scalar_expr(expr)?; + terminated = true; + } + StmtKind::Return(None) => { + terminated = true; + } + StmtKind::ExprStmt(expr) => { + direct_local_store_static_scalar_expr(expr)?; + } + _ => return None, + } + } + saw_store.then_some(writes) +} + +/// Checks scalar expressions whose only variable read is the target being assigned. +fn direct_local_read_write_expr( + expr: &Expr, + target_name: &str, +) -> Option { + match &expr.kind { + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::Variable(name) if name == target_name => Some(DirectLocalStoreScalarKind::Int), + ExprKind::Negate(inner) => (direct_local_read_write_expr(inner, target_name)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int), + ExprKind::BinaryOp { left, op, right } + if matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul) => + { + (direct_local_read_write_expr(left, target_name)? == DirectLocalStoreScalarKind::Int + && direct_local_read_write_expr(right, target_name)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::BinaryOp { left, op, right } if matches!(op, BinOp::Div | BinOp::Mod) => { + let left_kind = direct_local_read_write_expr(left, target_name)?; + let right_kind = direct_local_read_write_expr(right, target_name)?; + (local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind)) + .then_some(if *op == BinOp::Div { + DirectLocalStoreScalarKind::Float + } else { + DirectLocalStoreScalarKind::Int + }) + } + _ => None, + } +} + +/// Returns int/bool local writes for the legacy local-scalar AOT subset. +pub(crate) fn literal_fragment_local_scalar_writes_with_static_calls( + fragment: &str, + static_call_supported: F, +) -> Option> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let program = parse_literal_fragment(fragment)?; + let mut writes = BTreeMap::new(); + let mut assigned = BTreeMap::new(); + local_scalar_block_writes( + &program, + &mut writes, + &mut assigned, + 0, + true, + &static_call_supported, + )?; + (!writes.is_empty()).then_some(writes) +} + +/// Checks a statement block against the int/bool local-scalar AOT subset. +fn local_scalar_block_writes( + program: &[Stmt], + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let mut terminated = false; + for stmt in program { + if terminated { + break; + } + terminated = local_scalar_stmt_writes( + stmt, + writes, + assigned, + loop_depth, + allow_type_changes, + static_call_supported, + )?; + } + Some(()) +} + +/// Checks one statement against the int/bool local-scalar AOT subset. +fn local_scalar_stmt_writes( + stmt: &Stmt, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Echo(expr) => { + local_scalar_echo_expr(expr, assigned, static_call_supported)?; + Some(false) + } + StmtKind::Assign { name, value } => { + let kind = local_scalar_value_expr(value, assigned, static_call_supported)?; + local_scalar_record_write(writes, assigned, name, kind, allow_type_changes)?; + Some(false) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let mut then_assigned = assigned.clone(); + local_scalar_block_writes( + then_body, + writes, + &mut then_assigned, + loop_depth, + false, + static_call_supported, + )?; + for (elseif_condition, elseif_body) in elseif_clauses { + local_scalar_condition_expr(elseif_condition, assigned, static_call_supported)?; + let mut elseif_assigned = assigned.clone(); + local_scalar_block_writes( + elseif_body, + writes, + &mut elseif_assigned, + loop_depth, + false, + static_call_supported, + )?; + } + if let Some(else_body) = else_body { + let mut else_assigned = assigned.clone(); + local_scalar_block_writes( + else_body, + writes, + &mut else_assigned, + loop_depth, + false, + static_call_supported, + )?; + } + Some(false) + } + StmtKind::While { condition, body } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + Some(false) + } + StmtKind::DoWhile { body, condition } => { + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + local_scalar_condition_expr(condition, &body_assigned, static_call_supported)?; + Some(false) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init.as_deref() { + local_scalar_for_clause_writes( + init, + writes, + assigned, + allow_type_changes, + static_call_supported, + )?; + } + if let Some(condition) = condition { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + } + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + if let Some(update) = update.as_deref() { + local_scalar_for_clause_writes( + update, + writes, + &mut body_assigned, + false, + static_call_supported, + )?; + } + Some(false) + } + StmtKind::Switch { + subject, + cases, + default, + } => { + local_scalar_switch_stmt_writes( + subject, + cases, + default.as_deref(), + writes, + assigned, + loop_depth, + static_call_supported, + )?; + Some(false) + } + StmtKind::Break(level) if *level > 0 && *level <= loop_depth => Some(true), + StmtKind::Continue(level) if *level > 0 && *level <= loop_depth => Some(true), + StmtKind::Return(Some(expr)) => { + local_scalar_value_expr(expr, assigned, static_call_supported)?; + Some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => local_scalar_expr_stmt_writes( + expr, + writes, + assigned, + allow_type_changes, + static_call_supported, + ), + _ => None, + } +} + +/// Checks a local-scalar switch while preserving conservative assignment facts. +fn local_scalar_switch_stmt_writes( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + writes: &mut BTreeMap, + assigned: &BTreeMap, + loop_depth: usize, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let subject_kind = local_scalar_condition_expr(subject, assigned, static_call_supported)?; + for (conditions, body) in cases { + for condition in conditions { + (local_scalar_condition_expr(condition, assigned, static_call_supported)? + == subject_kind) + .then_some(())?; + } + let mut case_assigned = assigned.clone(); + local_scalar_switch_block_writes( + body, + writes, + &mut case_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + } + if let Some(default) = default { + let mut default_assigned = assigned.clone(); + local_scalar_switch_block_writes( + default, + writes, + &mut default_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + } + Some(()) +} + +/// Checks a switch case/default body against the local-scalar subset. +fn local_scalar_switch_block_writes( + statements: &[Stmt], + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + local_scalar_block_writes( + statements, + writes, + assigned, + loop_depth, + allow_type_changes, + static_call_supported, + ) +} + +/// Checks an inline `for` init/update statement against the local-scalar subset. +fn local_scalar_for_clause_writes( + stmt: &Stmt, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Assign { .. } | StmtKind::ExprStmt(_) => (!local_scalar_stmt_writes( + stmt, + writes, + assigned, + 0, + allow_type_changes, + static_call_supported, + )?) + .then_some(()), + _ => None, + } +} + +/// Checks one expression statement accepted by the local-scalar AOT subset. +fn local_scalar_expr_stmt_writes( + expr: &Expr, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::Print(value) => { + local_scalar_echo_expr(value, assigned, static_call_supported)?; + Some(false) + } + ExprKind::Assignment { target, value, .. } => { + let ExprKind::Variable(name) = &target.kind else { + return None; + }; + let kind = local_scalar_value_expr(value, assigned, static_call_supported)?; + local_scalar_record_write(writes, assigned, name, kind, allow_type_changes)?; + Some(false) + } + ExprKind::PreIncrement(name) | ExprKind::PostIncrement(name) => { + local_scalar_inc_dec_write(writes, assigned, name)?; + Some(false) + } + ExprKind::PreDecrement(name) | ExprKind::PostDecrement(name) => { + local_scalar_inc_dec_write(writes, assigned, name)?; + Some(false) + } + _ => { + local_scalar_value_expr(expr, assigned, static_call_supported)?; + Some(false) + } + } +} + +/// Records an increment/decrement write for an assigned integer local. +fn local_scalar_inc_dec_write( + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + name: &str, +) -> Option<()> { + (assigned.get(name) == Some(&DirectLocalStoreScalarKind::Int)).then_some(())?; + local_scalar_record_write( + writes, + assigned, + name, + DirectLocalStoreScalarKind::Int, + true, + ) +} + +/// Records one local-scalar write, allowing type changes only in linear statement flow. +fn local_scalar_record_write( + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + name: &str, + kind: DirectLocalStoreScalarKind, + allow_type_change: bool, +) -> Option<()> { + if crate::superglobals::is_superglobal(name) { + return None; + } + if let Some(existing) = assigned.get(name) { + (*existing == kind || allow_type_change).then_some(())?; + } + assigned.insert(name.to_string(), kind); + writes.insert(name.to_string(), kind); + Some(()) +} + +/// Checks a control-flow condition for the local-scalar AOT subset. +fn local_scalar_condition_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + let kind = local_scalar_value_expr(expr, assigned, static_call_supported)?; + matches!( + kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) + .then_some(kind) +} + +/// Checks an expression that can be emitted by local-scalar echo. +fn local_scalar_echo_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + local_scalar_echo_expr(left, assigned, static_call_supported)?; + local_scalar_echo_expr(right, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::String) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let then_kind = local_scalar_echo_expr(then_expr, assigned, static_call_supported)?; + let else_kind = local_scalar_echo_expr(else_expr, assigned, static_call_supported)?; + (then_kind == else_kind).then_some(then_kind) + } + _ => local_scalar_value_expr(expr, assigned, static_call_supported), + } +} + +/// Checks an int/bool expression for the local-scalar AOT subset. +fn local_scalar_value_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::Null => Some(DirectLocalStoreScalarKind::Null), + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::BoolLiteral(_) => Some(DirectLocalStoreScalarKind::Bool), + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::Variable(name) => assigned.get(name).copied(), + ExprKind::Negate(inner) => { + (local_scalar_value_expr(inner, assigned, static_call_supported)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::BitNot(inner) => { + (local_scalar_value_expr(inner, assigned, static_call_supported)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::Not(inner) => { + local_scalar_condition_expr(inner, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::Bool) + } + ExprKind::ErrorSuppress(inner) => { + local_scalar_value_expr(inner, assigned, static_call_supported) + } + ExprKind::Print(inner) => { + local_scalar_echo_expr(inner, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::Int) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let then_kind = local_scalar_value_expr(then_expr, assigned, static_call_supported)?; + let else_kind = local_scalar_value_expr(else_expr, assigned, static_call_supported)?; + (then_kind == else_kind).then_some(then_kind) + } + ExprKind::BinaryOp { left, op, right } => { + local_scalar_binary_expr(left, op.clone(), right, assigned, static_call_supported) + } + ExprKind::FunctionCall { name, args } => { + let function_name = name.to_string(); + if let Some(kind) = local_scalar_construct_call_expr(&function_name, args, assigned) { + return Some(kind); + } + (fold_static_builtin_int_call(function_name.trim_start_matches('\\'), args).is_some() + || static_call_supported(&function_name, args)) + .then_some(DirectLocalStoreScalarKind::Int) + } + _ => None, + } +} + +/// Checks language-construct calls that local-scalar AOT can lower without scope reads. +fn local_scalar_construct_call_expr( + name: &str, + args: &[Expr], + assigned: &BTreeMap, +) -> Option { + if has_named_args(args) + || args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return None; + } + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "isset" => (!args.is_empty() + && args.iter().all( + |arg| matches!(&arg.kind, ExprKind::Variable(name) if assigned.contains_key(name)), + )) + .then_some(DirectLocalStoreScalarKind::Bool), + "empty" if args.len() == 1 => match &args[0].kind { + ExprKind::Variable(name) if assigned.contains_key(name) => { + Some(DirectLocalStoreScalarKind::Bool) + } + _ => { + local_scalar_value_expr(args.first()?, assigned, &|_, _| false)?; + Some(DirectLocalStoreScalarKind::Bool) + } + }, + _ => None, + } +} + +/// Checks a binary expression for the local-scalar AOT subset. +fn local_scalar_binary_expr( + left: &Expr, + op: BinOp, + right: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + let left_kind = local_scalar_value_expr(left, assigned, static_call_supported)?; + let right_kind = local_scalar_value_expr(right, assigned, static_call_supported)?; + match op { + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight + if left_kind == DirectLocalStoreScalarKind::Int + && right_kind == DirectLocalStoreScalarKind::Int => + { + Some(DirectLocalStoreScalarKind::Int) + } + BinOp::Div + if local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind) => + { + Some(DirectLocalStoreScalarKind::Float) + } + BinOp::Mod + if local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind) => + { + Some(DirectLocalStoreScalarKind::Int) + } + BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq + if left_kind == DirectLocalStoreScalarKind::Int + && right_kind == DirectLocalStoreScalarKind::Int => + { + Some(DirectLocalStoreScalarKind::Bool) + } + BinOp::Eq | BinOp::NotEq + if left_kind == right_kind + && matches!( + left_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) => + { + Some(DirectLocalStoreScalarKind::Bool) + } + BinOp::And | BinOp::Or + if matches!( + left_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) && matches!( + right_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) => + { + Some(DirectLocalStoreScalarKind::Bool) + } + _ => None, + } +} + +/// Returns true when the scalar kind participates in numeric-only local AOT operations. +fn local_scalar_numeric_kind(kind: DirectLocalStoreScalarKind) -> bool { + matches!( + kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Float + ) +} + +/// Checks the narrow write-only shape that codegen can store directly into caller locals. +fn direct_local_store_candidate_writes( + program: &[Stmt], +) -> Option> { + let mut writes = Vec::new(); + let mut saw_store = false; + let mut terminated = false; + for stmt in program { + if terminated { + return None; + } + match &stmt.kind { + StmtKind::Assign { name, value } => { + let kind = direct_local_store_static_scalar_expr(value)?; + writes.push((name.clone(), kind)); + saw_store = true; + } + StmtKind::Return(Some(expr)) => { + direct_local_store_static_scalar_expr(expr)?; + terminated = true; + } + StmtKind::Return(None) => { + terminated = true; + } + StmtKind::ExprStmt(expr) => { + direct_local_store_static_scalar_expr(expr)?; + } + _ => return None, + } + } + saw_store.then_some(writes) +} + +/// Returns the scalar kind when an expression can be materialized by direct local-store codegen. +fn direct_local_store_static_scalar_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(DirectLocalStoreScalarKind::Null), + ExprKind::BoolLiteral(_) => Some(DirectLocalStoreScalarKind::Bool), + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::Negate(inner) => direct_local_store_static_numeric_expr(inner), + ExprKind::Not(inner) => { + direct_local_store_static_scalar_expr(inner).map(|_| DirectLocalStoreScalarKind::Bool) + } + ExprKind::BinaryOp { left, op, right } => match op { + BinOp::Concat => { + direct_local_store_static_concat_operand(left)?; + direct_local_store_static_concat_operand(right)?; + Some(DirectLocalStoreScalarKind::String) + } + _ => None, + }, + _ => None, + } +} + +/// Returns the scalar kind for a numeric expression accepted by unary minus. +fn direct_local_store_static_numeric_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + _ => None, + } +} + +/// Returns true when an expression is safe for compile-time PHP string concatenation. +fn direct_local_store_static_concat_operand(expr: &Expr) -> Option<()> { + match direct_local_store_static_scalar_expr(expr)? { + DirectLocalStoreScalarKind::Null + | DirectLocalStoreScalarKind::Bool + | DirectLocalStoreScalarKind::Int + | DirectLocalStoreScalarKind::String => Some(()), + DirectLocalStoreScalarKind::Float => None, + } +} + +/// Returns a deterministic internal function name for a literal eval fragment. +pub(crate) fn eir_function_name(fragment: &str) -> String { + format!( + "{}_{:016x}", + EIR_AOT_FUNCTION_PREFIX, + stable_fragment_hash(fragment) + ) +} + +/// Returns a deterministic internal function name for a scope-read eval fragment. +pub(crate) fn eir_scope_read_function_name(fragment: &str) -> String { + format!( + "{}_scope_{:016x}", + EIR_AOT_FUNCTION_PREFIX, + stable_fragment_hash(fragment) + ) +} + +/// Builds the shared literal eval AOT plan for scan, lowering, and codegen decisions. +pub(crate) fn plan_literal_fragment_with_static_calls( + fragment: &str, + static_call_supported: F, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, +{ + plan_literal_fragment_with_static_and_method_calls( + fragment, + static_call_supported, + |_, _, _| false, + ) +} + +/// Builds the shared literal eval AOT plan with function and static-method support. +pub(crate) fn plan_literal_fragment_with_static_and_method_calls( + fragment: &str, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let Some(program) = parse_literal_fragment(fragment) else { + return parse_error_plan(); + }; + plan_parsed_literal_fragment_with_static_and_method_calls( + fragment, + program, + static_call_supported, + static_method_supported, + ) +} + +/// Builds the literal eval AOT plan with call-site source and static-method metadata. +pub(crate) fn plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment: &str, + source_path: Option<&str>, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let Some(program) = parse_literal_fragment_with_source_path(fragment, source_path) else { + return parse_error_plan(); + }; + plan_parsed_literal_fragment_with_static_and_method_calls( + fragment, + program, + static_call_supported, + static_method_supported, + ) +} + +/// Returns a conservative plan for fragments that cannot be parsed statically. +fn parse_error_plan() -> EvalAotPlan { + EvalAotPlan { + function_name: None, + eir_program: None, + scope_read_function_name: None, + scope_read_eir_program: None, + reads: BTreeSet::new(), + array_read_constraints: BTreeSet::new(), + assoc_array_read_constraints: BTreeSet::new(), + float_predicate_read_constraints: BTreeSet::new(), + writes: BTreeSet::new(), + scope_direct_writes: BTreeSet::new(), + scope_flush_writes: BTreeSet::new(), + creates_unknown_vars: true, + needs_eval_context: true, + needs_global_scope: true, + fallback_reason: Some(EvalAotFallbackReason::ParseError), + native_only_scalar: false, + } +} + +/// Builds the shared literal eval AOT plan from an already parsed fragment program. +fn plan_parsed_literal_fragment_with_static_and_method_calls( + fragment: &str, + program: Program, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let mut scope_access = collect_scope_accesses(&program); + let fragment_scope_reads = scope_access.reads.clone(); + scope_access.reads = collect_scope_reads_before_writes(&program); + let folded_program = fold_static_builtin_calls_in_program(program.clone()); + let support = EirStaticCallPredicates { + function: &static_call_supported, + static_method: &static_method_supported, + }; + let eir_program = + program_is_eir_function_safe(&folded_program, &support).then_some(folded_program.clone()); + let scope_names = scope_access + .reads + .union(&scope_access.writes) + .cloned() + .collect::>(); + let scope_write_only_linear = scope_access.reads.is_empty() + && fragment_scope_reads.is_empty() + && !scope_access.writes.is_empty() + && program_is_eir_scope_write_linear_function_safe(&folded_program, &support, &scope_names); + let scope_eir_safe = eir_program.is_none() + && !scope_names.is_empty() + && !scope_access.creates_unknown_vars + && program_is_eir_scope_read_function_safe(&folded_program, &support, &scope_names); + let scope_flush_local = + scope_eir_safe && scope_access.reads.is_empty() && !scope_access.writes.is_empty(); + let scope_direct = scope_eir_safe + && !scope_flush_local + && (!scope_access.reads.is_empty() || scope_write_only_linear); + let scope_direct_writes = if scope_direct { + scope_access.writes.clone() + } else { + BTreeSet::new() + }; + let scope_flush_writes = if scope_flush_local { + scope_access.writes.clone() + } else { + BTreeSet::new() + }; + let array_read_constraint_sets = + collect_array_scope_read_constraint_sets(&folded_program, &scope_access.reads); + let array_read_constraints = array_read_constraint_sets.array_like; + let assoc_array_read_constraints = array_read_constraint_sets.assoc; + let float_predicate_read_constraints = + collect_float_predicate_scope_read_constraints(&folded_program, &scope_access.reads); + let scope_read_eir_program = (scope_direct || scope_flush_local).then_some(folded_program); + let native_only_scalar = program_is_native_only_scalar(&program, &static_call_supported); + let is_fully_static_no_bridge = eir_program.is_some() || native_only_scalar; + let has_scope_read_eir = scope_read_eir_program.is_some(); + let needs_global_scope = + !is_fully_static_no_bridge && !has_scope_read_eir && scope_access.has_scope_access(); + EvalAotPlan { + function_name: eir_program.as_ref().map(|_| eir_function_name(fragment)), + eir_program, + scope_read_function_name: scope_read_eir_program + .as_ref() + .map(|_| eir_scope_read_function_name(fragment)), + scope_read_eir_program, + reads: scope_access.reads, + array_read_constraints, + assoc_array_read_constraints, + float_predicate_read_constraints, + writes: scope_access.writes, + scope_direct_writes, + scope_flush_writes, + creates_unknown_vars: scope_access.creates_unknown_vars, + needs_eval_context: !is_fully_static_no_bridge && !has_scope_read_eir, + needs_global_scope, + fallback_reason: (!is_fully_static_no_bridge && !has_scope_read_eir) + .then(|| classify_fallback_reason(&program)), + native_only_scalar, + } +} + +/// Classifies the first visible reason this fragment cannot avoid the bridge. +fn classify_fallback_reason(program: &[Stmt]) -> EvalAotFallbackReason { + program + .iter() + .find_map(stmt_fallback_reason) + .unwrap_or(EvalAotFallbackReason::UnsupportedScope) +} + +/// Classifies one statement for a human-readable eval AOT fallback marker. +fn stmt_fallback_reason(stmt: &Stmt) -> Option { + match &stmt.kind { + StmtKind::Include { .. } | StmtKind::IncludeOnceMark { .. } => { + Some(EvalAotFallbackReason::IncludeOrRequire) + } + StmtKind::IncludeOnceGuard { body, .. } + | StmtKind::Synthetic(body) + | StmtKind::NamespaceBlock { body, .. } => body.iter().find_map(stmt_fallback_reason), + StmtKind::FunctionDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::FunctionVariantMark { .. } + | StmtKind::ConstDecl { .. } + | StmtKind::ClassDecl { .. } + | StmtKind::EnumDecl { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::InterfaceDecl { .. } + | StmtKind::TraitDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => Some(EvalAotFallbackReason::Declaration), + StmtKind::Global { .. } | StmtKind::StaticVar { .. } => { + Some(EvalAotFallbackReason::GlobalOrStatic) + } + StmtKind::RefAssign { .. } => Some(EvalAotFallbackReason::ReferenceOrByRef), + StmtKind::Foreach { + array, + value_by_ref, + body, + .. + } => { + if *value_by_ref { + return Some(EvalAotFallbackReason::ReferenceOrByRef); + } + expr_fallback_reason(array) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + .or(Some(EvalAotFallbackReason::ArrayOrIterable)) + } + StmtKind::Try { .. } | StmtKind::Throw(_) => Some(EvalAotFallbackReason::TryOrThrow), + StmtKind::ArrayAssign { .. } + | StmtKind::NestedArrayAssign { .. } + | StmtKind::ArrayPush { .. } + | StmtKind::ListUnpack { .. } => Some(EvalAotFallbackReason::ArrayOrIterable), + StmtKind::PropertyAssign { .. } + | StmtKind::StaticPropertyAssign { .. } + | StmtKind::StaticPropertyArrayPush { .. } + | StmtKind::StaticPropertyArrayAssign { .. } + | StmtKind::PropertyArrayPush { .. } + | StmtKind::PropertyArrayAssign { .. } => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + expr_fallback_reason(expr) + } + StmtKind::Assign { value, .. } | StmtKind::TypedAssign { value, .. } => { + expr_fallback_reason(value) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => expr_fallback_reason(condition) + .or_else(|| then_body.iter().find_map(stmt_fallback_reason)) + .or_else(|| { + elseif_clauses.iter().find_map(|(condition, body)| { + expr_fallback_reason(condition) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + }) + }) + .or_else(|| { + else_body + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }), + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + expr_fallback_reason(condition).or_else(|| body.iter().find_map(stmt_fallback_reason)) + } + StmtKind::For { + init, + condition, + update, + body, + } => init + .as_deref() + .and_then(stmt_fallback_reason) + .or_else(|| condition.as_ref().and_then(expr_fallback_reason)) + .or_else(|| update.as_deref().and_then(stmt_fallback_reason)) + .or_else(|| body.iter().find_map(stmt_fallback_reason)), + StmtKind::Switch { + subject, + cases, + default, + } => expr_fallback_reason(subject) + .or_else(|| { + cases.iter().find_map(|(conditions, body)| { + conditions + .iter() + .find_map(expr_fallback_reason) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + }) + }) + .or_else(|| { + default + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }) + .or(Some(EvalAotFallbackReason::UnsupportedControlFlow)), + StmtKind::Break(_) | StmtKind::Continue(_) => { + Some(EvalAotFallbackReason::UnsupportedControlFlow) + } + StmtKind::Return(None) | StmtKind::NamespaceDecl { .. } | StmtKind::UseDecl { .. } => None, + StmtKind::IfDef { + then_body, + else_body, + .. + } => then_body.iter().find_map(stmt_fallback_reason).or_else(|| { + else_body + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }), + } +} + +/// Classifies one expression for a human-readable eval AOT fallback marker. +fn expr_fallback_reason(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::Variable(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => None, + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Clone(inner) + | ExprKind::YieldFrom(inner) => expr_fallback_reason(inner), + ExprKind::Throw(_) => Some(EvalAotFallbackReason::TryOrThrow), + ExprKind::BinaryOp { left, right, .. } + | ExprKind::NullCoalesce { + value: left, + default: right, + } + | ExprKind::ShortTernary { + value: left, + default: right, + } + | ExprKind::ArrayAccess { + array: left, + index: right, + } => expr_fallback_reason(left) + .or_else(|| expr_fallback_reason(right)) + .or(Some(EvalAotFallbackReason::ArrayOrIterable)), + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => expr_fallback_reason(condition) + .or_else(|| expr_fallback_reason(then_expr)) + .or_else(|| expr_fallback_reason(else_expr)), + ExprKind::Cast { target, expr } => { + if matches!(target, CastType::Array) { + return Some(EvalAotFallbackReason::ArrayOrIterable); + } + expr_fallback_reason(expr) + } + ExprKind::Match { + subject, + arms, + default, + } => expr_fallback_reason(subject) + .or_else(|| { + arms.iter().find_map(|(conditions, result)| { + conditions + .iter() + .find_map(expr_fallback_reason) + .or_else(|| expr_fallback_reason(result)) + }) + }) + .or_else(|| default.as_deref().and_then(expr_fallback_reason)), + ExprKind::FunctionCall { args, .. } => args + .iter() + .find_map(expr_fallback_reason) + .or(Some(EvalAotFallbackReason::UnsupportedStaticCall)), + ExprKind::ClosureCall { .. } | ExprKind::ExprCall { .. } => { + Some(EvalAotFallbackReason::DynamicCall) + } + ExprKind::Pipe { value, callable } => expr_fallback_reason(value) + .or_else(|| expr_fallback_reason(callable)) + .or(Some(EvalAotFallbackReason::DynamicCall)), + ExprKind::NewDynamic { .. } | ExprKind::NewDynamicObject { .. } => { + Some(EvalAotFallbackReason::DynamicClassOrMember) + } + ExprKind::DynamicPropertyAccess { .. } + | ExprKind::NullsafeDynamicPropertyAccess { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } => { + Some(EvalAotFallbackReason::DynamicClassOrMember) + } + ExprKind::NewObject { .. } + | ExprKind::NewScopedObject { .. } + | ExprKind::PropertyAccess { .. } + | ExprKind::NullsafePropertyAccess { .. } + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::MethodCall { .. } + | ExprKind::NullsafeMethodCall { .. } + | ExprKind::StaticMethodCall { .. } + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::This => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) | ExprKind::Spread(_) => { + Some(EvalAotFallbackReason::ArrayOrIterable) + } + ExprKind::Assignment { .. } + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::NamedArg { .. } => Some(EvalAotFallbackReason::UnsupportedScope), + ExprKind::Closure { .. } => Some(EvalAotFallbackReason::Declaration), + ExprKind::IncludeValue { .. } => Some(EvalAotFallbackReason::IncludeOrRequire), + ExprKind::InstanceOf { value, target } => expr_fallback_reason(value) + .or_else(|| match target { + crate::parser::ast::InstanceOfTarget::Name(_) => None, + crate::parser::ast::InstanceOfTarget::Expr(expr) => expr_fallback_reason(expr), + }) + .or(Some(EvalAotFallbackReason::ObjectOrMemberAccess)), + ExprKind::FirstClassCallable(target) => callable_target_fallback_reason(target), + ExprKind::ConstRef(_) | ExprKind::MagicConstant(_) => { + Some(EvalAotFallbackReason::UnsupportedConstruct) + } + ExprKind::PtrCast { expr, .. } => { + expr_fallback_reason(expr).or(Some(EvalAotFallbackReason::UnsupportedConstruct)) + } + ExprKind::BufferNew { len, .. } => { + expr_fallback_reason(len).or(Some(EvalAotFallbackReason::UnsupportedConstruct)) + } + ExprKind::Yield { .. } => Some(EvalAotFallbackReason::UnsupportedControlFlow), + } +} + +/// Classifies first-class callable expressions for fallback markers. +fn callable_target_fallback_reason(target: &CallableTarget) -> Option { + match target { + CallableTarget::Function(_) => Some(EvalAotFallbackReason::DynamicCall), + CallableTarget::StaticMethod { .. } => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + CallableTarget::Method { object, .. } => { + expr_fallback_reason(object).or(Some(EvalAotFallbackReason::ObjectOrMemberAccess)) + } + } +} + +/// Returns true for the native-only scalar eval subset in an already parsed program. +fn program_is_native_only_scalar(program: &[Stmt], static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + let mut terminated = false; + for stmt in program { + if terminated { + return false; + } + let Some(done) = stmt_is_native_only_scalar(stmt, &static_call_supported) else { + return false; + }; + terminated = done; + } + true +} + +/// Variable read/write metadata collected from a parsed eval fragment. +struct EvalScopeAccess { + reads: BTreeSet, + writes: BTreeSet, + creates_unknown_vars: bool, +} + +impl EvalScopeAccess { + /// Creates an empty eval scope access accumulator. + fn new() -> Self { + Self { + reads: BTreeSet::new(), + writes: BTreeSet::new(), + creates_unknown_vars: false, + } + } + + /// Returns true when the fragment touches any eval-visible variable storage. + fn has_scope_access(&self) -> bool { + !self.reads.is_empty() || !self.writes.is_empty() || self.creates_unknown_vars + } + + /// Records a variable read. + fn read(&mut self, name: &str) { + self.reads.insert(name.to_string()); + } + + /// Records a variable write. + fn write(&mut self, name: &str) { + self.writes.insert(name.to_string()); + } + + /// Marks an access shape that cannot be mapped to a static variable name. + fn unknown_write(&mut self) { + self.creates_unknown_vars = true; + } +} + +/// Collects conservative eval-scope reads and writes from a parsed fragment. +fn collect_scope_accesses(program: &[Stmt]) -> EvalScopeAccess { + let mut access = EvalScopeAccess::new(); + for stmt in program { + collect_stmt_scope_access(stmt, &mut access); + } + access +} + +/// Collects variable reads that must come from the caller before local writes exist. +fn collect_scope_reads_before_writes(program: &[Stmt]) -> BTreeSet { + let mut reads = BTreeSet::new(); + let mut assigned = BTreeSet::new(); + collect_block_scope_reads_before_writes(program, &mut assigned, &mut reads); + reads +} + +/// Collects caller reads across a statement block and tracks definite local writes. +fn collect_block_scope_reads_before_writes( + body: &[Stmt], + assigned: &mut BTreeSet, + reads: &mut BTreeSet, +) { + for stmt in body { + collect_stmt_scope_reads_before_writes(stmt, assigned, reads); + } +} + +/// Collects caller reads for one statement before updating local assignment facts. +fn collect_stmt_scope_reads_before_writes( + stmt: &Stmt, + assigned: &mut BTreeSet, + reads: &mut BTreeSet, +) { + match &stmt.kind { + StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { + collect_expr_scope_reads_before_writes(value, assigned, reads); + assigned.insert(name.clone()); + } + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::Return(Some(expr)) => { + collect_expr_scope_reads_before_writes(expr, assigned, reads); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + let before = assigned.clone(); + let mut branch_outputs = Vec::new(); + let mut then_assigned = before.clone(); + collect_block_scope_reads_before_writes(then_body, &mut then_assigned, reads); + branch_outputs.push(then_assigned); + for (condition, body) in elseif_clauses { + collect_expr_scope_reads_before_writes(condition, &before, reads); + let mut branch_assigned = before.clone(); + collect_block_scope_reads_before_writes(body, &mut branch_assigned, reads); + branch_outputs.push(branch_assigned); + } + if let Some(else_body) = else_body { + let mut else_assigned = before.clone(); + collect_block_scope_reads_before_writes(else_body, &mut else_assigned, reads); + branch_outputs.push(else_assigned); + retain_definitely_assigned_after_branches(assigned, before, &branch_outputs); + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + let mut body_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_scope_reads_before_writes(init, assigned, reads); + } + if let Some(condition) = condition { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + } + let mut body_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + if let Some(update) = update { + collect_stmt_scope_reads_before_writes(update, &mut body_assigned, reads); + } + } + StmtKind::Foreach { + array, + key_var, + value_var, + body, + .. + } => { + collect_expr_scope_reads_before_writes(array, assigned, reads); + if expr_is_static_empty_array_literal_source(array) { + return; + } + let mut body_assigned = assigned.clone(); + body_assigned.insert(value_var.clone()); + if let Some(key_var) = key_var { + body_assigned.insert(key_var.clone()); + } + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + if expr_is_non_empty_static_array_literal_source(array) { + assigned.insert(value_var.clone()); + if let Some(key_var) = key_var { + assigned.insert(key_var.clone()); + } + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_scope_reads_before_writes(subject, assigned, reads); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + } + let mut case_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut case_assigned, reads); + } + if let Some(default) = default { + let mut default_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(default, &mut default_assigned, reads); + } + } + StmtKind::Synthetic(body) | StmtKind::NamespaceBlock { body, .. } => { + collect_block_scope_reads_before_writes(body, assigned, reads); + } + _ => { + let mut access = EvalScopeAccess::new(); + collect_stmt_scope_access(stmt, &mut access); + extend_reads_not_assigned(reads, assigned, access.reads); + assigned.extend(access.writes); + } + } +} + +/// Keeps only names assigned on every branch after an if/elseif/else chain. +fn retain_definitely_assigned_after_branches( + assigned: &mut BTreeSet, + before: BTreeSet, + branch_outputs: &[BTreeSet], +) { + let mut definitely = before; + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.iter()) + { + if branch_outputs.iter().all(|branch| branch.contains(name)) { + definitely.insert(name.clone()); + } + } + *assigned = definitely; +} + +/// Collects caller reads from one expression using current assignment facts. +fn collect_expr_scope_reads_before_writes( + expr: &Expr, + assigned: &BTreeSet, + reads: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Variable(name) => { + if !assigned.contains(name) { + reads.insert(name.clone()); + } + } + ExprKind::Assignment { + prelude, + target, + value, + result_target, + .. + } => { + let mut expr_assigned = assigned.clone(); + for stmt in prelude { + collect_stmt_scope_reads_before_writes(stmt, &mut expr_assigned, reads); + } + collect_expr_scope_reads_before_writes(value, &expr_assigned, reads); + match &target.kind { + ExprKind::Variable(name) => { + expr_assigned.insert(name.clone()); + } + _ => collect_expr_scope_reads_before_writes(target, &expr_assigned, reads), + } + if let Some(result_target) = result_target { + collect_expr_scope_reads_before_writes(result_target, &expr_assigned, reads); + } + } + _ => { + let mut access = EvalScopeAccess::new(); + collect_expr_scope_access(expr, &mut access); + extend_reads_not_assigned(reads, assigned, access.reads); + } + } +} + +/// Adds collected reads that are not already definitely local to this fragment. +fn extend_reads_not_assigned( + reads: &mut BTreeSet, + assigned: &BTreeSet, + names: BTreeSet, +) { + reads.extend(names.into_iter().filter(|name| !assigned.contains(name))); +} + +/// Caller-scope variables that need array-specific call-site proof. +#[derive(Default)] +struct ArrayScopeReadConstraintSets { + array_like: BTreeSet, + assoc: BTreeSet, +} + +/// Collects caller-scope reads that must be array-like for accepted AOT calls. +fn collect_array_scope_read_constraint_sets( + program: &[Stmt], + scope_reads: &BTreeSet, +) -> ArrayScopeReadConstraintSets { + let mut constraints = ArrayScopeReadConstraintSets::default(); + for stmt in program { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, &mut constraints); + } + constraints +} + +/// Collects array constraints from one statement in the EIR AOT subset. +fn collect_stmt_array_scope_read_constraints( + stmt: &Stmt, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + match &stmt.kind { + StmtKind::Synthetic(body) => { + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + collect_expr_array_scope_read_constraints(expr, scope_reads, constraints); + } + StmtKind::Assign { value, .. } => { + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in then_body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + for (condition, body) in elseif_clauses { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + if let Some(else_body) = else_body { + for stmt in else_body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_array_scope_read_constraints(init, scope_reads, constraints); + } + if let Some(condition) = condition { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + if let Some(update) = update { + collect_stmt_array_scope_read_constraints(update, scope_reads, constraints); + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Foreach { array, body, .. } => { + if let ExprKind::Variable(variable) = &array.kind { + if scope_reads.contains(variable) { + constraints.array_like.insert(variable.clone()); + } + } + collect_expr_array_scope_read_constraints(array, scope_reads, constraints); + if expr_is_static_empty_array_literal_source(array) { + return; + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_array_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + if let Some(default) = default { + for stmt in default { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + } + _ => {} + } +} + +/// Collects array constraints from one expression in the EIR AOT subset. +fn collect_expr_array_scope_read_constraints( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + match &expr.kind { + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } => { + collect_expr_array_scope_read_constraints(inner, scope_reads, constraints); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_array_scope_read_constraints(left, scope_reads, constraints); + collect_expr_array_scope_read_constraints(right, scope_reads, constraints); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + collect_expr_array_scope_read_constraints(then_expr, scope_reads, constraints); + collect_expr_array_scope_read_constraints(else_expr, scope_reads, constraints); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + collect_expr_array_scope_read_constraints(default, scope_reads, constraints); + } + ExprKind::Cast { expr, .. } => { + collect_expr_array_scope_read_constraints(expr, scope_reads, constraints); + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_array_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, result) in arms { + for condition in conditions { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + collect_expr_array_scope_read_constraints(result, scope_reads, constraints); + } + if let Some(default) = default { + collect_expr_array_scope_read_constraints(default, scope_reads, constraints); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_array_scope_read_constraints(array, scope_reads, constraints); + collect_expr_array_scope_read_constraints(index, scope_reads, constraints); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_array_scope_read_constraints(item, scope_reads, constraints); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_array_scope_read_constraints(key, scope_reads, constraints); + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + } + } + ExprKind::FunctionCall { name, args } => { + collect_builtin_array_scope_read_constraints( + name.as_str(), + args, + scope_reads, + constraints, + ); + for arg in args { + collect_expr_array_scope_read_constraints(arg, scope_reads, constraints); + } + } + _ => {} + } +} + +/// Collects array caller-type constraints from supported runtime builtin calls. +fn collect_builtin_array_scope_read_constraints( + name: &str, + args: &[Expr], + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + let short_name = php_symbol_key(name.trim_start_matches('\\')); + let Some(args) = normalize_eir_runtime_builtin_args(&short_name, args) else { + return; + }; + match short_name.as_str() { + "count" if (1..=2).contains(&args.len()) && eir_count_mode_is_default_zero(args.get(1)) => { + collect_scope_array_like_constraint(&args[0], scope_reads, constraints); + } + "array_key_exists" + if args.len() == 2 && eir_array_key_exists_static_key_is_safe(&args[0]) => + { + collect_scope_array_like_constraint(&args[1], scope_reads, constraints); + if eir_array_key_exists_static_key_needs_assoc_array(&args[0]) { + collect_scope_assoc_array_constraint(&args[1], scope_reads, constraints); + } + } + _ => {} + } +} + +/// Records that one expression must be a caller-side array when it reads scope. +fn collect_scope_array_like_constraint( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + if let ExprKind::Variable(variable) = &expr.kind { + if scope_reads.contains(variable) { + constraints.array_like.insert(variable.clone()); + } + } +} + +/// Records that one expression must be a caller-side associative array. +fn collect_scope_assoc_array_constraint( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + if let ExprKind::Variable(variable) = &expr.kind { + if scope_reads.contains(variable) { + constraints.assoc.insert(variable.clone()); + } + } +} + +/// Collects caller-scope reads that must be int/float for IEEE float predicates. +fn collect_float_predicate_scope_read_constraints( + program: &[Stmt], + scope_reads: &BTreeSet, +) -> BTreeSet { + let mut constraints = BTreeSet::new(); + for stmt in program { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, &mut constraints); + } + constraints +} + +/// Collects float-predicate constraints from one statement in the EIR AOT subset. +fn collect_stmt_float_predicate_scope_read_constraints( + stmt: &Stmt, + scope_reads: &BTreeSet, + constraints: &mut BTreeSet, +) { + match &stmt.kind { + StmtKind::Synthetic(body) => { + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + collect_expr_float_predicate_scope_read_constraints(expr, scope_reads, constraints); + } + StmtKind::Assign { value, .. } => { + collect_expr_float_predicate_scope_read_constraints(value, scope_reads, constraints); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in then_body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + for (condition, body) in elseif_clauses { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + if let Some(else_body) = else_body { + for stmt in else_body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_float_predicate_scope_read_constraints(init, scope_reads, constraints); + } + if let Some(condition) = condition { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + if let Some(update) = update { + collect_stmt_float_predicate_scope_read_constraints( + update, + scope_reads, + constraints, + ); + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Foreach { array, body, .. } => { + collect_expr_float_predicate_scope_read_constraints(array, scope_reads, constraints); + if expr_is_static_empty_array_literal_source(array) { + return; + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_float_predicate_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + if let Some(default) = default { + for stmt in default { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + } + _ => {} + } +} + +/// Collects float-predicate constraints from one expression in the EIR AOT subset. +fn collect_expr_float_predicate_scope_read_constraints( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } => { + collect_expr_float_predicate_scope_read_constraints(inner, scope_reads, constraints); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_float_predicate_scope_read_constraints(left, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(right, scope_reads, constraints); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + collect_expr_float_predicate_scope_read_constraints( + then_expr, + scope_reads, + constraints, + ); + collect_expr_float_predicate_scope_read_constraints( + else_expr, + scope_reads, + constraints, + ); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_expr_float_predicate_scope_read_constraints(value, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(default, scope_reads, constraints); + } + ExprKind::Cast { expr, .. } => { + collect_expr_float_predicate_scope_read_constraints(expr, scope_reads, constraints); + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_float_predicate_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, result) in arms { + for condition in conditions { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + collect_expr_float_predicate_scope_read_constraints( + result, + scope_reads, + constraints, + ); + } + if let Some(default) = default { + collect_expr_float_predicate_scope_read_constraints( + default, + scope_reads, + constraints, + ); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_float_predicate_scope_read_constraints(array, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(index, scope_reads, constraints); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_float_predicate_scope_read_constraints(item, scope_reads, constraints); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_float_predicate_scope_read_constraints(key, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints( + value, + scope_reads, + constraints, + ); + } + } + ExprKind::FunctionCall { name, args } => { + let name = php_symbol_key(name.as_str().trim_start_matches('\\')); + if matches!(name.as_str(), "is_finite" | "is_infinite" | "is_nan") && args.len() == 1 { + collect_scope_read_variables_in_expr(&args[0], scope_reads, constraints); + } + for arg in args { + collect_expr_float_predicate_scope_read_constraints(arg, scope_reads, constraints); + } + } + _ => {} + } +} + +/// Collects scope-read variable names that occur anywhere inside an expression. +fn collect_scope_read_variables_in_expr( + expr: &Expr, + scope_reads: &BTreeSet, + variables: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Variable(name) => { + if scope_reads.contains(name) { + variables.insert(name.clone()); + } + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } + | ExprKind::Cast { expr: inner, .. } => { + collect_scope_read_variables_in_expr(inner, scope_reads, variables); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_scope_read_variables_in_expr(left, scope_reads, variables); + collect_scope_read_variables_in_expr(right, scope_reads, variables); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_scope_read_variables_in_expr(condition, scope_reads, variables); + collect_scope_read_variables_in_expr(then_expr, scope_reads, variables); + collect_scope_read_variables_in_expr(else_expr, scope_reads, variables); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_scope_read_variables_in_expr(value, scope_reads, variables); + collect_scope_read_variables_in_expr(default, scope_reads, variables); + } + ExprKind::ArrayAccess { array, index } => { + collect_scope_read_variables_in_expr(array, scope_reads, variables); + collect_scope_read_variables_in_expr(index, scope_reads, variables); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_scope_read_variables_in_expr(item, scope_reads, variables); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_scope_read_variables_in_expr(key, scope_reads, variables); + collect_scope_read_variables_in_expr(value, scope_reads, variables); + } + } + ExprKind::FunctionCall { args, .. } => { + for arg in args { + collect_scope_read_variables_in_expr(arg, scope_reads, variables); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_scope_read_variables_in_expr(subject, scope_reads, variables); + for (conditions, result) in arms { + for condition in conditions { + collect_scope_read_variables_in_expr(condition, scope_reads, variables); + } + collect_scope_read_variables_in_expr(result, scope_reads, variables); + } + if let Some(default) = default { + collect_scope_read_variables_in_expr(default, scope_reads, variables); + } + } + _ => {} + } +} + +/// Adds one statement's eval-scope reads and writes to the accumulator. +fn collect_stmt_scope_access(stmt: &Stmt, access: &mut EvalScopeAccess) { + match &stmt.kind { + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::Return(Some(expr)) => collect_expr_scope_access(expr, access), + StmtKind::Return(None) + | StmtKind::Break(_) + | StmtKind::Continue(_) + | StmtKind::NamespaceDecl { .. } + | StmtKind::UseDecl { .. } + | StmtKind::IncludeOnceMark { .. } + | StmtKind::FunctionVariantMark { .. } => {} + StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { + collect_expr_scope_access(value, access); + access.write(name); + } + StmtKind::RefAssign { target, source } => { + access.write(target); + collect_expr_scope_access(source, access); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_scope_access(condition, access); + collect_block_scope_access(then_body, access); + for (condition, body) in elseif_clauses { + collect_expr_scope_access(condition, access); + collect_block_scope_access(body, access); + } + if let Some(else_body) = else_body { + collect_block_scope_access(else_body, access); + } + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + collect_block_scope_access(then_body, access); + if let Some(else_body) = else_body { + collect_block_scope_access(else_body, access); + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_scope_access(condition, access); + collect_block_scope_access(body, access); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_scope_access(init, access); + } + if let Some(condition) = condition { + collect_expr_scope_access(condition, access); + } + if let Some(update) = update { + collect_stmt_scope_access(update, access); + } + collect_block_scope_access(body, access); + } + StmtKind::ArrayAssign { + array, + index, + value, + } => { + access.read(array); + access.write(array); + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::NestedArrayAssign { target, value } => { + collect_assignment_target_scope_access(target, access); + collect_expr_scope_access(value, access); + } + StmtKind::ArrayPush { array, value } => { + access.read(array); + access.write(array); + collect_expr_scope_access(value, access); + } + StmtKind::Foreach { + array, + key_var, + value_var, + body, + .. + } => { + collect_expr_scope_access(array, access); + if expr_is_static_empty_array_literal_source(array) { + return; + } + if let Some(key_var) = key_var { + access.write(key_var); + } + access.write(value_var); + collect_block_scope_access(body, access); + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_scope_access(subject, access); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_scope_access(condition, access); + } + collect_block_scope_access(body, access); + } + if let Some(default) = default { + collect_block_scope_access(default, access); + } + } + StmtKind::IncludeOnceGuard { body, .. } | StmtKind::Synthetic(body) => { + collect_block_scope_access(body, access); + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + collect_block_scope_access(try_body, access); + for catch in catches { + if let Some(variable) = &catch.variable { + access.write(variable); + } + collect_block_scope_access(&catch.body, access); + } + if let Some(finally_body) = finally_body { + collect_block_scope_access(finally_body, access); + } + } + StmtKind::NamespaceBlock { body, .. } => collect_block_scope_access(body, access), + StmtKind::FunctionDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::ConstDecl { .. } + | StmtKind::ClassDecl { .. } + | StmtKind::EnumDecl { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::InterfaceDecl { .. } + | StmtKind::TraitDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => {} + StmtKind::ListUnpack { vars, value } => { + collect_expr_scope_access(value, access); + for var in vars { + access.write(var); + } + } + StmtKind::Global { vars } => { + for var in vars { + access.write(var); + } + access.creates_unknown_vars = true; + } + StmtKind::StaticVar { name, init } => { + collect_expr_scope_access(init, access); + access.write(name); + access.creates_unknown_vars = true; + } + StmtKind::PropertyAssign { object, value, .. } + | StmtKind::PropertyArrayPush { object, value, .. } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(value, access); + } + StmtKind::PropertyArrayAssign { + object, + index, + value, + .. + } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::StaticPropertyAssign { value, .. } + | StmtKind::StaticPropertyArrayPush { value, .. } => { + collect_expr_scope_access(value, access); + } + StmtKind::StaticPropertyArrayAssign { index, value, .. } => { + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::Include { path, .. } => collect_expr_scope_access(path, access), + } +} + +/// Adds every statement in a block to the scope access accumulator. +fn collect_block_scope_access(body: &[Stmt], access: &mut EvalScopeAccess) { + for stmt in body { + collect_stmt_scope_access(stmt, access); + } +} + +/// Adds one expression's eval-scope reads and writes to the accumulator. +fn collect_expr_scope_access(expr: &Expr, access: &mut EvalScopeAccess) { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::ConstRef(_) + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => {} + ExprKind::Variable(name) + | ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => { + access.read(name); + if matches!( + &expr.kind, + ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + ) { + access.write(name); + } + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_scope_access(left, access); + collect_expr_scope_access(right, access); + } + ExprKind::InstanceOf { value, target } => { + collect_expr_scope_access(value, access); + if let crate::parser::ast::InstanceOfTarget::Expr(target) = target { + collect_expr_scope_access(target, access); + } + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::Throw(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::Clone(inner) + | ExprKind::YieldFrom(inner) + | ExprKind::Cast { expr: inner, .. } + | ExprKind::PtrCast { expr: inner, .. } => collect_expr_scope_access(inner, access), + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } + | ExprKind::Pipe { + value, + callable: default, + } => { + collect_expr_scope_access(value, access); + collect_expr_scope_access(default, access); + } + ExprKind::Assignment { + target, + value, + result_target, + prelude, + .. + } => { + for stmt in prelude { + collect_stmt_scope_access(stmt, access); + } + collect_assignment_target_scope_access(target, access); + collect_expr_scope_access(value, access); + if let Some(result_target) = result_target { + collect_assignment_target_scope_access(result_target, access); + } + } + ExprKind::FunctionCall { args, .. } + | ExprKind::ClosureCall { args, .. } + | ExprKind::ExprCall { args, .. } + | ExprKind::NewObject { args, .. } + | ExprKind::StaticMethodCall { args, .. } + | ExprKind::NewScopedObject { args, .. } => { + if let ExprKind::ExprCall { callee, .. } = &expr.kind { + collect_expr_scope_access(callee, access); + } + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_scope_access(item, access); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_scope_access(key, access); + collect_expr_scope_access(value, access); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_scope_access(subject, access); + for (conditions, value) in arms { + for condition in conditions { + collect_expr_scope_access(condition, access); + } + collect_expr_scope_access(value, access); + } + if let Some(default) = default { + collect_expr_scope_access(default, access); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_scope_access(array, access); + collect_expr_scope_access(index, access); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_scope_access(condition, access); + collect_expr_scope_access(then_expr, access); + collect_expr_scope_access(else_expr, access); + } + ExprKind::Closure { + params, + body, + captures, + capture_refs, + .. + } => { + for (_, _, default, _) in params { + if let Some(default) = default { + collect_expr_scope_access(default, access); + } + } + for capture in captures.iter().chain(capture_refs.iter()) { + access.read(capture); + } + collect_block_scope_access(body, access); + } + ExprKind::NamedArg { value, .. } => collect_expr_scope_access(value, access), + ExprKind::IncludeValue { path, .. } => collect_expr_scope_access(path, access), + ExprKind::NewDynamic { name_expr, args } => { + collect_expr_scope_access(name_expr, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::NewDynamicObject { + class_name, args, .. + } => { + collect_expr_scope_access(class_name, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + collect_expr_scope_access(object, access); + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(property, access); + } + ExprKind::NullsafeMethodCall { object, args, .. } + | ExprKind::MethodCall { object, args, .. } => { + collect_expr_scope_access(object, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(method, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::StaticPropertyAccess { .. } | ExprKind::This => {} + ExprKind::BufferNew { len, .. } => collect_expr_scope_access(len, access), + ExprKind::FirstClassCallable(target) => { + collect_callable_target_scope_access(target, access) + } + ExprKind::Yield { key, value } => { + if let Some(key) = key { + collect_expr_scope_access(key, access); + } + if let Some(value) = value { + collect_expr_scope_access(value, access); + } + } + } +} + +/// Records the variable effects of an assignment target expression. +fn collect_assignment_target_scope_access(expr: &Expr, access: &mut EvalScopeAccess) { + match &expr.kind { + ExprKind::Variable(name) => access.write(name), + ExprKind::ArrayAccess { array, index } => { + collect_expr_scope_access(array, access); + collect_expr_scope_access(index, access); + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + collect_expr_scope_access(object, access); + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(property, access); + } + _ => { + collect_expr_scope_access(expr, access); + access.unknown_write(); + } + } +} + +/// Adds variable reads from a first-class callable target. +fn collect_callable_target_scope_access( + target: &crate::parser::ast::CallableTarget, + access: &mut EvalScopeAccess, +) { + match target { + crate::parser::ast::CallableTarget::Function(_) => {} + crate::parser::ast::CallableTarget::StaticMethod { .. } => {} + crate::parser::ast::CallableTarget::Method { object, .. } => { + collect_expr_scope_access(object, access); + } + } +} + +/// Hashes a fragment with a stable FNV-1a variant for deterministic symbol names. +fn stable_fragment_hash(fragment: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325u64; + for byte in fragment.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + hash +} + +/// Definite local facts tracked while classifying an eval fragment for EIR AOT. +#[derive(Clone, Default)] +struct EirLocalFacts { + assigned: BTreeSet, + int_locals: BTreeSet, + float_locals: BTreeSet, + array_locals: BTreeSet, +} + +impl EirLocalFacts { + /// Creates empty local facts for a fresh eval fragment block. + fn new() -> Self { + Self::default() + } + + /// Returns true when a variable is definitely assigned in this control-flow path. + fn is_assigned(&self, name: &str) -> bool { + self.assigned.contains(name) + } + + /// Returns true when a variable is definitely assigned as an integer value. + fn is_int_local(&self, name: &str) -> bool { + self.int_locals.contains(name) + } + + /// Returns true when a variable is definitely assigned as a floating value. + fn is_float_local(&self, name: &str) -> bool { + self.float_locals.contains(name) + } + + /// Returns true when a variable is definitely assigned from a static array literal. + fn is_array_local(&self, name: &str) -> bool { + self.array_locals.contains(name) + } + + /// Records an assignment and updates scalar/array local facts for that variable. + fn assign( + &mut self, + name: &str, + value: &Expr, + support: &S, + scope_reads: Option<&BTreeSet>, + ) where + S: EirStaticCallSupport, + { + self.assigned.insert(name.to_string()); + if expr_is_eir_int_value_safe(value, support, self, scope_reads) { + self.int_locals.insert(name.to_string()); + } else { + self.int_locals.remove(name); + } + if expr_is_eir_float_value_safe(value, support, self, scope_reads) { + self.float_locals.insert(name.to_string()); + } else { + self.float_locals.remove(name); + } + if expr_is_eir_static_array_literal_source_safe(value, support, self, scope_reads) { + self.array_locals.insert(name.to_string()); + } else { + self.array_locals.remove(name); + } + } + + /// Records that a variable is definitely assigned, but with no narrower local fact. + fn assign_unknown(&mut self, name: &str) { + self.assigned.insert(name.to_string()); + self.int_locals.remove(name); + self.array_locals.remove(name); + } +} + +/// Returns true when the fragment can be lowered as a no-scope EIR function today. +fn program_is_eir_function_safe(program: &[Stmt], support: &S) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + block_is_eir_function_safe(program, support, &mut facts, None, 0).is_some() +} + +/// Returns true when a fragment can be lowered as an EIR function reading eval scope. +fn program_is_eir_scope_read_function_safe( + program: &[Stmt], + support: &S, + scope_reads: &BTreeSet, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + block_is_eir_function_safe(program, support, &mut facts, Some(scope_reads), 0).is_some() +} + +/// Returns true when a writes-only fragment can be an EIR eval-scope function. +fn program_is_eir_scope_write_linear_function_safe( + program: &[Stmt], + support: &S, + scope_names: &BTreeSet, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + let mut terminated = false; + for stmt in program { + if terminated { + return false; + } + let Some(done) = + stmt_is_eir_scope_write_linear_function_safe(stmt, support, scope_names, &mut facts) + else { + return false; + }; + terminated = done; + } + true +} + +/// Checks one statement for the linear writes-only EIR eval-scope subset. +fn stmt_is_eir_scope_write_linear_function_safe( + stmt: &Stmt, + support: &S, + scope_names: &BTreeSet, + facts: &mut EirLocalFacts, +) -> Option +where + S: EirStaticCallSupport, +{ + match &stmt.kind { + StmtKind::Synthetic(body) => { + let mut terminated = false; + for stmt in body { + if terminated { + return None; + } + terminated = stmt_is_eir_scope_write_linear_function_safe( + stmt, + support, + scope_names, + facts, + )?; + } + Some(terminated) + } + StmtKind::Assign { name, value } if scope_names.contains(name) => { + expr_is_eir_function_safe(value, support, facts, Some(scope_names)).then_some(())?; + facts.assign(name, value, support, Some(scope_names)); + Some(false) + } + StmtKind::Echo(expr) => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(false) + } + StmtKind::Return(Some(expr)) => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + expr_is_eir_function_safe(inner, support, facts, Some(scope_names)).then_some(false) + } + _ => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(false) + } + }, + _ => None, + } +} + +/// Checks a statement block for the no-scope EIR-function eval subset. +fn block_is_eir_function_safe( + body: &[Stmt], + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> Option +where + S: EirStaticCallSupport, +{ + let mut terminated = false; + for stmt in body { + if terminated { + return None; + } + let done = stmt_is_eir_function_safe(stmt, support, facts, scope_reads, loop_depth)?; + terminated = done; + } + Some(terminated) +} + +/// Checks one statement for the initial no-scope EIR-function eval subset. +fn stmt_is_eir_function_safe( + stmt: &Stmt, + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> Option +where + S: EirStaticCallSupport, +{ + match &stmt.kind { + StmtKind::Synthetic(body) => { + block_is_eir_function_safe(body, support, facts, scope_reads, loop_depth) + } + StmtKind::Echo(expr) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(false) + } + StmtKind::Assign { name, value } + if scope_reads.is_some_and(|names| names.contains(name)) => + { + expr_is_eir_function_safe(value, support, facts, scope_reads).then_some(())?; + facts.assign(name, value, support, scope_reads); + Some(false) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + if_stmt_is_eir_function_safe( + then_body, + elseif_clauses, + else_body.as_deref(), + support, + facts, + scope_reads, + loop_depth, + ) + .then_some(false) + } + StmtKind::While { condition, body } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + let mut body_facts = facts.clone(); + block_is_eir_function_safe(body, support, &mut body_facts, scope_reads, loop_depth + 1) + .map(|_| false) + } + StmtKind::DoWhile { condition, body } => { + let mut body_facts = facts.clone(); + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + expr_is_eir_function_safe(condition, support, &body_facts, scope_reads).then_some(false) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + if stmt_is_eir_function_safe(init, support, facts, scope_reads, loop_depth)? { + return None; + } + } + if let Some(condition) = condition { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + } + let mut body_facts = facts.clone(); + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + if let Some(update) = update { + if stmt_is_eir_function_safe( + update, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )? { + return None; + } + } + Some(false) + } + StmtKind::Foreach { + array, + key_var, + value_var, + value_by_ref, + body, + } => { + let static_empty = expr_is_static_empty_array_literal_source(array); + if (scope_reads.is_none() && !static_empty) + || *value_by_ref + || !expr_is_eir_foreach_source_safe(array, scope_reads) + { + return None; + } + expr_is_eir_foreach_source_lowerable(array, support, facts, scope_reads) + .then_some(())?; + let mut body_facts = facts.clone(); + body_facts.assign_unknown(value_var); + if let Some(key_var) = key_var { + body_facts.assign_unknown(key_var); + } + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + if expr_is_non_empty_static_array_literal_source(array) { + facts.assign_unknown(value_var); + if let Some(key_var) = key_var { + facts.assign_unknown(key_var); + } + } + Some(false) + } + StmtKind::Switch { + subject, + cases, + default, + } => switch_stmt_is_eir_function_safe( + subject, + cases, + default.as_deref(), + support, + facts, + scope_reads, + loop_depth, + ) + .then_some(false), + StmtKind::Break(level) => (*level > 0 && *level <= loop_depth).then_some(true), + StmtKind::Continue(level) => (*level > 0 && *level <= loop_depth).then_some(true), + StmtKind::Return(Some(expr)) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + expr_is_eir_function_safe(inner, support, facts, scope_reads).then_some(false) + } + _ => expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(false), + }, + _ => None, + } +} + +/// Returns true when a foreach source has EIR-safe eval AOT semantics. +fn expr_is_eir_foreach_source_safe(expr: &Expr, scope_reads: Option<&BTreeSet>) -> bool { + if expr_is_static_array_literal_source(expr) { + return true; + } + matches!( + &expr.kind, + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) + ) +} + +/// Returns true when a foreach source can be lowered by the EIR backend. +fn expr_is_eir_foreach_source_lowerable( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + _ => expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true when a static array source is a literal expression. +fn expr_is_static_array_literal_source(expr: &Expr) -> bool { + matches!( + &expr.kind, + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) + ) +} + +/// Returns true when a static array source is a literal known to skip its body. +fn expr_is_static_empty_array_literal_source(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::ArrayLiteral(items) => items.is_empty(), + ExprKind::ArrayLiteralAssoc(pairs) => pairs.is_empty(), + _ => false, + } +} + +/// Returns true when a static array source is a literal known to iterate at least once. +fn expr_is_non_empty_static_array_literal_source(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::ArrayLiteral(items) => !items.is_empty(), + ExprKind::ArrayLiteralAssoc(pairs) => !pairs.is_empty(), + _ => false, + } +} + +/// Checks a switch statement while preserving conservative assignment facts. +fn switch_stmt_is_eir_function_safe( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> bool +where + S: EirStaticCallSupport, +{ + if !expr_is_eir_function_safe(subject, support, facts, scope_reads) { + return false; + } + if !switch_default_position_is_eir_safe(cases, default) { + return false; + } + for (conditions, body) in cases { + for condition in conditions { + if !expr_is_eir_function_safe(condition, support, facts, scope_reads) { + return false; + } + } + let mut case_facts = facts.clone(); + if block_is_eir_function_safe(body, support, &mut case_facts, scope_reads, loop_depth + 1) + .is_none() + { + return false; + } + } + if let Some(default) = default { + let mut default_facts = facts.clone(); + if block_is_eir_function_safe( + default, + support, + &mut default_facts, + scope_reads, + loop_depth + 1, + ) + .is_none() + { + return false; + } + } + true +} + +/// Returns true when EIR switch lowering can reconstruct the default source position. +fn switch_default_position_is_eir_safe( + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, +) -> bool { + let Some(default) = default else { + return true; + }; + if cases.is_empty() { + return true; + } + let Some(default_start) = default.first().map(|stmt| stmt.span) else { + return false; + }; + if default_start == crate::span::Span::dummy() { + return false; + } + cases.iter().all(|(conditions, _)| { + conditions + .first() + .is_some_and(|condition| condition.span != crate::span::Span::dummy()) + }) +} + +/// Checks an if/elseif/else chain and propagates only definitely assigned locals. +fn if_stmt_is_eir_function_safe( + then_body: &[Stmt], + elseif_clauses: &[(Expr, Vec)], + else_body: Option<&[Stmt]>, + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> bool +where + S: EirStaticCallSupport, +{ + let before = facts.clone(); + let mut branch_outputs = Vec::new(); + let mut then_facts = before.clone(); + if block_is_eir_function_safe(then_body, support, &mut then_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(then_facts); + + for (condition, body) in elseif_clauses { + if !expr_is_eir_function_safe(condition, support, &before, scope_reads) { + return false; + } + let mut branch_facts = before.clone(); + if block_is_eir_function_safe(body, support, &mut branch_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(branch_facts); + } + + let Some(else_body) = else_body else { + return true; + }; + let mut else_facts = before.clone(); + if block_is_eir_function_safe(else_body, support, &mut else_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(else_facts); + + *facts = definitely_assigned_after_eir_branches(before, &branch_outputs); + true +} + +/// Keeps only local facts that are true after every branch in an if/elseif/else chain. +fn definitely_assigned_after_eir_branches( + before: EirLocalFacts, + branch_outputs: &[EirLocalFacts], +) -> EirLocalFacts { + let mut definitely = before; + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.assigned.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.assigned.contains(name)) + { + definitely.assigned.insert(name.clone()); + } + } + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.int_locals.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.int_locals.contains(name)) + { + definitely.int_locals.insert(name.clone()); + } + } + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.array_locals.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.array_locals.contains(name)) + { + definitely.array_locals.insert(name.clone()); + } + } + definitely +} + +/// Checks one expression for the initial no-scope EIR-function eval subset. +fn expr_is_eir_function_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => true, + ExprKind::Variable(name) => { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) => expr_is_eir_function_safe(inner, support, facts, scope_reads), + ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => facts.is_int_local(name), + ExprKind::BinaryOp { left, op, right } => { + matches!( + op, + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Div + | BinOp::Mod + | BinOp::Pow + | BinOp::Lt + | BinOp::Gt + | BinOp::LtEq + | BinOp::GtEq + | BinOp::Eq + | BinOp::NotEq + | BinOp::StrictEq + | BinOp::StrictNotEq + | BinOp::And + | BinOp::Or + | BinOp::Xor + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight + | BinOp::Spaceship + | BinOp::Concat + ) && expr_is_eir_function_safe(left, support, facts, scope_reads) + && expr_is_eir_function_safe(right, support, facts, scope_reads) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads) + && expr_is_eir_function_safe(then_expr, support, facts, scope_reads) + && expr_is_eir_function_safe(else_expr, support, facts, scope_reads) + } + ExprKind::ShortTernary { value, default } => { + expr_is_eir_function_safe(value, support, facts, scope_reads) + && expr_is_eir_function_safe(default, support, facts, scope_reads) + } + ExprKind::NullCoalesce { value, default } => { + expr_is_eir_function_safe(value, support, facts, scope_reads) + && expr_is_eir_function_safe(default, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } => { + matches!( + target, + CastType::Int | CastType::Float | CastType::String | CastType::Bool + ) && expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::Match { + subject, + arms, + default, + } => { + expr_is_eir_function_safe(subject, support, facts, scope_reads) + && default.as_ref().is_some_and(|default| { + expr_is_eir_function_safe(default, support, facts, scope_reads) + }) + && arms.iter().all(|(conditions, result)| { + conditions.iter().all(|condition| { + expr_is_eir_function_safe(condition, support, facts, scope_reads) + }) && expr_is_eir_function_safe(result, support, facts, scope_reads) + }) + } + ExprKind::ArrayAccess { array, index } => { + expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + && expr_is_eir_function_safe(index, support, facts, scope_reads) + } + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + } + ExprKind::FunctionCall { name, args } => { + eir_call_user_func_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_construct_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_runtime_builtin_call_is_safe( + name.as_str(), + args, + support, + facts, + scope_reads, + ) + || fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args) + .is_some() + || support.function_supported(name.as_str(), args) + } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => support.static_method_supported(receiver, method, args), + _ => false, + } +} + +/// Returns true when a static `call_user_func*()` callback maps to an AOT-safe call. +fn eir_call_user_func_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "call_user_func" => { + let Some((callback, callback_args)) = args.split_first() else { + return false; + }; + static_callback_call_is_eir_safe(callback, callback_args, support, facts, scope_reads) + } + "call_user_func_array" => { + let [callback, arg_array] = args else { + return false; + }; + let Some(callback_args) = static_call_user_func_array_args(arg_array) else { + return false; + }; + static_callback_call_is_eir_safe(callback, &callback_args, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a compile-time callback names a safe function or static method. +fn static_callback_call_is_eir_safe( + callback: &Expr, + callback_args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + if let Some((receiver, method)) = static_callback_static_method_parts(callback) { + return support.static_method_supported(&receiver, method.as_str(), callback_args); + } + static_callback_function_name(callback).is_some_and(|callback_name| { + let short_callback = callback_name.trim_start_matches('\\'); + eir_runtime_builtin_call_is_safe(short_callback, callback_args, support, facts, scope_reads) + || fold_static_builtin_call(short_callback, callback_args).is_some() + || support.function_supported(short_callback, callback_args) + }) +} + +/// Returns the function name from a compile-time callback expression. +fn static_callback_function_name(callback: &Expr) -> Option<&str> { + match &callback.kind { + ExprKind::StringLiteral(name) if !name.contains("::") => Some(name.as_str()), + ExprKind::FirstClassCallable(CallableTarget::Function(name)) => Some(name.as_str()), + _ => None, + } +} + +/// Returns the named receiver and method from a compile-time static-method callback. +fn static_callback_static_method_parts(callback: &Expr) -> Option<(StaticReceiver, String)> { + match &callback.kind { + ExprKind::StringLiteral(name) => static_callback_static_method_string_parts(name), + ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { + Some((receiver.clone(), method.clone())) + } + ExprKind::ArrayLiteral(items) => static_callback_static_method_array_parts(items), + _ => None, + } +} + +/// Splits a literal `Class::method` callback into its receiver and method. +fn static_callback_static_method_string_parts(name: &str) -> Option<(StaticReceiver, String)> { + let (class_name, method) = name.trim_start_matches('\\').rsplit_once("::")?; + let receiver = static_callback_static_method_named_receiver(class_name)?; + if method.is_empty() { + return None; + } + Some((receiver, method.to_string())) +} + +/// Extracts a literal `["Class", "method"]` callback target. +fn static_callback_static_method_array_parts(items: &[Expr]) -> Option<(StaticReceiver, String)> { + let [class_expr, method_expr] = items else { + return None; + }; + let receiver = static_callback_static_method_array_receiver(class_expr)?; + let ExprKind::StringLiteral(method) = &method_expr.kind else { + return None; + }; + if method.is_empty() { + return None; + } + Some((receiver, method.clone())) +} + +/// Returns the static receiver from the class part of a callable array. +fn static_callback_static_method_array_receiver(class_expr: &Expr) -> Option { + match &class_expr.kind { + ExprKind::StringLiteral(class_name) => { + static_callback_static_method_named_receiver(class_name) + } + ExprKind::ClassConstant { + receiver: StaticReceiver::Named(name), + } => Some(StaticReceiver::Named(name.clone())), + _ => None, + } +} + +/// Returns a named static receiver from a literal class name. +fn static_callback_static_method_named_receiver(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if class_name.is_empty() { + return None; + } + Some(StaticReceiver::Named(Name::from(class_name.to_string()))) +} + +/// Converts a static `call_user_func_array()` argument array into callback args. +fn static_call_user_func_array_args(arg_array: &Expr) -> Option> { + match &arg_array.kind { + ExprKind::ArrayLiteral(items) => Some(items.clone()), + ExprKind::ArrayLiteralAssoc(pairs) => { + static_call_user_func_array_assoc_args(pairs.as_slice()) + } + _ => None, + } +} + +/// Converts literal associative callback arrays into positional or named callback args. +fn static_call_user_func_array_assoc_args(pairs: &[(Expr, Expr)]) -> Option> { + let mut args = Vec::with_capacity(pairs.len()); + for (key, value) in pairs { + match &key.kind { + ExprKind::StringLiteral(name) => { + args.push(Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(value.clone()), + }, + value.span, + )); + } + ExprKind::IntLiteral(_) => args.push(value.clone()), + _ => return None, + } + } + Some(args) +} + +/// Returns true when an array source can be materialized inside eval EIR AOT. +fn expr_is_eir_static_array_source_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_literal_source_safe(expr, support, facts, scope_reads) + } + ExprKind::Variable(name) => facts.is_array_local(name), + _ => false, + } +} + +/// Returns true when a literal array expression can be materialized inside eval EIR AOT. +fn expr_is_eir_static_array_literal_source_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::ArrayLiteral(items) => items + .iter() + .all(|item| expr_is_eir_static_array_value_safe(item, support, facts, scope_reads)), + ExprKind::ArrayLiteralAssoc(pairs) => { + expr_is_eir_static_assoc_array_source_safe(pairs, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when an associative array source has statically reconstructable key semantics. +fn expr_is_eir_static_assoc_array_source_safe( + pairs: &[(Expr, Expr)], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut next_auto_key = 0i64; + let mut auto_key_initialized = false; + for (key, value) in pairs { + if static_assoc_key_is_parser_generated(key, value) { + let ExprKind::IntLiteral(generated) = &key.kind else { + return false; + }; + if *generated != next_auto_key { + return false; + } + if !expr_is_eir_static_array_value_safe(value, support, facts, scope_reads) { + return false; + } + advance_static_array_auto_key(&mut next_auto_key, &mut auto_key_initialized); + continue; + } + if !expr_is_eir_static_array_key_safe(key, support, facts, scope_reads) + || !expr_is_eir_static_array_value_safe(value, support, facts, scope_reads) + { + return false; + } + update_static_array_auto_key_from_explicit_key( + key, + &mut next_auto_key, + &mut auto_key_initialized, + ); + } + true +} + +/// Returns true when a static array key can be lowered without eval bridge state. +fn expr_is_eir_static_array_key_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) | ExprKind::Null => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::FloatLiteral(_) if static_integral_float_array_key_value(expr).is_some() => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::Negate(inner) + if matches!(inner.kind, ExprKind::IntLiteral(_)) + || static_integral_float_array_key_value(expr).is_some() => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::StringLiteral(_) => expr_is_eir_function_safe(expr, support, facts, scope_reads), + _ => false, + } +} + +/// Returns true for parser-synthesized integer keys from unkeyed assoc entries. +fn static_assoc_key_is_parser_generated(key: &Expr, value: &Expr) -> bool { + matches!(key.kind, ExprKind::IntLiteral(_)) && key.span == value.span +} + +/// Advances the static array auto-key cursor after an implicit generated key. +fn advance_static_array_auto_key(next_auto_key: &mut i64, auto_key_initialized: &mut bool) { + *next_auto_key = next_auto_key.saturating_add(1); + *auto_key_initialized = true; +} + +/// Updates the static array auto-key cursor from an explicit integer-like key. +fn update_static_array_auto_key_from_explicit_key( + key: &Expr, + next_auto_key: &mut i64, + auto_key_initialized: &mut bool, +) { + if let Some(value) = static_integer_array_key_value(key) { + let candidate = value.saturating_add(1); + if !*auto_key_initialized || candidate > *next_auto_key { + *next_auto_key = candidate; + } + *auto_key_initialized = true; + } +} + +/// Returns the integer value for static keys that affect PHP's next auto key. +fn static_integer_array_key_value(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key), + ExprKind::StringLiteral(value) if is_php_integer_array_key(value) => value.parse().ok(), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg(), + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key), + _ => None, + }, + _ => None, + } +} + +/// Returns the integer key for a float literal that PHP casts without a precision warning. +fn static_integral_float_array_key_value(key: &Expr) -> Option { + let value = match &key.kind { + ExprKind::FloatLiteral(value) => *value, + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::FloatLiteral(value) => -*value, + _ => return None, + }, + _ => return None, + }; + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + if value < i64::MIN as f64 || value >= i64::MAX as f64 { + return None; + } + Some(value as i64) +} + +/// Returns true when a static array value can be lowered without eval bridge state. +fn expr_is_eir_static_array_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Spread(_) => false, + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + } + _ => expr_is_eir_function_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true for language-construct calls that can safely lower through EIR AOT. +fn eir_construct_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + if has_named_args(args) + || args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return false; + } + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "isset" => { + !args.is_empty() + && args + .iter() + .all(|arg| eir_isset_probe_is_safe(arg, support, facts, scope_reads)) + } + "empty" if args.len() == 1 => match &args[0].kind { + ExprKind::Variable(name) => eir_variable_probe_is_safe(name, facts, scope_reads), + _ => expr_is_eir_function_safe(&args[0], support, facts, scope_reads), + }, + _ => false, + } +} + +/// Returns true when an `isset()` operand can lower without evaluating dynamic scope state. +fn eir_isset_probe_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) => eir_variable_probe_is_safe(name, facts, scope_reads), + ExprKind::ArrayAccess { .. } => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a variable probe can use local facts or direct eval read params. +fn eir_variable_probe_is_safe( + name: &str, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) +} + +/// Returns true for builtin calls that the normal EIR backend can lower at runtime. +fn eir_runtime_builtin_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + let short_name = php_symbol_key(name.trim_start_matches('\\')); + let Some(args) = normalize_eir_runtime_builtin_args(&short_name, args) else { + return false; + }; + match short_name.as_str() { + "boolval" if args.len() == 1 => { + eir_boolval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "array_key_exists" if args.len() == 2 => { + eir_array_key_exists_args_are_safe(&args[0], &args[1], support, facts, scope_reads) + } + "count" if (1..=2).contains(&args.len()) => { + eir_count_mode_is_default_zero(args.get(1)) + && eir_count_arg_is_safe(&args[0], support, facts, scope_reads) + } + "floatval" if args.len() == 1 => { + eir_floatval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "gettype" if args.len() == 1 => { + eir_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "intval" if args.len() == 1 => { + eir_intval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_array" if args.len() == 1 => { + eir_array_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_iterable" if args.len() == 1 => { + eir_array_like_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_object" if args.len() == 1 => { + eir_object_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_numeric" | "is_resource" if args.len() == 1 => { + eir_scalar_cast_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_finite" | "is_infinite" | "is_nan" if args.len() == 1 => { + eir_float_predicate_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" + | "is_real" | "is_scalar" | "is_string" + if args.len() == 1 => + { + eir_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "strval" if args.len() == 1 => { + eir_strval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "strlen" if args.len() == 1 => { + eir_strlen_arg_is_safe(&args[0], support, facts, scope_reads) + } + _ => false, + } +} + +/// Normalizes EIR-safe builtin call arguments for eval AOT gating. +/// +/// Static spread arrays are expanded through the shared call planner; dynamic +/// spreads that remain after planning stay on the eval bridge fallback. +fn normalize_eir_runtime_builtin_args(short_name: &str, args: &[Expr]) -> Option> { + let has_spread = args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))); + if !has_named_args(args) && !has_spread { + return Some(args.to_vec()); + } + let sig = builtin_call_sig(short_name)?; + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(&sig, args, call_span, false, false).ok()?; + if plan.has_spread_args() { + return None; + } + Some(plan.normalized_args()) +} + +/// Returns true when `array_key_exists()` can lower through EIR without eval bridge state. +fn eir_array_key_exists_args_are_safe( + key: &Expr, + array: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + if !eir_array_key_exists_static_key_is_safe(key) { + return false; + } + match &array.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + } + ExprKind::ArrayLiteral(_) => { + !eir_array_key_exists_static_key_needs_assoc_array(key) + && expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + } + ExprKind::Variable(name) => { + !eir_array_key_exists_static_key_needs_assoc_array(key) && facts.is_array_local(name) + } + _ => false, + } +} + +/// Returns true when the key type has target-aware lowering for mixed array probes. +fn eir_array_key_exists_static_key_is_safe(key: &Expr) -> bool { + match &key.kind { + ExprKind::IntLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::Null => true, + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key).is_some(), + ExprKind::Negate(inner) => { + matches!(inner.kind, ExprKind::IntLiteral(_)) + || static_integral_float_array_key_value(key).is_some() + } + _ => false, + } +} + +/// Returns true when the static key only has safe mixed-array semantics for hashes. +/// +/// String keys can now probe indexed arrays too: numeric strings normalize to an +/// integer bounds check and non-integer strings return false on indexed arrays. +fn eir_array_key_exists_static_key_needs_assoc_array(key: &Expr) -> bool { + matches!(key.kind, ExprKind::Null) +} + +/// Returns true when `count()` uses PHP's default non-recursive mode. +fn eir_count_mode_is_default_zero(mode: Option<&Expr>) -> bool { + match mode { + None => true, + Some(expr) => matches!(expr.kind, ExprKind::IntLiteral(0)), + } +} + +/// Returns true when a value can reach `count()` as a concrete EIR array. +fn eir_count_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + _ => expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true when a value can reach `boolval()` through an EIR-supported scalar path. +fn eir_boolval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `floatval()` through an EIR-supported scalar path. +fn eir_floatval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `intval()` through an EIR-supported scalar path. +fn eir_intval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `gettype()`/`is_*()` through EIR-safe probes. +fn eir_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach array-like type probes through safe EIR paths. +fn eir_array_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + || eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `is_iterable()` through currently safe EIR paths. +fn eir_array_like_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + || eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `is_object()` through currently safe EIR paths. +fn eir_object_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) + || expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach IEEE float predicates without PHP coercion surprises. +fn eir_float_predicate_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) | ExprKind::BoolLiteral(_) => true, + ExprKind::Variable(name) => { + scope_reads.is_some_and(|reads| reads.contains(name)) + || facts.is_int_local(name) + || facts.is_float_local(name) + } + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + eir_float_predicate_arg_is_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } + if matches!(target, CastType::Int | CastType::Float | CastType::Bool) => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a value can reach `strval()` through an EIR-supported scalar path. +fn eir_strval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when an expression is scalar-like enough for EIR cast builtins. +fn eir_scalar_cast_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::Null => true, + ExprKind::Variable(name) => { + scope_reads.is_some_and(|reads| reads.contains(name)) + || (facts.is_assigned(name) && !facts.is_array_local(name)) + } + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + eir_scalar_cast_arg_is_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } + if matches!( + target, + CastType::Int | CastType::Float | CastType::String | CastType::Bool + ) => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::ArrayAccess { .. } => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::FunctionCall { name, args } => { + eir_call_user_func_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_runtime_builtin_call_is_safe( + name.as_str(), + args, + support, + facts, + scope_reads, + ) + || fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args) + .is_some() + || support.function_supported(name.as_str(), args) + } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => support.static_method_supported(receiver, method, args), + _ => false, + } +} + +/// Returns true when a value can reach `strlen()` as `Str` or boxed `Mixed`. +fn eir_strlen_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => true, + ExprKind::Variable(name) => { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) + } + ExprKind::Cast { target, expr } if matches!(target, CastType::String) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when an expression is known to produce an integer in the EIR AOT subset. +fn expr_is_eir_int_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) => true, + ExprKind::Variable(name) => facts.is_int_local(name), + ExprKind::Negate(inner) | ExprKind::BitNot(inner) | ExprKind::ErrorSuppress(inner) => { + expr_is_eir_int_value_safe(inner, support, facts, scope_reads) + } + ExprKind::Print(inner) => expr_is_eir_function_safe(inner, support, facts, scope_reads), + ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => facts.is_int_local(name), + ExprKind::Cast { target, expr } if matches!(target, CastType::Int) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::BinaryOp { left, op, right } => { + let int_operands = expr_is_eir_int_value_safe(left, support, facts, scope_reads) + && expr_is_eir_int_value_safe(right, support, facts, scope_reads); + match op { + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Mod + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight => int_operands, + BinOp::Spaceship => { + expr_is_eir_function_safe(left, support, facts, scope_reads) + && expr_is_eir_function_safe(right, support, facts, scope_reads) + } + _ => false, + } + } + ExprKind::FunctionCall { name, args } => { + fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args).is_some() + } + _ => false, + } +} + +/// Returns true when an expression is known to produce a float in the EIR AOT subset. +fn expr_is_eir_float_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::FloatLiteral(_) => true, + ExprKind::Variable(name) => facts.is_float_local(name), + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + expr_is_eir_float_value_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } if matches!(target, CastType::Float) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Rewrites foldable static builtin calls in a program to integer literals. +fn fold_static_builtin_calls_in_program(program: Program) -> Program { + program + .into_iter() + .map(fold_static_builtin_calls_in_stmt) + .collect() +} + +/// Rewrites foldable static builtin calls inside one statement. +fn fold_static_builtin_calls_in_stmt(stmt: Stmt) -> Stmt { + let kind = match stmt.kind { + StmtKind::Echo(expr) => StmtKind::Echo(fold_static_builtin_calls_in_expr(expr)), + StmtKind::Assign { name, value } => StmtKind::Assign { + name, + value: fold_static_builtin_calls_in_expr(value), + }, + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => StmtKind::If { + condition: fold_static_builtin_calls_in_expr(condition), + then_body: fold_static_builtin_calls_in_program(then_body), + elseif_clauses: elseif_clauses + .into_iter() + .map(|(condition, body)| { + ( + fold_static_builtin_calls_in_expr(condition), + fold_static_builtin_calls_in_program(body), + ) + }) + .collect(), + else_body: else_body.map(fold_static_builtin_calls_in_program), + }, + StmtKind::While { condition, body } => StmtKind::While { + condition: fold_static_builtin_calls_in_expr(condition), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::DoWhile { condition, body } => StmtKind::DoWhile { + condition: fold_static_builtin_calls_in_expr(condition), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::For { + init, + condition, + update, + body, + } => StmtKind::For { + init: init.map(|stmt| Box::new(fold_static_builtin_calls_in_stmt(*stmt))), + condition: condition.map(fold_static_builtin_calls_in_expr), + update: update.map(|stmt| Box::new(fold_static_builtin_calls_in_stmt(*stmt))), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::Switch { + subject, + cases, + default, + } => StmtKind::Switch { + subject: fold_static_builtin_calls_in_expr(subject), + cases: cases + .into_iter() + .map(|(conditions, body)| { + ( + conditions + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + fold_static_builtin_calls_in_program(body), + ) + }) + .collect(), + default: default.map(fold_static_builtin_calls_in_program), + }, + StmtKind::Return(Some(expr)) => { + StmtKind::Return(Some(fold_static_builtin_calls_in_expr(expr))) + } + StmtKind::ExprStmt(expr) => StmtKind::ExprStmt(fold_static_builtin_calls_in_expr(expr)), + other => other, + }; + Stmt { + kind, + span: stmt.span, + attributes: stmt.attributes, + } +} + +/// Rewrites foldable static builtin calls inside one expression. +fn fold_static_builtin_calls_in_expr(expr: Expr) -> Expr { + let span = expr.span; + let kind = match expr.kind { + ExprKind::Negate(inner) => { + ExprKind::Negate(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::Not(inner) => ExprKind::Not(Box::new(fold_static_builtin_calls_in_expr(*inner))), + ExprKind::BitNot(inner) => { + ExprKind::BitNot(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::Print(inner) => { + ExprKind::Print(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::BinaryOp { left, op, right } => ExprKind::BinaryOp { + left: Box::new(fold_static_builtin_calls_in_expr(*left)), + op, + right: Box::new(fold_static_builtin_calls_in_expr(*right)), + }, + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => ExprKind::Ternary { + condition: Box::new(fold_static_builtin_calls_in_expr(*condition)), + then_expr: Box::new(fold_static_builtin_calls_in_expr(*then_expr)), + else_expr: Box::new(fold_static_builtin_calls_in_expr(*else_expr)), + }, + ExprKind::ShortTernary { value, default } => ExprKind::ShortTernary { + value: Box::new(fold_static_builtin_calls_in_expr(*value)), + default: Box::new(fold_static_builtin_calls_in_expr(*default)), + }, + ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { + value: Box::new(fold_static_builtin_calls_in_expr(*value)), + default: Box::new(fold_static_builtin_calls_in_expr(*default)), + }, + ExprKind::Cast { target, expr } => ExprKind::Cast { + target, + expr: Box::new(fold_static_builtin_calls_in_expr(*expr)), + }, + ExprKind::Match { + subject, + arms, + default, + } => ExprKind::Match { + subject: Box::new(fold_static_builtin_calls_in_expr(*subject)), + arms: arms + .into_iter() + .map(|(conditions, result)| { + ( + conditions + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + fold_static_builtin_calls_in_expr(result), + ) + }) + .collect(), + default: default.map(|expr| Box::new(fold_static_builtin_calls_in_expr(*expr))), + }, + ExprKind::ArrayLiteral(items) => ExprKind::ArrayLiteral( + items + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + ), + ExprKind::ArrayLiteralAssoc(pairs) => ExprKind::ArrayLiteralAssoc( + pairs + .into_iter() + .map(|(key, value)| { + ( + fold_static_builtin_calls_in_expr(key), + fold_static_builtin_calls_in_expr(value), + ) + }) + .collect(), + ), + ExprKind::FunctionCall { name, args } => { + let folded_args = args + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect::>(); + if let Some(kind) = fold_static_call_user_func_call( + name.as_str().trim_start_matches('\\'), + &folded_args, + ) { + kind + } else if let Some(kind) = + fold_static_builtin_call(name.as_str().trim_start_matches('\\'), &folded_args) + { + kind + } else { + ExprKind::FunctionCall { + name, + args: folded_args, + } + } + } + other => other, + }; + Expr { kind, span } +} + +/// Folds `call_user_func*()` when the callback is a pure foldable builtin. +fn fold_static_call_user_func_call(short_name: &str, args: &[Expr]) -> Option { + match php_symbol_key(short_name).as_str() { + "call_user_func" => { + let (callback, callback_args) = args.split_first()?; + fold_static_callback_call(callback, callback_args) + } + "call_user_func_array" => { + let [callback, arg_array] = args else { + return None; + }; + let callback_args = static_call_user_func_array_args(arg_array)?; + fold_static_callback_call(callback, &callback_args) + } + _ => None, + } +} + +/// Folds one static string callback when it names a pure foldable builtin. +fn fold_static_callback_call(callback: &Expr, callback_args: &[Expr]) -> Option { + let ExprKind::StringLiteral(callback_name) = &callback.kind else { + return None; + }; + if callback_name.contains("::") { + return None; + } + fold_static_builtin_call(callback_name.trim_start_matches('\\'), callback_args) +} + +/// Checks one statement for the native-only literal eval runtime-feature subset. +fn stmt_is_native_only_scalar(stmt: &Stmt, static_call_supported: &F) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Echo(expr) => { + echo_expr_is_native_only_scalar(expr, static_call_supported).then_some(false) + } + StmtKind::Return(Some(expr)) => { + value_expr_is_native_only_scalar(expr, static_call_supported).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + echo_expr_is_native_only_scalar(inner, static_call_supported).then_some(false) + } + _ => value_expr_is_native_only_scalar(expr, static_call_supported).then_some(false), + }, + _ => None, + } +} + +/// Checks an echo expression for the native-only literal eval runtime-feature subset. +fn echo_expr_is_native_only_scalar(expr: &Expr, static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => true, + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + echo_expr_is_native_only_scalar(left, static_call_supported) + && echo_expr_is_native_only_scalar(right, static_call_supported) + } + _ => value_expr_is_native_only_scalar(expr, static_call_supported), + } +} + +/// Checks a value expression for the native-only literal eval runtime-feature subset. +fn value_expr_is_native_only_scalar(expr: &Expr, static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) => true, + ExprKind::Negate(inner) => value_expr_is_native_only_scalar(inner, static_call_supported), + ExprKind::Not(inner) => value_expr_is_native_only_scalar(inner, static_call_supported), + ExprKind::BinaryOp { left, op, right } => { + matches!( + op, + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Mod + | BinOp::Lt + | BinOp::Gt + | BinOp::LtEq + | BinOp::GtEq + | BinOp::Eq + | BinOp::NotEq + | BinOp::And + | BinOp::Or + ) && value_expr_is_native_only_scalar(left, static_call_supported) + && value_expr_is_native_only_scalar(right, static_call_supported) + } + ExprKind::FunctionCall { name, args } => { + let short_name = name.as_str().trim_start_matches('\\'); + fold_static_builtin_int_call(short_name, args).is_some() + || static_call_supported(name.as_str(), args) + } + _ => false, + } +} + +/// Folds pure static builtin calls whose integer result is fully known at compile time. +pub(crate) fn fold_static_builtin_int_call(short_name: &str, args: &[Expr]) -> Option { + let ExprKind::IntLiteral(value) = fold_static_builtin_call(short_name, args)? else { + return None; + }; + Some(value) +} + +/// Folds pure static builtin calls whose scalar result is fully known at compile time. +fn fold_static_builtin_call(short_name: &str, args: &[Expr]) -> Option { + let name = php_symbol_key(short_name); + let normalized_args = normalize_static_builtin_args(&name, args)?; + let args = normalized_args.as_slice(); + match name.as_str() { + name if name == "strlen" => fold_strlen(args).map(ExprKind::IntLiteral), + name if name == "intval" => fold_intval(args).map(ExprKind::IntLiteral), + name if name == "floatval" => fold_floatval(args).map(ExprKind::FloatLiteral), + name if name == "strval" => fold_strval(args).map(ExprKind::StringLiteral), + name if name == "boolval" => fold_boolval(args).map(ExprKind::BoolLiteral), + name if name == "is_int" || name == "is_integer" || name == "is_long" => { + fold_type_probe(args, LiteralTypeProbe::Int).map(ExprKind::BoolLiteral) + } + name if name == "is_string" => { + fold_type_probe(args, LiteralTypeProbe::String).map(ExprKind::BoolLiteral) + } + name if name == "is_bool" => { + fold_type_probe(args, LiteralTypeProbe::Bool).map(ExprKind::BoolLiteral) + } + name if name == "is_float" || name == "is_double" || name == "is_real" => { + fold_type_probe(args, LiteralTypeProbe::Float).map(ExprKind::BoolLiteral) + } + name if name == "is_null" => { + fold_type_probe(args, LiteralTypeProbe::Null).map(ExprKind::BoolLiteral) + } + name if name == "is_scalar" => fold_is_scalar(args).map(ExprKind::BoolLiteral), + name if name == "gettype" => fold_gettype(args).map(ExprKind::StringLiteral), + name if name == "abs" => fold_abs(args).map(ExprKind::IntLiteral), + name if name == "count" => fold_count(args).map(ExprKind::IntLiteral), + name if name == "array_key_exists" => { + fold_array_key_exists(args).map(ExprKind::BoolLiteral) + } + name if name == "floor" => fold_floor(args).map(ExprKind::FloatLiteral), + name if name == "ceil" => fold_ceil(args).map(ExprKind::FloatLiteral), + name if name == "sqrt" => fold_sqrt(args).map(ExprKind::FloatLiteral), + name if name == "round" => fold_round(args).map(ExprKind::FloatLiteral), + name if name == "ord" => fold_ord(args).map(ExprKind::IntLiteral), + name if name == "chr" => fold_chr(args).map(ExprKind::StringLiteral), + name if name == "min" => fold_min(args).map(ExprKind::IntLiteral), + name if name == "max" => fold_max(args).map(ExprKind::IntLiteral), + name if name == "strtolower" => { + fold_ascii_case(args, AsciiCaseFold::Lower).map(ExprKind::StringLiteral) + } + name if name == "strtoupper" => { + fold_ascii_case(args, AsciiCaseFold::Upper).map(ExprKind::StringLiteral) + } + name if name == "ucfirst" => { + fold_ascii_first_char_case(args, FirstCharCaseFold::Upper).map(ExprKind::StringLiteral) + } + name if name == "lcfirst" => { + fold_ascii_first_char_case(args, FirstCharCaseFold::Lower).map(ExprKind::StringLiteral) + } + name if name == "strrev" => fold_ascii_strrev(args).map(ExprKind::StringLiteral), + name if name == "substr" => fold_ascii_substr(args).map(ExprKind::StringLiteral), + name if name == "str_repeat" => fold_ascii_str_repeat(args).map(ExprKind::StringLiteral), + name if name == "trim" => { + fold_ascii_default_trim(args, TrimSide::Both).map(ExprKind::StringLiteral) + } + name if name == "ltrim" => { + fold_ascii_default_trim(args, TrimSide::Left).map(ExprKind::StringLiteral) + } + name if name == "rtrim" || name == "chop" => { + fold_ascii_default_trim(args, TrimSide::Right).map(ExprKind::StringLiteral) + } + name if name == "str_contains" => { + fold_ascii_string_predicate(args, StringPredicate::Contains).map(ExprKind::BoolLiteral) + } + name if name == "str_starts_with" => { + fold_ascii_string_predicate(args, StringPredicate::StartsWith) + .map(ExprKind::BoolLiteral) + } + name if name == "str_ends_with" => { + fold_ascii_string_predicate(args, StringPredicate::EndsWith).map(ExprKind::BoolLiteral) + } + _ => None, + } +} + +/// Normalizes named/static-spread builtin arguments before attempting a static fold. +fn normalize_static_builtin_args(short_name: &str, args: &[Expr]) -> Option> { + let sig = builtin_call_sig(short_name)?; + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(&sig, args, call_span, true, false).ok()?; + Some(plan.normalized_args()) +} + +/// Folds `strlen("literal")` to an integer result. +fn fold_strlen(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + i64::try_from(value.len()).ok() +} + +/// Folds `intval()` for literal scalar inputs whose PHP result is unambiguous here. +fn fold_intval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::StringLiteral(value) => value.trim().parse::().ok(), + _ => None, + } +} + +/// Folds `floatval()` for literal scalar inputs whose PHP result is unambiguous here. +fn fold_floatval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = match &args[0].kind { + ExprKind::IntLiteral(value) => *value as f64, + ExprKind::FloatLiteral(value) => *value, + ExprKind::BoolLiteral(value) => f64::from(u8::from(*value)), + ExprKind::StringLiteral(value) => value.trim().parse::().ok()?, + ExprKind::Null => 0.0, + _ => return None, + }; + value.is_finite().then_some(value) +} + +/// Folds `strval()` for literal scalar inputs with stable PHP string results. +fn fold_strval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(value.to_string()), + ExprKind::BoolLiteral(true) => Some("1".to_string()), + ExprKind::BoolLiteral(false) | ExprKind::Null => Some(String::new()), + ExprKind::StringLiteral(value) => Some(value.clone()), + _ => None, + } +} + +/// Folds `boolval()` for literal scalar inputs whose PHP truthiness is clear. +fn fold_boolval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(*value != 0), + ExprKind::BoolLiteral(value) => Some(*value), + ExprKind::StringLiteral(value) => Some(!(value.is_empty() || value == "0")), + ExprKind::Null => Some(false), + _ => None, + } +} + +/// Literal scalar type checked by pure `is_*` builtin folds. +enum LiteralTypeProbe { + Int, + String, + Bool, + Float, + Null, +} + +/// Folds pure `is_*` type probes for literal scalar inputs. +fn fold_type_probe(args: &[Expr], probe: LiteralTypeProbe) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Int)), + ExprKind::StringLiteral(_) => Some(matches!(probe, LiteralTypeProbe::String)), + ExprKind::BoolLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Bool)), + ExprKind::FloatLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Float)), + ExprKind::Null => Some(matches!(probe, LiteralTypeProbe::Null)), + _ => None, + } +} + +/// Folds `is_scalar()` for literal scalar and null inputs. +fn fold_is_scalar(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::FloatLiteral(_) => Some(true), + ExprKind::Null => Some(false), + _ => None, + } +} + +/// Folds `gettype()` for literal scalar and null inputs with stable PHP spellings. +fn fold_gettype(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ty = match &args[0].kind { + ExprKind::IntLiteral(_) => "integer", + ExprKind::FloatLiteral(_) => "double", + ExprKind::StringLiteral(_) => "string", + ExprKind::BoolLiteral(_) => "boolean", + ExprKind::Null => "NULL", + _ => return None, + }; + Some(ty.to_string()) +} + +/// Folds `abs()` for integer literals that stay representable as `int`. +fn fold_abs(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + const_int_expr(&args[0])?.checked_abs() +} + +/// Folds `count()` for static array literals whose element expressions have no side effects. +fn fold_count(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + i64::try_from(static_array_key_ids(&args[0])?.len()).ok() +} + +/// Folds `array_key_exists()` for static array literals and static scalar keys. +fn fold_array_key_exists(args: &[Expr]) -> Option { + if args.len() != 2 { + return None; + } + let key = static_array_key_fold_id(&args[0])?; + Some(static_array_key_ids(&args[1])?.contains(&key)) +} + +/// Returns normalized key identifiers for a static array literal. +fn static_array_key_ids(expr: &Expr) -> Option> { + match &expr.kind { + ExprKind::ArrayLiteral(items) => { + if !items.iter().all(static_array_value_is_fold_safe) { + return None; + } + (0..items.len()) + .map(|index| Some(format!("i:{index}"))) + .collect() + } + ExprKind::ArrayLiteralAssoc(pairs) => { + let mut keys = BTreeSet::new(); + for (key, value) in pairs { + if !static_array_value_is_fold_safe(value) { + return None; + } + keys.insert(static_array_key_fold_id(key)?); + } + Some(keys) + } + _ => None, + } +} + +/// Returns true when evaluating this expression while building a static array has no side effects. +fn static_array_value_is_fold_safe(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => true, + ExprKind::Negate(inner) | ExprKind::Not(inner) | ExprKind::BitNot(inner) => { + static_array_value_is_fold_safe(inner) + } + ExprKind::ArrayLiteral(items) => items.iter().all(static_array_value_is_fold_safe), + ExprKind::ArrayLiteralAssoc(pairs) => pairs.iter().all(|(key, value)| { + static_array_key_fold_id(key).is_some() && static_array_value_is_fold_safe(value) + }), + _ => false, + } +} + +/// Returns a normalized key identifier for static array-literal count folding. +fn static_array_key_fold_id(expr: &Expr) -> Option { + if let Some(value) = static_integer_array_key_value(expr) { + return Some(format!("i:{value}")); + } + match &expr.kind { + ExprKind::Null => Some("s:".to_string()), + ExprKind::StringLiteral(value) => Some(format!("s:{value}")), + _ => None, + } +} + +/// Folds `floor()` for finite numeric literals. +fn fold_floor(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::floor) +} + +/// Folds `ceil()` for finite numeric literals. +fn fold_ceil(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::ceil) +} + +/// Folds `sqrt()` for non-negative finite numeric literals. +fn fold_sqrt(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = const_finite_numeric_expr(&args[0])?; + (value >= 0.0) + .then(|| value.sqrt()) + .filter(|sqrt| sqrt.is_finite()) +} + +/// Folds one-argument `round()` for finite numeric literals. +fn fold_round(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::round) +} + +/// Applies a finite `f64` builtin fold to one numeric literal argument. +fn fold_finite_numeric_unary(args: &[Expr], fold: fn(f64) -> f64) -> Option { + if args.len() != 1 { + return None; + } + let value = fold(const_finite_numeric_expr(&args[0])?); + value.is_finite().then_some(value) +} + +/// Folds `ord()` for literal strings by returning the first byte value. +fn fold_ord(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + value.as_bytes().first().map(|byte| i64::from(*byte)) +} + +/// Folds `chr()` for ASCII byte values representable by the AST string type. +fn fold_chr(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = const_int_expr(&args[0])?; + let byte = u8::try_from(value).ok()?; + if !byte.is_ascii() { + return None; + } + Some(char::from(byte).to_string()) +} + +/// Folds `min()` over integer literal arguments. +fn fold_min(args: &[Expr]) -> Option { + fold_int_values(args)?.into_iter().min() +} + +/// Folds `max()` over integer literal arguments. +fn fold_max(args: &[Expr]) -> Option { + fold_int_values(args)?.into_iter().max() +} + +/// Collects integer literal arguments for variadic pure numeric folds. +fn fold_int_values(args: &[Expr]) -> Option> { + if args.is_empty() { + return None; + } + args.iter().map(const_int_expr).collect() +} + +/// ASCII-only case conversion supported by literal eval builtin folding. +enum AsciiCaseFold { + Lower, + Upper, +} + +/// First-byte ASCII case conversion supported by literal eval builtin folding. +enum FirstCharCaseFold { + Lower, + Upper, +} + +/// Side selected by default-mask ASCII trim folding. +enum TrimSide { + Left, + Right, + Both, +} + +/// Two-string ASCII predicates supported by literal eval builtin folding. +enum StringPredicate { + Contains, + StartsWith, + EndsWith, +} + +/// Folds ASCII-only `strtolower()` and `strtoupper()` literal calls. +fn fold_ascii_case(args: &[Expr], mode: AsciiCaseFold) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let folded = match mode { + AsciiCaseFold::Lower => value.to_ascii_lowercase(), + AsciiCaseFold::Upper => value.to_ascii_uppercase(), + }; + Some(folded) +} + +/// Folds ASCII-only `ucfirst()` and `lcfirst()` literal calls. +fn fold_ascii_first_char_case(args: &[Expr], mode: FirstCharCaseFold) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let mut bytes = value.as_bytes().to_vec(); + if let Some(first) = bytes.first_mut() { + match mode { + FirstCharCaseFold::Lower => first.make_ascii_lowercase(), + FirstCharCaseFold::Upper => first.make_ascii_uppercase(), + } + } + String::from_utf8(bytes).ok() +} + +/// Folds ASCII-only `strrev()` literal calls with PHP byte-order behavior. +fn fold_ascii_strrev(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + Some(value.bytes().rev().map(char::from).collect()) +} + +/// Folds ASCII-only `substr()` literal calls with non-negative offset and length. +fn fold_ascii_substr(args: &[Expr]) -> Option { + if !(2..=3).contains(&args.len()) { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let offset = usize::try_from(const_int_expr(&args[1])?).ok()?; + let start = offset.min(value.len()); + let end = if let Some(length_arg) = args.get(2) { + let length = usize::try_from(const_int_expr(length_arg)?).ok()?; + start.saturating_add(length).min(value.len()) + } else { + value.len() + }; + Some(value[start..end].to_string()) +} + +/// Folds ASCII-only `str_repeat()` literal calls with a bounded static result. +fn fold_ascii_str_repeat(args: &[Expr]) -> Option { + if args.len() != 2 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let times = usize::try_from(const_int_expr(&args[1])?).ok()?; + let bytes = value.len().checked_mul(times)?; + if bytes > MAX_STATIC_STRING_FOLD_BYTES { + return None; + } + Some(value.repeat(times)) +} + +/// Folds one-argument ASCII `trim()`/`ltrim()`/`rtrim()` calls using PHP's default mask. +fn fold_ascii_default_trim(args: &[Expr], side: TrimSide) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let trimmed = match side { + TrimSide::Left => value.trim_start_matches(is_php_default_trim_char), + TrimSide::Right => value.trim_end_matches(is_php_default_trim_char), + TrimSide::Both => value.trim_matches(is_php_default_trim_char), + }; + Some(trimmed.to_string()) +} + +/// Returns true for characters removed by PHP's default trim character mask. +fn is_php_default_trim_char(ch: char) -> bool { + matches!(ch, '\0' | '\t' | '\n' | '\r' | '\x0b' | ' ') +} + +/// Folds ASCII-only two-string predicate calls to their boolean result. +fn fold_ascii_string_predicate(args: &[Expr], predicate: StringPredicate) -> Option { + if args.len() != 2 { + return None; + } + let (ExprKind::StringLiteral(haystack), ExprKind::StringLiteral(needle)) = + (&args[0].kind, &args[1].kind) + else { + return None; + }; + if !haystack.is_ascii() || !needle.is_ascii() { + return None; + } + Some(match predicate { + StringPredicate::Contains => haystack.contains(needle), + StringPredicate::StartsWith => haystack.starts_with(needle), + StringPredicate::EndsWith => haystack.ends_with(needle), + }) +} + +/// Evaluates integer-only literal expressions recognized by eval AOT analysis. +pub(crate) fn const_int_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::Negate(inner) => const_int_expr(inner)?.checked_neg(), + _ => None, + } +} + +/// Evaluates finite numeric literal expressions recognized by eval AOT analysis. +fn const_finite_numeric_expr(expr: &Expr) -> Option { + const MAX_EXACT_F64_INT: i64 = 9_007_199_254_740_992; + let value = match &expr.kind { + ExprKind::IntLiteral(value) if (-MAX_EXACT_F64_INT..=MAX_EXACT_F64_INT).contains(value) => { + *value as f64 + } + ExprKind::FloatLiteral(value) => *value, + ExprKind::Negate(inner) => -const_finite_numeric_expr(inner)?, + _ => return None, + }; + value.is_finite().then_some(value) +} + +/// Checks a user-function signature against the native-only eval call subset. +pub(crate) fn static_function_signature_supported(signature: &FunctionSig, args: &[Expr]) -> bool { + if !signature.declared_return + || signature.declared_params.iter().any(|declared| !declared) + || signature.ref_params.len() != signature.params.len() + || signature.variadic.is_some() + || !static_function_return_type_supported(&signature.return_type) + { + return false; + } + let Some(args) = normalize_static_function_args(signature, args) else { + return false; + }; + signature.params.len() == args.len() + && signature + .params + .iter() + .zip(signature.ref_params.iter().copied()) + .zip(args.iter()) + .all(|((param, by_ref), arg)| !by_ref && static_function_arg_supported(¶m.1, arg)) +} + +/// Normalizes user-function arguments for eval AOT eligibility checks. +/// +/// Static spread arrays are expanded through the shared call planner; dynamic +/// spreads that remain after planning stay on the eval bridge fallback. +fn normalize_static_function_args(signature: &FunctionSig, args: &[Expr]) -> Option> { + if !crate::types::call_args::has_named_args(args) + && !args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return normalize_positional_static_function_args(signature, args); + } + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(signature, args, call_span, false, false).ok()?; + if plan.has_spread_args() { + return None; + } + Some(plan.normalized_args()) +} + +/// Appends scalar default values for positional static user-function calls. +fn normalize_positional_static_function_args( + signature: &FunctionSig, + args: &[Expr], +) -> Option> { + if args.len() > signature.params.len() { + return None; + } + let mut normalized = args.to_vec(); + for idx in args.len()..signature.params.len() { + let default = signature.defaults.get(idx)?.clone()?; + normalized.push(default); + } + Some(normalized) +} + +/// Returns true when a user function return can be boxed by eval EIR AOT. +fn static_function_return_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) +} + +/// Returns true when a literal argument matches the supported scalar parameter type. +fn static_function_arg_supported(param_ty: &PhpType, arg: &Expr) -> bool { + matches!( + (param_ty.codegen_repr(), &arg.kind), + (PhpType::Int, ExprKind::IntLiteral(_)) + | (PhpType::Bool, ExprKind::BoolLiteral(_)) + | (PhpType::Float, ExprKind::FloatLiteral(_)) + | (PhpType::Str, ExprKind::StringLiteral(_)) + ) +} diff --git a/src/image_prelude/detect.rs b/src/image_prelude/detect.rs index a3dd2163a8..ef2fa5d95c 100644 --- a/src/image_prelude/detect.rs +++ b/src/image_prelude/detect.rs @@ -248,6 +248,7 @@ fn expr_refs_image(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -340,6 +341,11 @@ fn expr_refs_image(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_image(object) || args.iter().any(expr_refs_image) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_image(object) || expr_refs_image(method) || args.iter().any(expr_refs_image), ExprKind::StaticMethodCall { receiver, args, .. } => { receiver_refs_image(receiver) || args.iter().any(expr_refs_image) } diff --git a/src/ir/function.rs b/src/ir/function.rs index 6b3929263e..6ca48f1c99 100644 --- a/src/ir/function.rs +++ b/src/ir/function.rs @@ -14,7 +14,7 @@ use crate::ir::instr::{InstId, Instruction}; use crate::ir::types::IrType; use crate::ir::value::{Value, ValueId}; use crate::parser::ast::Stmt; -use crate::types::{FunctionSig, PhpType}; +use crate::types::{AttrArgEntry, FunctionSig, PhpType}; /// Module-local identifier for an EIR function. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -63,6 +63,8 @@ pub struct Function { pub entry: BlockId, pub source_signature: Option, pub signature: Option, + pub attribute_names: Vec, + pub attribute_args: Vec>>, pub generator_source: Option, pub flags: FunctionFlags, } @@ -83,6 +85,8 @@ impl Function { entry: BlockId::from_raw(0), source_signature: None, signature: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), generator_source: None, flags: FunctionFlags::default(), } @@ -179,6 +183,9 @@ pub enum LocalKind { NamedArgTemp, IteratorState, GeneratorState, + EvalContext, + EvalScope, + EvalGlobalScope, } /// Function-level shape flags used by lowering and later codegen. diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 389ff8e84a..c0ee10211b 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -112,18 +112,36 @@ pub enum Immediate { Bool(bool), Data(DataId), LocalSlot(LocalSlotId), - LocalSlotPair { first: LocalSlotId, second: LocalSlotId }, + LocalSlotPair { + first: LocalSlotId, + second: LocalSlotId, + }, GlobalName(DataId), FunctionRef(FunctionId), BuiltinRef(BuiltinId), RuntimeRef(RuntimeId), ExternRef(u32), ClassRef(u32), - EnumCaseRef { enum_id: u32, case_id: u32 }, - MethodRef { class: u32, method: u32 }, - PropertyRef { class: u32, property: u32 }, - FieldRef { layout: u32, field: u32 }, - FunctionVariantRef { group: u32, variant: u32 }, + EnumCaseRef { + enum_id: u32, + case_id: u32, + }, + MethodRef { + class: u32, + method: u32, + }, + PropertyRef { + class: u32, + property: u32, + }, + FieldRef { + layout: u32, + field: u32, + }, + FunctionVariantRef { + group: u32, + variant: u32, + }, HeapKind(IrHeapKind), MixedTag(u8), MixedNumericOp(MixedNumericOp), @@ -203,6 +221,9 @@ pub enum Op { InitStaticLocal, LoadStaticProperty, StoreStaticProperty, + LoadReflectionStaticProperty, + StoreReflectionStaticProperty, + ReflectionStaticPropertyInitialized, IAdd, ISub, IMul, @@ -310,9 +331,13 @@ pub enum Op { IteratorMethodCall, SplRuntimeCall, ObjectNew, + EvalObjectNew, + ObjectCloneShallow, DynamicObjectNew, DynamicObjectNewMixed, + DynamicObjectNewWithoutConstructorMixed, PropGet, + PropInitialized, PropSet, /// Loads the raw reference-cell pointer stored in a reference property's slot, /// without dereferencing it. Used to alias a local to `$obj->prop` and to return @@ -334,6 +359,7 @@ pub enum Op { MethodLookup, MethodCall, StaticMethodCall, + EvalStaticMethodCall, /// Coerces a PHP numeric string operand to its integer value for an int-backed enum /// `from()`/`tryFrom()` call. Operand: the string. Immediate: data id of the PHP /// `TypeError` message thrown when the string is not numeric. Result: `I64`. @@ -355,6 +381,15 @@ pub enum Op { Call, FunctionVariantCall, BuiltinCall, + EvalLiteralCall, + EvalScopeGet, + EvalScopeSet, + EvalFunctionCall, + EvalFunctionCallArray, + EvalFunctionExists, + EvalClassExists, + EvalConstantExists, + EvalConstantFetch, RuntimeCall, ExternCall, ClosureNew, @@ -420,12 +455,49 @@ impl Op { use Effects as E; use Op::*; match self { - ConstI64 | ConstF64 | ConstStr | ConstNull | ConstBool | ConstClassName - | DataAddr | IAdd | ISub | IMul | IPow | INeg | IBitAnd | IBitOr | IBitXor - | IBitNot | IShl | IShrA | FAdd | FSub | FMul | FDiv | FPow | FNeg | ICmp - | FCmp | StrLen | IToF | FToI | BoolToStr | StrToI | StrToF | StrToNumber - | MixedTagOf | IsNull | IsTruthy | IsEmpty | FunctionVariantDispatch | PtrCast - | PtrOffset | Move | Borrow | Nop => E::PURE, + ConstI64 + | ConstF64 + | ConstStr + | ConstNull + | ConstBool + | ConstClassName + | DataAddr + | IAdd + | ISub + | IMul + | IPow + | INeg + | IBitAnd + | IBitOr + | IBitXor + | IBitNot + | IShl + | IShrA + | FAdd + | FSub + | FMul + | FDiv + | FPow + | FNeg + | ICmp + | FCmp + | StrLen + | IToF + | FToI + | BoolToStr + | StrToI + | StrToF + | StrToNumber + | MixedTagOf + | IsNull + | IsTruthy + | IsEmpty + | FunctionVariantDispatch + | PtrCast + | PtrOffset + | Move + | Borrow + | Nop => E::PURE, IDiv | ISDiv | ISMod | PtrCheckNonnull => E::MAY_FATAL, ICheckedAdd | ICheckedSub | ICheckedMul => E::ALLOC_HEAP | E::READS_HEAP, ConstEnumCase => E::ALLOC_HEAP, @@ -435,13 +507,29 @@ impl Op { | FinallyExit => E::WRITES_LOCAL, PromoteLocalRefCell => { E::READS_LOCAL | E::WRITES_LOCAL | E::ALLOC_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP - }, + } AliasLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL, - ReleaseLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP, - LoadGlobal | LoadStaticProperty | ScopedConstantGet | ClassAttrNames - | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, - StoreGlobal | StoreStaticLocal | StoreStaticProperty | InitStaticLocal | IncludeOnceMark - | FunctionVariantMark | TryPushHandler | TryPopHandler => E::WRITES_GLOBAL, + ReleaseLocalRefCell => { + E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP + } + LoadGlobal + | LoadStaticProperty + | LoadReflectionStaticProperty + | ReflectionStaticPropertyInitialized + | ScopedConstantGet + | ClassAttrNames + | ClassAttrArgs + | ClassGetAttributes + | CatchCurrent => E::READS_GLOBAL, + StoreGlobal + | StoreStaticLocal + | StoreStaticProperty + | StoreReflectionStaticProperty + | InitStaticLocal + | IncludeOnceMark + | FunctionVariantMark + | TryPushHandler + | TryPopHandler => E::WRITES_GLOBAL, IncludeOnceGuard => E::READS_GLOBAL | E::WRITES_GLOBAL, IToStr | FToStr | ResourceToStr | StrConcat | StrCharAt | StrInterpolate | MixedCastString | VarDump | PrintR => E::ALLOC_CONCAT, @@ -459,10 +547,11 @@ impl Op { } ArrayGet => E::READS_HEAP | E::MAY_FATAL | E::MAY_WARN, StrPersist | ArrayEnsureUnique | HashEnsureUnique | ArrayCloneShallow - | HashCloneShallow => E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP, - ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | LoadPropRefCell => { - E::READS_HEAP + | HashCloneShallow | ObjectCloneShallow => { + E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP } + ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | PropInitialized + | LoadPropRefCell => E::READS_HEAP, LoadArrayElemRefCell => E::READS_HEAP | E::MAY_FATAL, BindRefCellPtr => E::WRITES_LOCAL, ArraySet | HashSet | HashUnset | ArrayPush | HashAppend | OffsetUnset | PropSet @@ -479,7 +568,8 @@ impl Op { } HashSpread => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP, IterStart | IterCurrentKey | IterCurrentValue | IteratorMethodCall - | SplRuntimeCall | DynamicObjectNew | DynamicObjectNewMixed | DynamicPropGet | NullsafePropGet + | SplRuntimeCall | DynamicObjectNew | DynamicObjectNewMixed + | DynamicObjectNewWithoutConstructorMixed | DynamicPropGet | NullsafePropGet | NullsafeMethodCall | MethodLookup | MethodCall | StaticMethodCall | InstanceOfDynamic | MixedNumericBinop | LooseEq | LooseNotEq | Spaceship => { E::READS_HEAP | E::MAY_DEOPT @@ -491,10 +581,26 @@ impl Op { EnumBackingStringToInt | EnumBackingMixedToInt => { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } - Call | FunctionVariantCall | BuiltinCall | RuntimeCall | ClosureCall | ExprCall - | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { - E::all().difference(E::REFCOUNT_OP) + EvalFunctionExists | EvalClassExists | EvalConstantExists => E::READS_GLOBAL, + EvalScopeGet => E::READS_HEAP | E::MAY_FATAL, + EvalScopeSet => E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL, + EvalConstantFetch => { + E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } + Call + | FunctionVariantCall + | BuiltinCall + | EvalLiteralCall + | EvalFunctionCall + | EvalFunctionCallArray + | EvalObjectNew + | EvalStaticMethodCall + | RuntimeCall + | ClosureCall + | ExprCall + | CallableDescriptorInvoke + | PipeCall + | FiberRuntimeCall => E::all().difference(E::REFCOUNT_OP), ExternCall | ExternGlobalLoad | ExternGlobalStore => { E::READS_HEAP | E::WRITES_HEAP | E::READS_PROCESS | E::WRITES_PROCESS | E::MAY_THROW } @@ -515,6 +621,11 @@ impl Op { Op::Call | Op::FunctionVariantCall | Op::BuiltinCall + | Op::EvalLiteralCall + | Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalObjectNew + | Op::EvalStaticMethodCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -557,6 +668,9 @@ impl Op { InitStaticLocal => "init_static_local", LoadStaticProperty => "load_static_property", StoreStaticProperty => "store_static_property", + LoadReflectionStaticProperty => "load_reflection_static_property", + StoreReflectionStaticProperty => "store_reflection_static_property", + ReflectionStaticPropertyInitialized => "reflection_static_property_initialized", IAdd => "iadd", ISub => "isub", IMul => "imul", @@ -664,9 +778,15 @@ impl Op { IteratorMethodCall => "iterator_method_call", SplRuntimeCall => "spl_runtime_call", ObjectNew => "object_new", + EvalObjectNew => "eval_object_new", + ObjectCloneShallow => "object_clone_shallow", DynamicObjectNew => "dynamic_object_new", DynamicObjectNewMixed => "dynamic_object_new_mixed", + DynamicObjectNewWithoutConstructorMixed => { + "dynamic_object_new_without_constructor_mixed" + } PropGet => "prop_get", + PropInitialized => "prop_initialized", PropSet => "prop_set", LoadPropRefCell => "load_prop_ref_cell", LoadArrayElemRefCell => "load_array_elem_ref_cell", @@ -678,6 +798,7 @@ impl Op { MethodLookup => "method_lookup", MethodCall => "method_call", StaticMethodCall => "static_method_call", + EvalStaticMethodCall => "eval_static_method_call", EnumBackingStringToInt => "enum_backing_string_to_int", EnumBackingMixedToInt => "enum_backing_mixed_to_int", ClassConstant => "class_constant", @@ -689,6 +810,15 @@ impl Op { Call => "call", FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", + EvalLiteralCall => "eval_literal_call", + EvalScopeGet => "eval_scope_get", + EvalScopeSet => "eval_scope_set", + EvalFunctionCall => "eval_function_call", + EvalFunctionCallArray => "eval_function_call_array", + EvalFunctionExists => "eval_function_exists", + EvalClassExists => "eval_class_exists", + EvalConstantExists => "eval_constant_exists", + EvalConstantFetch => "eval_constant_fetch", RuntimeCall => "runtime_call", ExternCall => "extern_call", ClosureNew => "closure_new", diff --git a/src/ir/mod.rs b/src/ir/mod.rs index d4a525f648..4ea6b714ca 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -36,7 +36,7 @@ pub use instr::{ PassOrigin,}; pub use module::{ ClassTable, DataId, DataPool, EnumTable, ExternDecl, ExternParamDecl, InterfaceTable, - Module, PackedLayoutTable, + Module, PackedLayoutTable, TraitMethodInfo, }; pub use print::{print_function, print_module}; pub use types::{IrHeapKind, IrType}; diff --git a/src/ir/module.rs b/src/ir/module.rs index ddee0b271b..e55a526edf 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -9,12 +9,13 @@ //! - Runtime helper bodies remain outside EIR; modules reference runtime //! features and metadata needed to select/link helpers. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; use crate::ir::function::{Function, FunctionId}; use crate::ir::types::IrType; +use crate::parser::ast::{ExprKind, Visibility}; use crate::types::{ ClassInfo, EnumInfo, ExternClassInfo, FunctionSig, InterfaceInfo, PackedClassInfo, PhpType, }; @@ -35,10 +36,21 @@ impl DataId { } } +/// Method metadata retained for standalone trait reflection. +#[derive(Debug, Clone)] +pub struct TraitMethodInfo { + pub signature: FunctionSig, + pub visibility: Visibility, + pub is_static: bool, + pub is_final: bool, + pub is_abstract: bool, +} + /// Complete EIR module for one compile target. #[derive(Debug, Clone)] pub struct Module { pub target: Target, + pub source_path: Option, pub functions: Vec, pub class_methods: Vec, pub closures: Vec, @@ -56,7 +68,17 @@ pub struct Module { pub declared_class_names: Vec, pub declared_interface_names: Vec, pub declared_trait_names: Vec, + pub declared_trait_source_lines: HashMap, pub declared_trait_uses: HashMap>, + pub declared_trait_method_names: HashMap>, + pub declared_trait_methods: HashMap>, + pub declared_trait_property_names: HashMap>, + pub declared_trait_constant_names: HashMap>, + pub declared_trait_constants: HashMap>, + pub declared_trait_constant_visibilities: HashMap>, + pub declared_trait_final_constants: HashMap>, + /// Prescanned global constant values used by EIR lowering and eval metadata registration. + pub global_constants: HashMap, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -72,6 +94,7 @@ impl Module { pub fn new(target: Target) -> Self { Self { target, + source_path: None, functions: Vec::new(), class_methods: Vec::new(), closures: Vec::new(), @@ -89,7 +112,16 @@ impl Module { declared_class_names: Vec::new(), declared_interface_names: Vec::new(), declared_trait_names: Vec::new(), + declared_trait_source_lines: HashMap::new(), declared_trait_uses: HashMap::new(), + declared_trait_method_names: HashMap::new(), + declared_trait_methods: HashMap::new(), + declared_trait_property_names: HashMap::new(), + declared_trait_constant_names: HashMap::new(), + declared_trait_constants: HashMap::new(), + declared_trait_constant_visibilities: HashMap::new(), + declared_trait_final_constants: HashMap::new(), + global_constants: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 508a134912..b6f7302c7b 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -26,7 +26,10 @@ pub enum ValidationError { NoBlocks, NoEntryBlock, EntryBlockHasParams(BlockId), - BlockIdMismatch { expected: BlockId, actual: BlockId }, + BlockIdMismatch { + expected: BlockId, + actual: BlockId, + }, BlockMissingTerminator(BlockId), UnknownBlock(BlockId), UnknownInstruction(InstId), @@ -126,7 +129,10 @@ fn validate_function_shape(function: &Function) -> Result<(), ValidationError> { if function.block(function.entry).is_none() { return Err(ValidationError::NoEntryBlock); } - if !function.blocks[function.entry.as_raw() as usize].params.is_empty() { + if !function.blocks[function.entry.as_raw() as usize] + .params + .is_empty() + { return Err(ValidationError::EntryBlockHasParams(function.entry)); } for (index, block) in function.blocks.iter().enumerate() { @@ -211,7 +217,14 @@ fn validate_instructions( validate_instruction_result(function, block.id, index as u32, *inst_id, inst)?; validate_instruction_effects(*inst_id, inst)?; validate_instruction_immediate(*inst_id, inst)?; - validate_instruction_operands(function, block.id, index as u32, *inst_id, inst, dominators)?; + validate_instruction_operands( + function, + block.id, + index as u32, + *inst_id, + inst, + dominators, + )?; validate_opcode_rules(function, *inst_id, inst)?; } } @@ -243,7 +256,13 @@ fn validate_instruction_result( if value.ownership != inst.result_ownership { return Err(ValidationError::OwnershipTypeMismatch(value_id)); } - if value.def != (ValueDef::Instruction { block, index, inst: inst_id }) { + if value.def + != (ValueDef::Instruction { + block, + index, + inst: inst_id, + }) + { return Err(ValidationError::ValueDefMismatch(value_id)); } Ok(()) @@ -252,7 +271,10 @@ fn validate_instruction_result( } /// Validates that non-refinable opcodes carry their canonical effect set. -fn validate_instruction_effects(inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_instruction_effects( + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { let expected = inst.op.default_effects(); if !inst.op.allows_effect_refinement() && inst.effects != expected { return Err(ValidationError::EffectMismatch { @@ -265,7 +287,10 @@ fn validate_instruction_effects(inst_id: InstId, inst: &Instruction) -> Result<( } /// Validates immediate shape for opcodes whose immediate is structurally required. -fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_instruction_immediate( + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { use Immediate as Imm; use Op::*; match inst.op { @@ -273,8 +298,14 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstF64 => require_immediate(inst_id, inst, "f64", |imm| matches!(imm, Imm::F64(_))), ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard - | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell - | EnumBackingStringToInt | EnumBackingMixedToInt => { + | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalLiteralCall + | EvalFunctionCallArray | EvalFunctionExists | EvalClassExists | EvalConstantExists + | EvalConstantFetch + | EvalStaticMethodCall + | EnumBackingStringToInt + | EnumBackingMixedToInt + | PropInitialized + | ReflectionStaticPropertyInitialized => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } LoadLocal | StoreLocal | UnsetLocal | LoadRefCell | StoreRefCell | ReleaseLocalRefCell @@ -285,6 +316,9 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result PromoteLocalRefCell | AliasLocalRefCell => require_immediate(inst_id, inst, "local slot pair", |imm| { matches!(imm, Imm::LocalSlotPair { .. }) }), + EvalScopeGet | EvalScopeSet => require_immediate(inst_id, inst, "global name", |imm| { + matches!(imm, Imm::GlobalName(_)) + }), ICmp | FCmp => require_immediate(inst_id, inst, "comparison predicate", |imm| { matches!(imm, Imm::CmpPredicate(_)) }), @@ -320,12 +354,18 @@ fn require_immediate( matches_expected: impl FnOnce(&Immediate) -> bool, ) -> Result<(), ValidationError> { let Some(imm) = inst.immediate.as_ref() else { - return Err(ValidationError::MissingImmediate { inst: inst_id, expected }); + return Err(ValidationError::MissingImmediate { + inst: inst_id, + expected, + }); }; if matches_expected(imm) { Ok(()) } else { - Err(ValidationError::MissingImmediate { inst: inst_id, expected }) + Err(ValidationError::MissingImmediate { + inst: inst_id, + expected, + }) } } @@ -345,7 +385,11 @@ fn validate_instruction_operands( } /// Validates core opcode operand/result type rules. -fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_opcode_rules( + function: &Function, + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { use Op::*; match inst.op { ConstI64 | ConstBool | ConstNull => check_count(inst_id, inst, 0, "0"), @@ -353,13 +397,18 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | CallableArrayNew | GeneratorNew | InvokerRefArg | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark - | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | ConcatReset - | GcCollect | Nop => { + | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists + | EvalClassExists | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } + EvalLiteralCall | EvalFunctionCallArray | EvalScopeGet => { + check_count(inst_id, inst, 1, "1") + } + EvalScopeSet => check_count(inst_id, inst, 2, "2"), ClosureNew => Ok(()), FirstClassCallableNew => check_count_at_most(inst_id, inst, 1, "0 or 1"), ObjectNew => Ok(()), + EvalStaticMethodCall => Ok(()), IAdd | ISub | IMul | IDiv | ISDiv | ISMod | IPow | IBitAnd | IBitOr | IBitXor | IShl | IShrA => check_binary(function, inst_id, inst, IrType::I64, "I64"), ICheckedAdd | ICheckedSub | ICheckedMul => { @@ -385,29 +434,51 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio StrToI | StrToF | StrToNumber | StrLen | StrPersist => { check_unary(function, inst_id, inst, IrType::Str, "Str") } - StrConcat | StrEq | StrCmp | StrLooseEq => check_binary(function, inst_id, inst, IrType::Str, "Str"), + StrConcat | StrEq | StrCmp | StrLooseEq => { + check_binary(function, inst_id, inst, IrType::Str, "Str") + } StrCharAt => { check_count(inst_id, inst, 2, "2")?; check_operand_type(function, inst_id, inst, 0, IrType::Str, "Str")?; check_operand_type(function, inst_id, inst, 1, IrType::I64, "I64") } BufferNew => check_unary(function, inst_id, inst, IrType::I64, "I64"), - LoadLocal | LoadRefCell | LoadGlobal | LoadStaticLocal | LoadStaticProperty | ExternGlobalLoad => { - check_count(inst_id, inst, 0, "0") - } + LoadLocal + | LoadRefCell + | LoadGlobal + | LoadStaticLocal + | LoadStaticProperty + | LoadReflectionStaticProperty + | ReflectionStaticPropertyInitialized + | ExternGlobalLoad => check_count(inst_id, inst, 0, "0"), UnsetLocal | PromoteLocalRefCell | AliasLocalRefCell | ReleaseLocalRefCell => { check_count(inst_id, inst, 0, "0") } - StoreLocal | StoreGlobal | StoreStaticLocal | InitStaticLocal | StoreStaticProperty | ExternGlobalStore - | StoreRefCell | BindRefCellPtr | Acquire | Release | Move | Borrow | EnsureOwned - | EchoValue | PrintValue | WriteStdout | WriteStrStdout | VarDump | PrintR - | ThrowException | GeneratorReturn | PtrCheckNonnull => { + StoreLocal | StoreGlobal | StoreStaticLocal | InitStaticLocal | StoreStaticProperty + | StoreReflectionStaticProperty | ExternGlobalStore | StoreRefCell | BindRefCellPtr + | Acquire | Release | Move | Borrow | EnsureOwned | EchoValue | PrintValue | WriteStdout + | WriteStrStdout | VarDump | PrintR | ThrowException | GeneratorReturn + | PtrCheckNonnull => { check_count(inst_id, inst, 1, "1") } MixedTagOf | MixedUnbox | MixedCastBool | MixedCastInt | MixedCastFloat - | MixedCastString => check_heap_unary(function, inst_id, inst, IrHeapKind::Mixed, "Heap(Mixed)"), - ArrayUnion => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Array), "Heap(Array)"), - HashUnion => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)"), + | MixedCastString => { + check_heap_unary(function, inst_id, inst, IrHeapKind::Mixed, "Heap(Mixed)") + } + ArrayUnion => check_binary( + function, + inst_id, + inst, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + ), + HashUnion => check_binary( + function, + inst_id, + inst, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + ), ArrayHashUnion => check_array_hash_union(function, inst_id, inst), HashArrayUnion => check_hash_array_union(function, inst_id, inst), HashSpread => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)"), @@ -423,9 +494,17 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio } MixedArrayAppend => { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Mixed), "Heap(Mixed)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Mixed), + "Heap(Mixed)", + ) } - HashLen | HashGet | HashIsset | HashSet | HashAppend | HashEnsureUnique | HashCloneShallow => { + HashLen | HashGet | HashIsset | HashSet | HashAppend | HashEnsureUnique + | HashCloneShallow => { check_first_heap(function, inst_id, inst, IrHeapKind::Hash, "Heap(Hash)") } IterCurrentValueRef => check_count(inst_id, inst, 1, "1"), @@ -433,8 +512,19 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio BufferLen | BufferGet | BufferSet | BufferFree => { check_first_heap(function, inst_id, inst, IrHeapKind::Buffer, "Heap(Buffer)") } - PropGet | PropSet | LoadPropRefCell | DynamicPropGet | DynamicPropSet | NullsafePropGet - | NullsafeMethodCall | MethodLookup | MethodCall | InstanceOf | InstanceOfDynamic => { + DynamicObjectNewWithoutConstructorMixed + | PropGet + | PropInitialized + | PropSet + | LoadPropRefCell + | DynamicPropGet + | DynamicPropSet + | NullsafePropGet + | NullsafeMethodCall + | MethodLookup + | MethodCall + | InstanceOf + | InstanceOfDynamic => { check_count_at_least(inst_id, inst, 1, "at least 1") } _ => Ok(()), @@ -448,8 +538,22 @@ fn check_array_hash_union( inst: &Instruction, ) -> Result<(), ValidationError> { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Array), "Heap(Array)")?; - check_operand_type(function, inst_id, inst, 1, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + )?; + check_operand_type( + function, + inst_id, + inst, + 1, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + ) } /// Validates the operand shape for associative+indexed array union. @@ -459,12 +563,31 @@ fn check_hash_array_union( inst: &Instruction, ) -> Result<(), ValidationError> { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)")?; - check_operand_type(function, inst_id, inst, 1, IrType::Heap(IrHeapKind::Array), "Heap(Array)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + )?; + check_operand_type( + function, + inst_id, + inst, + 1, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + ) } /// Validates one exact operand count. -fn check_count(inst_id: InstId, inst: &Instruction, expected: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count( + inst_id: InstId, + inst: &Instruction, + expected: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() == expected { Ok(()) } else { @@ -477,7 +600,12 @@ fn check_count(inst_id: InstId, inst: &Instruction, expected: usize, expected_la } /// Validates a minimum operand count. -fn check_count_at_least(inst_id: InstId, inst: &Instruction, min: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count_at_least( + inst_id: InstId, + inst: &Instruction, + min: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() >= min { Ok(()) } else { @@ -490,7 +618,12 @@ fn check_count_at_least(inst_id: InstId, inst: &Instruction, min: usize, expecte } /// Validates a maximum operand count. -fn check_count_at_most(inst_id: InstId, inst: &Instruction, max: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count_at_most( + inst_id: InstId, + inst: &Instruction, + max: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() <= max { Ok(()) } else { @@ -664,7 +797,9 @@ fn validate_terminators( validate_branch_args(function, *default, default_args)?; validate_terminator_uses(function, block.id, default_args, dominators)?; } - Terminator::Return { value } => validate_return(function, block.id, *value, dominators)?, + Terminator::Return { value } => { + validate_return(function, block.id, *value, dominators)? + } Terminator::Throw { value } => { validate_use(function, *value, block.id, None, dominators)?; } @@ -753,7 +888,11 @@ fn validate_terminator_uses( } /// Validates destination block argument count and type compatibility. -fn validate_branch_args(function: &Function, target: BlockId, args: &[ValueId]) -> Result<(), ValidationError> { +fn validate_branch_args( + function: &Function, + target: BlockId, + args: &[ValueId], +) -> Result<(), ValidationError> { let Some(target_block) = function.block(target) else { return Err(ValidationError::UnknownBlock(target)); }; @@ -850,9 +989,9 @@ fn definition_dominates_use( .map(|set| set.contains(&block)) .unwrap_or(false) } - ValueDef::Instruction { block, index, .. } if block == use_block => { - use_inst_index.map(|use_index| index < use_index).unwrap_or(true) - } + ValueDef::Instruction { block, index, .. } if block == use_block => use_inst_index + .map(|use_index| index < use_index) + .unwrap_or(true), ValueDef::Instruction { block, .. } => dominators .get(&use_block) .map(|set| set.contains(&block)) @@ -873,42 +1012,125 @@ fn definition_dominates_use( /// resolve to `{self}` (no reachable predecessor), so genuine uses inside dead /// code remain flagged until they are neutralized. fn compute_dominators(function: &Function) -> HashMap> { - let all_blocks: HashSet = function.blocks.iter().map(|block| block.id).collect(); let predecessors = compute_predecessors(function); let reachable = reachable_from_entry(function, &predecessors); - let mut dominators = HashMap::new(); - for block in &function.blocks { - if block.id == function.entry { - dominators.insert(block.id, HashSet::from([block.id])); - } else { - dominators.insert(block.id, all_blocks.clone()); - } - } + let block_count = function.blocks.len(); + let all_blocks = full_block_bitset(block_count); + let mut dominators = vec![all_blocks.clone(); block_count]; + let entry_index = function.entry.as_raw() as usize; + dominators[entry_index] = empty_block_bitset(block_count); + set_block_bit(&mut dominators[entry_index], entry_index); + let predecessor_indices = reachable_predecessor_indices(function, &predecessors, &reachable); let mut changed = true; while changed { changed = false; - for block in &function.blocks { + for (block_index, block) in function.blocks.iter().enumerate() { if block.id == function.entry { continue; } - let preds: Vec = predecessors - .get(&block.id) - .map(|preds| preds.iter().copied().filter(|p| reachable.contains(p)).collect()) - .unwrap_or_default(); - let mut next = if preds.is_empty() { - HashSet::new() + let mut next = if predecessor_indices[block_index].is_empty() { + empty_block_bitset(block_count) } else { - intersection_of_predecessors(&preds, &dominators, &all_blocks) + all_blocks.clone() }; - next.insert(block.id); - if dominators.get(&block.id) != Some(&next) { - dominators.insert(block.id, next); + for pred_index in &predecessor_indices[block_index] { + bitset_and_assign(&mut next, &dominators[*pred_index]); + } + set_block_bit(&mut next, block_index); + if dominators[block_index] != next { + dominators[block_index] = next; changed = true; } } } - dominators + dominator_bitsets_to_map(function, &dominators) +} + +/// Converts reachable predecessor block ids into dense block indices for bitset dominator scans. +fn reachable_predecessor_indices( + function: &Function, + predecessors: &HashMap>, + reachable: &HashSet, +) -> Vec> { + function + .blocks + .iter() + .map(|block| { + predecessors + .get(&block.id) + .map(|preds| { + preds + .iter() + .filter(|pred| reachable.contains(pred)) + .map(|pred| pred.as_raw() as usize) + .collect() + }) + .unwrap_or_default() + }) + .collect() +} + +/// Builds a bitset with every block bit set, masking unused bits in the last word. +fn full_block_bitset(block_count: usize) -> Vec { + let mut bits = vec![u64::MAX; block_bitset_word_count(block_count)]; + let trailing_bits = block_count % u64::BITS as usize; + if trailing_bits != 0 { + if let Some(last) = bits.last_mut() { + *last = (1_u64 << trailing_bits) - 1; + } + } + bits +} + +/// Builds an empty block bitset with enough words for the function block count. +fn empty_block_bitset(block_count: usize) -> Vec { + vec![0; block_bitset_word_count(block_count)] +} + +/// Returns the number of machine words needed to represent one block bitset. +fn block_bitset_word_count(block_count: usize) -> usize { + block_count.div_ceil(u64::BITS as usize) +} + +/// Marks one block index as present in a dominator bitset. +fn set_block_bit(bits: &mut [u64], block_index: usize) { + let word = block_index / u64::BITS as usize; + let bit = block_index % u64::BITS as usize; + bits[word] |= 1_u64 << bit; +} + +/// Intersects a dominator bitset in place with another predecessor bitset. +fn bitset_and_assign(left: &mut [u64], right: &[u64]) { + for (left_word, right_word) in left.iter_mut().zip(right.iter()) { + *left_word &= *right_word; + } +} + +/// Converts dense dominator bitsets back to the validator's public block-id map. +fn dominator_bitsets_to_map( + function: &Function, + dominators: &[Vec], +) -> HashMap> { + let mut map = HashMap::with_capacity(function.blocks.len()); + for (block_index, block) in function.blocks.iter().enumerate() { + let mut set = HashSet::new(); + for (candidate_index, candidate) in function.blocks.iter().enumerate() { + if block_bit_is_set(&dominators[block_index], candidate_index) { + set.insert(candidate.id); + } + } + map.insert(block.id, set); + } + map +} + +/// Returns whether a block index is present in a dominator bitset. +fn block_bit_is_set(bits: &[u64], block_index: usize) -> bool { + let word = block_index / u64::BITS as usize; + let bit = block_index % u64::BITS as usize; + bits.get(word) + .is_some_and(|word_bits| (word_bits & (1_u64 << bit)) != 0) } /// Computes the set of blocks reachable from the entry over the same edge set as @@ -993,25 +1215,6 @@ fn successors(term: &Terminator) -> Vec { } } -/// Intersects dominator sets for all predecessors. -fn intersection_of_predecessors( - predecessors: &[BlockId], - dominators: &HashMap>, - fallback: &HashSet, -) -> HashSet { - let mut iter = predecessors.iter(); - let Some(first) = iter.next() else { - return HashSet::new(); - }; - let mut result = dominators.get(first).cloned().unwrap_or_else(|| fallback.clone()); - for pred in iter { - if let Some(set) = dominators.get(pred) { - result = result.intersection(set).copied().collect(); - } - } - result -} - /// Returns true when PHP type metadata can use the given EIR storage type. fn php_type_compatible(ir_type: IrType, php_type: &PhpType) -> bool { let php_type = php_type.codegen_repr(); @@ -1022,8 +1225,8 @@ fn php_type_compatible(ir_type: IrType, php_type: &PhpType) -> bool { /// Returns true when ownership is coherent with storage and PHP type metadata. fn ownership_compatible(ir_type: IrType, php_type: &PhpType, ownership: Ownership) -> bool { let php_type = php_type.codegen_repr(); - let tracks_lifetime = ir_type.is_refcounted_storage() - || Ownership::php_type_needs_lifetime_tracking(&php_type); + let tracks_lifetime = + ir_type.is_refcounted_storage() || Ownership::php_type_needs_lifetime_tracking(&php_type); if tracks_lifetime { !matches!(ownership, Ownership::NonHeap) } else { diff --git a/src/ir_lower/builtin_datetime.rs b/src/ir_lower/builtin_datetime.rs index 3cd226009d..2dc3065a81 100644 --- a/src/ir_lower/builtin_datetime.rs +++ b/src/ir_lower/builtin_datetime.rs @@ -21,7 +21,7 @@ use std::collections::HashSet; -use crate::ir::{Immediate, Module, Op}; +use crate::ir::{Function, Immediate, Module, Op}; use crate::ir_lower::function; use crate::parser::ast::ExprKind; use crate::types::{CheckResult, PhpType}; @@ -71,6 +71,7 @@ pub(crate) fn lower_referenced_builtin_datetime_methods( constants: &std::collections::HashMap, fiber_return_sigs: &std::collections::HashMap, ) { + lower_eval_date_alias_methods_if_needed(module, check_result, constants, fiber_return_sigs); loop { let mut methods = referenced_builtin_datetime_methods(module); methods.sort(); @@ -99,6 +100,153 @@ pub(crate) fn lower_referenced_builtin_datetime_methods( } } +/// Lowers DateTime-family methods that runtime eval aliases may call dynamically. +fn lower_eval_date_alias_methods_if_needed( + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + if !module_uses_eval(module) { + return; + } + let mut methods = eval_date_alias_builtin_datetime_methods(module); + methods.sort(); + methods.dedup(); + for (class_name, method_key) in methods { + lower_builtin_datetime_method( + &class_name, + &method_key, + module, + check_result, + constants, + fiber_return_sigs, + ); + } +} + +/// Returns the builtin DateTime-family methods reachable from eval alias dispatch. +fn eval_date_alias_builtin_datetime_methods(module: &Module) -> Vec<(String, String)> { + let mut methods = Vec::new(); + for class_name in ["DateTime", "DateTimeImmutable", "DateTimeZone", "DateInterval"] { + push_constructor_and_interface_methods(&mut methods, module, class_name); + } + for method_name in [ + "createFromFormat", + "getLastErrors", + "__elephc_date_parse_from_format", + "__elephc_date_parse", + "__elephc_date_sun_info", + "__elephc_date_sunfunc", + "__elephc_strptime", + "__elephc_timezone_name_from_abbr", + "__elephc_cal_to_jd", + "__elephc_cal_from_jd", + "__elephc_cal_days_in_month", + "__elephc_cal_info", + "__elephc_gregoriantojd", + "__elephc_jdtogregorian", + "__elephc_juliantojd", + "__elephc_jdtojulian", + "__elephc_frenchtojd", + "__elephc_jdtofrench", + "__elephc_jewishtojd", + "__elephc_jdtojewish", + "__elephc_jddayofweek", + "__elephc_jdmonthname", + "__elephc_jdtounix", + "__elephc_unixtojd", + "__elephc_easter_days", + "__elephc_easter_date", + "__elephc_gettimeofday", + "__elephc_strftime", + "diff", + "format", + "add", + "sub", + "modify", + "getTimestamp", + "setTimestamp", + "getTimezone", + "setTimezone", + "getOffset", + "setDate", + "setISODate", + "setTime", + ] { + methods.push(("DateTime".to_string(), php_method_key(method_name))); + methods.push(("DateTimeImmutable".to_string(), php_method_key(method_name))); + } + for method_name in ["createFromDateString", "format"] { + methods.push(("DateInterval".to_string(), php_method_key(method_name))); + } + for method_name in [ + "getName", + "getOffset", + "listIdentifiers", + "getLocation", + "getTransitions", + "listAbbreviations", + ] { + methods.push(("DateTimeZone".to_string(), php_method_key(method_name))); + } + methods +} + +/// Returns true when the lowered module has any dependency on the eval bridge. +fn module_uses_eval(module: &Module) -> bool { + module.required_runtime_features.eval_bridge + || module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) + .any(|function| function_uses_eval(module, function)) +} + +/// Returns true when one lowered function contains an eval bridge instruction. +fn function_uses_eval(module: &Module, function: &Function) -> bool { + function + .instructions + .iter() + .any(|inst| instruction_uses_eval(module, inst)) +} + +/// Returns true when one instruction requires eval runtime support. +fn instruction_uses_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + matches!( + inst.op, + Op::EvalLiteralCall + | Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalObjectNew + | Op::EvalStaticMethodCall + | Op::EvalFunctionExists + | Op::EvalClassExists + | Op::EvalConstantExists + | Op::EvalConstantFetch + ) || builtin_call_is_eval(module, inst) +} + +/// Returns true when one lowered builtin call is PHP's `eval` construct. +fn builtin_call_is_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + if inst.op != Op::BuiltinCall { + return false; + } + let Some(Immediate::Data(data)) = inst.immediate else { + return false; + }; + module + .data + .function_names + .get(data.as_raw() as usize) + .is_some_and(|name| crate::names::php_symbol_key(name.trim_start_matches('\\')) == "eval") +} + /// Finds builtin date/time methods whose symbols are required by already-lowered EIR. /// /// Returns `(class_name, method_key)` pairs for every `ObjectNew`, diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 6bce5860a6..c85894163d 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -11,11 +11,11 @@ //! - Control-flow joins can reload locals from slots, so Phase 03 does not need //! to synthesize block-parameter phis for every PHP variable yet. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use crate::ir::{ - BlockId, Builder, DataId, DataPool, Effects, Immediate, IrType, LocalKind, LocalSlotId, Op, - Ownership, ValueId, Function, + BlockId, Builder, DataId, DataPool, Effects, Function, Immediate, IrType, LocalKind, + LocalSlotId, Op, Ownership, ValueId, }; use crate::names::{php_symbol_key, property_hook_get_method, property_hook_set_method}; use crate::parser::ast::{Expr, ExprKind, StaticReceiver, Stmt, TypeExpr}; @@ -88,6 +88,12 @@ pub(crate) struct ClosureCapture { pub value: ValueId, } +const EVAL_CONTEXT_LOCAL_NAME: &str = "__eir_eval_context"; +const EVAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_scope"; +const EVAL_GLOBAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_global_scope"; +const EVAL_ARGC_LOCAL_NAME: &str = "argc"; +const EVAL_ARGV_LOCAL_NAME: &str = "argv"; + /// Mutable state for one function body while it is lowered. pub(crate) struct LoweringContext<'m, 'f> { pub builder: Builder<'f>, @@ -114,6 +120,11 @@ pub(crate) struct LoweringContext<'m, 'f> { pub loop_stack: Vec, pub finally_stack: Vec, static_callable_locals: HashMap, + reflection_class_locals: HashMap, + reflection_function_locals: HashMap, + reflection_property_locals: HashMap, + reflection_method_locals: HashMap, + reflection_arg_array_locals: HashMap>, fiber_start_sigs: HashMap, ref_bound_locals: HashSet, ref_cell_owner_locals: HashMap, @@ -137,6 +148,13 @@ pub(crate) struct LoweringContext<'m, 'f> { pending_static_callable_result: Option, closure_counter: usize, hidden_temp_counter: usize, + eval_barrier_active: bool, + eval_executed: bool, + eval_scope_read_param: Option, + eval_scope_read_names: HashSet, + eval_scope_write_names: HashSet, + eval_scope_flush_names: BTreeSet, + source_path: Option, } impl<'m, 'f> LoweringContext<'m, 'f> { @@ -162,6 +180,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return_php_type: PhpType, in_main: bool, all_global_var_names: HashSet, + source_path: Option, ) -> Self { let return_type = return_ir_type(&return_php_type); Self { @@ -187,6 +206,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { loop_stack: Vec::new(), finally_stack: Vec::new(), static_callable_locals: HashMap::new(), + reflection_class_locals: HashMap::new(), + reflection_function_locals: HashMap::new(), + reflection_property_locals: HashMap::new(), + reflection_method_locals: HashMap::new(), + reflection_arg_array_locals: HashMap::new(), fiber_start_sigs: HashMap::new(), ref_bound_locals: HashSet::new(), ref_cell_owner_locals: HashMap::new(), @@ -201,9 +225,21 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pending_static_callable_result: None, closure_counter: 0, hidden_temp_counter: 0, + eval_barrier_active: false, + eval_executed: false, + eval_scope_read_param: None, + eval_scope_read_names: HashSet::new(), + eval_scope_write_names: HashSet::new(), + eval_scope_flush_names: BTreeSet::new(), + source_path, } } + /// Returns the canonical PHP source path associated with this lowered body, if known. + pub(crate) fn source_path(&self) -> Option<&str> { + self.source_path.as_deref() + } + /// Interns a string literal or metadata name in the module data pool. pub(crate) fn intern_string(&mut self, value: &str) -> DataId { self.data.intern_string(value) @@ -215,7 +251,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { TypeExpr::Named(name) => { let name = name.as_str().trim_start_matches('\\'); let php_type = named_type_expr_to_php_type(name); - if matches!(php_type, PhpType::Object(_)) && self.packed_classes.contains_key(name) { + if matches!(php_type, PhpType::Object(_)) && self.packed_classes.contains_key(name) + { PhpType::Packed(name.to_string()) } else { php_type @@ -227,9 +264,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { TypeExpr::Array(inner) => { PhpType::Array(Box::new(self.type_expr_to_php_type_for_value(inner))) } - TypeExpr::Nullable(inner) => { - PhpType::Union(vec![PhpType::Void, self.type_expr_to_php_type_for_value(inner)]) - } + TypeExpr::Nullable(inner) => PhpType::Union(vec![ + PhpType::Void, + self.type_expr_to_php_type_for_value(inner), + ]), TypeExpr::Union(members) => PhpType::Union( members .iter() @@ -257,7 +295,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Returns the current known PHP type for a local or `Mixed` when unknown. pub(crate) fn local_type(&self, name: &str) -> PhpType { - self.local_types.get(name).cloned().unwrap_or(PhpType::Mixed) + self.local_types + .get(name) + .cloned() + .unwrap_or(PhpType::Mixed) } /// Records a foreach loop-key local whose source is a concretely-indexed @@ -384,12 +425,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return *slot; } let ir_type = value_ir_type(&php_type); - let slot = self.builder.add_local( - Some(name.to_string()), - ir_type, - php_type.clone(), - kind, - ); + let slot = self + .builder + .add_local(Some(name.to_string()), ir_type, php_type.clone(), kind); self.local_slots.insert(name.to_string(), slot); self.local_kinds.insert(name.to_string(), kind); self.local_types.entry(name.to_string()).or_insert(php_type); @@ -453,12 +491,151 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_local_with_kind(name, php_type, LocalKind::OwnedTemp) } + /// Ensures this function has a persistent eval context handle slot. + pub(crate) fn declare_eval_context_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind( + EVAL_CONTEXT_LOCAL_NAME, + PhpType::Int, + LocalKind::EvalContext, + ) + } + + /// Ensures this function has a persistent eval scope handle slot. + pub(crate) fn declare_eval_scope_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind(EVAL_SCOPE_LOCAL_NAME, PhpType::Int, LocalKind::EvalScope) + } + + /// Ensures this function has a persistent eval global-scope handle slot. + pub(crate) fn declare_eval_global_scope_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind( + EVAL_GLOBAL_SCOPE_LOCAL_NAME, + PhpType::Int, + LocalKind::EvalGlobalScope, + ) + } + + /// Applies the static part of the eval barrier to visible PHP local storage. + pub(crate) fn apply_eval_barrier(&mut self) { + self.eval_barrier_active = true; + self.declare_eval_context_local(); + self.declare_eval_scope_local(); + self.declare_eval_global_scope_local(); + self.declare_eval_main_superglobals(); + let local_names = self + .local_slots + .iter() + .filter_map(|(name, slot)| { + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + (kind == LocalKind::PhpLocal).then_some((name.clone(), *slot)) + }) + .collect::>(); + for (name, slot) in local_names { + if eval_barrier_can_widen(&self.builder.local_php_type(slot)) { + self.set_local_type(&name, PhpType::Mixed); + } + } + for (name, ty) in self.local_types.clone() { + let kind = self + .local_kinds + .get(&name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + if kind == LocalKind::PhpLocal && eval_barrier_can_widen(&ty) { + self.local_types.insert(name, PhpType::Mixed); + } + } + } + + /// Enables direct eval-scope reads for selected variable names in an AOT eval body. + pub(crate) fn enable_eval_scope_access( + &mut self, + scope_param: String, + read_names: HashSet, + write_names: HashSet, + flush_names: BTreeSet, + ) { + self.eval_scope_read_param = Some(scope_param); + self.eval_scope_read_names = read_names; + self.eval_scope_write_names = write_names; + self.eval_scope_flush_names = flush_names; + } + + /// Flushes selected local slots back into the eval scope before function exit. + pub(crate) fn emit_eval_scope_finalizer(&mut self, span: Option) { + let Some(scope_param) = self.eval_scope_read_param.clone() else { + return; + }; + let names = self + .eval_scope_flush_names + .iter() + .cloned() + .collect::>(); + for name in names { + if !self.local_slots.contains_key(&name) { + continue; + } + let scope = self.load_local(&scope_param, span); + let value = self.load_local(&name, span); + let name_data = self.intern_global_name(&name); + self.emit_void( + Op::EvalScopeSet, + vec![scope.value, value.value], + Some(Immediate::GlobalName(name_data)), + Op::EvalScopeSet.default_effects(), + span, + ); + } + } + + /// Applies only the materialized local-scope part needed by EIR eval AOT. + pub(crate) fn apply_eval_scope_barrier(&mut self) { + self.eval_barrier_active = true; + self.declare_eval_scope_local(); + // Scope-sync codegen paths flush program globals into the local scope, + // so the global-scope handle slot must exist alongside the scope slot. + self.declare_eval_global_scope_local(); + } + + /// Ensures top-level eval fragments can see `$argc` and `$argv` by name. + fn declare_eval_main_superglobals(&mut self) { + if !self.in_main { + return; + } + self.declare_local(EVAL_ARGC_LOCAL_NAME, PhpType::Int); + self.mark_local_initialized(EVAL_ARGC_LOCAL_NAME); + self.declare_local(EVAL_ARGV_LOCAL_NAME, PhpType::Array(Box::new(PhpType::Str))); + self.mark_local_initialized(EVAL_ARGV_LOCAL_NAME); + } + + /// Returns true after this function has lowered an `eval()` call. + pub(crate) const fn has_eval_barrier(&self) -> bool { + self.eval_barrier_active + } + + /// Records that an `eval()` call was lowered, even when its fragment + /// compiled through a barrier-free AOT path. + pub(crate) fn mark_eval_executed(&mut self) { + self.eval_executed = true; + } + + /// Returns true when any `eval()` call was lowered in this function. + /// Unlike `has_eval_barrier`, this also covers barrier-free AOT evals: + /// dynamic constant probes must consult the eval registry either way. + pub(crate) const fn eval_executed(&self) -> bool { + self.eval_executed + } + /// Declares a hidden owner slot for a promoted local ref-cell pointer. fn declare_ref_cell_owner(&mut self, variable: &str, php_type: PhpType) -> LocalSlotId { let name = format!("__eir_ref_owner{}_{}", self.hidden_temp_counter, variable); self.hidden_temp_counter += 1; let slot = self.declare_local_with_kind(&name, php_type, LocalKind::RefCell); - self.ref_cell_owner_locals.insert(variable.to_string(), slot); + self.ref_cell_owner_locals + .insert(variable.to_string(), slot); slot } @@ -520,6 +697,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { if let Some(php_type) = self.extern_global_type(name) { return self.load_extern_global(name, php_type, span); } + if self.should_load_from_eval_scope(name) { + return self.load_eval_scope_name(name, span); + } let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, kind); // Superglobals carry a fixed `AssocArray{Str, Mixed}` type in every scope. @@ -561,6 +741,102 @@ impl<'m, 'f> LoweringContext<'m, 'f> { LoweredValue { value, ir_type } } + /// Returns true when a variable read should be sourced from the eval scope handle. + fn should_load_from_eval_scope(&self, name: &str) -> bool { + let Some(scope_param) = &self.eval_scope_read_param else { + return false; + }; + name != scope_param + && !self.local_slots.contains_key(name) + && (self.eval_scope_read_names.contains(name) + || self.eval_scope_write_names.contains(name)) + } + + /// Emits an `EvalScopeGet` for a selected eval-scope variable read. + fn load_eval_scope_name(&mut self, name: &str, span: Option) -> LoweredValue { + let scope_param = self + .eval_scope_read_param + .clone() + .expect("eval scope read mode has a scope parameter"); + let scope = self.load_local(&scope_param, span); + let name_data = self.intern_global_name(name); + let value = self + .builder + .emit_with_effects( + Op::EvalScopeGet, + vec![scope.value], + Some(Immediate::GlobalName(name_data)), + IrType::Heap(crate::ir::IrHeapKind::Mixed), + PhpType::Mixed, + Ownership::Borrowed, + Op::EvalScopeGet.default_effects(), + span, + ) + .expect("eval_scope_get produces a Mixed value"); + LoweredValue { + value, + ir_type: IrType::Heap(crate::ir::IrHeapKind::Mixed), + } + } + + /// Returns true when a variable write should be stored into the eval scope handle. + fn should_store_to_eval_scope(&self, name: &str) -> bool { + let Some(scope_param) = &self.eval_scope_read_param else { + return false; + }; + name != scope_param && self.eval_scope_write_names.contains(name) + } + + /// Emits an `EvalScopeSet` for a selected eval-scope variable write. + fn store_eval_scope_name( + &mut self, + name: &str, + value: LoweredValue, + span: Option, + ) -> LoweredValue { + let php_type = self.builder.value_php_type(value.value).codegen_repr(); + let previous_slot = self.local_slots.get(name).copied(); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + let scope_param = self + .eval_scope_read_param + .clone() + .expect("eval scope write mode has a scope parameter"); + let scope = self.load_local(&scope_param, span); + let name_data = self.intern_global_name(name); + self.emit_void( + Op::EvalScopeSet, + vec![scope.value, value.value], + Some(Immediate::GlobalName(name_data)), + Op::EvalScopeSet.default_effects(), + span, + ); + let slot = self.declare_local(name, php_type.clone()); + self.builder + .widen_local_storage_type(slot, php_type.clone()); + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| self.initialized_slots.contains(&slot)) + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| !self.initialized_slots.contains(&slot)) + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + let stored = crate::ir_lower::ownership::acquire_if_refcounted(self, value, span); + self.store_slot_with_op(slot, stored, Op::StoreLocal, span); + self.set_local_type(name, php_type); + if self.value_needs_release_after_retaining_store(value) { + crate::ir_lower::ownership::release_if_owned(self, value, span); + } + stored + } + /// Emits a load using the local slot's concrete frame-storage type. /// /// This is for cleanup paths that must release the value already present in @@ -575,9 +851,14 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) -> LoweredValue { let ir_type = value_ir_type(&php_type); let ownership = Ownership::for_php_type(&php_type); - let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, kind); - let is_ref_bound = self.is_ref_bound_local(name) && !uses_global && kind == LocalKind::PhpLocal; + let is_ref_bound = + self.is_ref_bound_local(name) && !uses_global && kind == LocalKind::PhpLocal; let op = match (is_ref_bound, uses_global, kind) { (true, _, _) => Op::LoadRefCell, (false, true, _) => Op::LoadGlobal, @@ -606,7 +887,12 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Releases the value currently stored in a local slot using frame-storage metadata. - pub(crate) fn release_stored_local_value(&mut self, name: &str, slot: LocalSlotId, span: Option) { + pub(crate) fn release_stored_local_value( + &mut self, + name: &str, + slot: LocalSlotId, + span: Option, + ) { let storage_type = self.builder.local_php_type(slot); if !Ownership::php_type_needs_lifetime_tracking(&storage_type) { return; @@ -616,8 +902,19 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits a store to a PHP local slot, updates type facts, and returns the stored value. - pub(crate) fn store_local(&mut self, name: &str, value: LoweredValue, php_type: PhpType, span: Option) -> LoweredValue { + pub(crate) fn store_local( + &mut self, + name: &str, + value: LoweredValue, + php_type: PhpType, + span: Option, + ) -> LoweredValue { self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); + self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); if let Some(extern_type) = self.extern_global_type(name) { let release_source_after_store = self.value_is_owning_temporary(value); @@ -628,9 +925,16 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } return value; } + if self.should_store_to_eval_scope(name) { + return self.store_eval_scope_name(name, value, span); + } let previous_slot = self.local_slots.get(name).copied(); let previous_type = self.local_type(name); - let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); let php_type = if uses_global { self.global_alias_type(name) @@ -720,7 +1024,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } return value; } - let is_ref_bound = self.is_ref_bound_local(name) && !uses_global && previous_kind == LocalKind::PhpLocal; + let is_ref_bound = + self.is_ref_bound_local(name) && !uses_global && previous_kind == LocalKind::PhpLocal; let op = match (is_ref_bound, previous_kind) { (true, _) => Op::StoreRefCell, (false, LocalKind::StaticLocal) => Op::StoreStaticLocal, @@ -778,6 +1083,56 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) } + /// Stores a synthetic foreach initializer in the local frame without eval-scope sync. + /// + /// Fresh `foreach` key/value locals need a concrete frame slot before the first + /// iteration, but PHP must not observe that setup when the iterable is empty. + /// Runtime eval-scope writes therefore use this path for the pre-loop null seed + /// and keep normal `store_local` for values assigned inside the loop body. + pub(crate) fn store_foreach_initializer_local_only( + &mut self, + name: &str, + value: LoweredValue, + php_type: PhpType, + span: Option, + ) -> LoweredValue { + let previous_slot = self.local_slots.get(name).copied(); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + let slot = self.declare_local(name, php_type.clone()); + self.builder + .widen_local_storage_type(slot, php_type.clone()); + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| self.initialized_slots.contains(&slot)) + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| !self.initialized_slots.contains(&slot)) + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_none() + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + let source = value; + let release_source_after_store = self.value_needs_release_after_retaining_store(value); + let stored = crate::ir_lower::ownership::acquire_if_refcounted(self, value, span); + self.store_slot_with_op(slot, stored, Op::StoreLocal, span); + self.set_local_type(name, php_type); + if release_source_after_store { + crate::ir_lower::ownership::release_if_owned(self, source, span); + } + stored + } + /// Returns the declared PHP type for an extern global visible as a variable. fn extern_global_type(&self, name: &str) -> Option { self.extern_globals.get(name).cloned() @@ -810,12 +1165,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits a write to a C extern global symbol using the already-lowered source value. - fn store_extern_global_name( - &mut self, - name: &str, - value: LoweredValue, - span: Option, - ) { + fn store_extern_global_name(&mut self, name: &str, value: LoweredValue, span: Option) { let data = self.intern_global_name(name); self.builder.emit_with_effects( Op::ExternGlobalStore, @@ -838,8 +1188,17 @@ impl<'m, 'f> LoweringContext<'m, 'f> { span: Option, ) -> LoweredValue { self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); + self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); - let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); let slot = self.declare_local(name, php_type.clone()); if uses_global { @@ -847,8 +1206,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.set_local_type(name, php_type); return value; } - let is_ref_bound = - self.is_ref_bound_local(name) && previous_kind == LocalKind::PhpLocal; + let is_ref_bound = self.is_ref_bound_local(name) && previous_kind == LocalKind::PhpLocal; match (is_ref_bound, previous_kind) { (true, _) => self.store_ref_cell_slot(slot, value, php_type, span), (false, LocalKind::StaticLocal) => { @@ -864,11 +1222,21 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits `unset($local)`, breaking by-reference aliases without writing through them. - pub(crate) fn unset_local(&mut self, name: &str, null: LoweredValue, span: Option) -> LoweredValue { + pub(crate) fn unset_local( + &mut self, + name: &str, + null: LoweredValue, + span: Option, + ) -> LoweredValue { if !self.is_ref_bound_local(name) { return self.store_local(name, null, PhpType::Void, span); } self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); + self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); let slot = self.declare_local(name, PhpType::Void); self.release_ref_cell_owner(name, span); @@ -935,6 +1303,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.promote_local_ref_cell(source, span); } self.clear_static_callable_local(target); + self.clear_reflection_class_local(target); + self.clear_reflection_function_local(target); + self.clear_reflection_property_local(target); + self.clear_reflection_method_local(target); + self.clear_reflection_arg_array_local(target); self.clear_fiber_start_sig(target); self.release_replaced_local_before_ref_alias(target, span); let source_slot = self.declare_local(source, source_ty.clone()); @@ -1082,8 +1455,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::HashArrayUnion | Op::ArrayToHash | Op::ObjectNew + | Op::ObjectCloneShallow | Op::DynamicObjectNew | Op::DynamicObjectNewMixed + | Op::DynamicObjectNewWithoutConstructorMixed | Op::ClosureNew | Op::FirstClassCallableNew | Op::CallableArrayNew @@ -1096,6 +1471,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::GeneratorYieldFrom | Op::Call | Op::FunctionVariantCall + | Op::EvalLiteralCall + | Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalConstantFetch + | Op::EvalStaticMethodCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -1160,10 +1540,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let op = self.builder.value_defining_op(value); (matches!(php_type, PhpType::Mixed | PhpType::Union(_)) || (php_type.is_refcounted() && php_type != PhpType::Str)) - && matches!( - op, - Some(Op::ArrayGet | Op::HashGet) - ) + && matches!(op, Some(Op::ArrayGet | Op::HashGet)) } /// Returns true for builtin calls whose return value is newly allocated for the caller. @@ -1185,16 +1562,16 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Returns true when straight-line callable binding metadata is safe for a local. pub(crate) fn can_track_static_callable_local(&self, name: &str) -> bool { - let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); !self.uses_global_storage(name, kind) && kind == LocalKind::PhpLocal } /// Records that a PHP local currently holds a compile-time-known callable. - pub(crate) fn bind_static_callable_local( - &mut self, - name: &str, - target: StaticCallableBinding, - ) { + pub(crate) fn bind_static_callable_local(&mut self, name: &str, target: StaticCallableBinding) { if self.can_track_static_callable_local(name) { self.static_callable_locals.insert(name.to_string(), target); } @@ -1205,6 +1582,85 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.static_callable_locals.get(name).cloned() } + /// Records that a PHP local currently holds a statically-known `ReflectionClass` object. + pub(crate) fn bind_reflection_class_local(&mut self, name: &str, reflected_class: String) { + if self.can_track_static_callable_local(name) { + self.reflection_class_locals + .insert(name.to_string(), reflected_class); + } + } + + /// Returns the reflected class associated with a local `ReflectionClass`, if known. + pub(crate) fn reflection_class_local(&self, name: &str) -> Option { + self.reflection_class_locals.get(name).cloned() + } + + /// Records that a PHP local currently holds a statically-known `ReflectionFunction`. + pub(crate) fn bind_reflection_function_local( + &mut self, + name: &str, + reflected_function: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_function_locals + .insert(name.to_string(), reflected_function); + } + } + + /// Returns the reflected function associated with a local `ReflectionFunction`. + pub(crate) fn reflection_function_local(&self, name: &str) -> Option { + self.reflection_function_locals.get(name).cloned() + } + + /// Records that a PHP local currently holds a statically-known `ReflectionProperty` object. + pub(crate) fn bind_reflection_property_local( + &mut self, + name: &str, + reflected_class: String, + reflected_property: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_property_locals + .insert(name.to_string(), (reflected_class, reflected_property)); + } + } + + /// Returns the reflected class/property associated with a local `ReflectionProperty`. + pub(crate) fn reflection_property_local(&self, name: &str) -> Option<(String, String)> { + self.reflection_property_locals.get(name).cloned() + } + + /// Records that a PHP local currently holds a statically-known `ReflectionMethod` object. + pub(crate) fn bind_reflection_method_local( + &mut self, + name: &str, + reflected_class: String, + reflected_method: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_method_locals + .insert(name.to_string(), (reflected_class, reflected_method)); + } + } + + /// Returns the reflected class/method associated with a local `ReflectionMethod`. + pub(crate) fn reflection_method_local(&self, name: &str) -> Option<(String, String)> { + self.reflection_method_locals.get(name).cloned() + } + + /// Records that a PHP local currently holds a safe static argument array for reflection. + pub(crate) fn bind_reflection_arg_array_local(&mut self, name: &str, args: Vec) { + if self.can_track_static_callable_local(name) { + self.reflection_arg_array_locals + .insert(name.to_string(), args); + } + } + + /// Returns the static reflection argument array associated with a local. + pub(crate) fn reflection_arg_array_local(&self, name: &str) -> Option> { + self.reflection_arg_array_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a Fiber with a known callback signature. pub(crate) fn bind_fiber_start_sig(&mut self, name: &str, sig: FunctionSig) { if self.can_track_static_callable_local(name) { @@ -1233,6 +1689,31 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.static_callable_locals.remove(name); } + /// Clears the compile-time `ReflectionClass` association for one local. + pub(crate) fn clear_reflection_class_local(&mut self, name: &str) { + self.reflection_class_locals.remove(name); + } + + /// Clears the compile-time `ReflectionFunction` association for one local. + pub(crate) fn clear_reflection_function_local(&mut self, name: &str) { + self.reflection_function_locals.remove(name); + } + + /// Clears the compile-time `ReflectionProperty` association for one local. + pub(crate) fn clear_reflection_property_local(&mut self, name: &str) { + self.reflection_property_locals.remove(name); + } + + /// Clears the compile-time `ReflectionMethod` association for one local. + pub(crate) fn clear_reflection_method_local(&mut self, name: &str) { + self.reflection_method_locals.remove(name); + } + + /// Clears the compile-time reflection argument-array association for one local. + pub(crate) fn clear_reflection_arg_array_local(&mut self, name: &str) { + self.reflection_arg_array_locals.remove(name); + } + /// Clears the known Fiber callback association for one local. pub(crate) fn clear_fiber_start_sig(&mut self, name: &str) { self.fiber_start_sigs.remove(name); @@ -1241,6 +1722,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Clears all compile-time callable associations after a control-flow join. pub(crate) fn clear_static_callable_locals(&mut self) { self.static_callable_locals.clear(); + self.reflection_class_locals.clear(); + self.reflection_function_locals.clear(); + self.reflection_property_locals.clear(); + self.reflection_method_locals.clear(); + self.reflection_arg_array_locals.clear(); self.fiber_start_sigs.clear(); } @@ -1354,7 +1840,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let ownership = Ownership::for_php_type(&php_type); let value = self .builder - .emit_with_effects(op, operands, immediate, ir_type, php_type, ownership, effects, span) + .emit_with_effects( + op, operands, immediate, ir_type, php_type, ownership, effects, span, + ) .expect("value opcode produces a value"); LoweredValue { value, ir_type } } @@ -1379,7 +1867,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { fn local_kind_uses_plain_store_cleanup(kind: LocalKind) -> bool { matches!( kind, - LocalKind::PhpLocal | LocalKind::HiddenTemp | LocalKind::OwnedTemp | LocalKind::NamedArgTemp + LocalKind::PhpLocal + | LocalKind::HiddenTemp + | LocalKind::OwnedTemp + | LocalKind::NamedArgTemp ) } @@ -1399,6 +1890,7 @@ fn builtin_call_result_owns_storage_as_temporary(name: &str) -> bool { | "array_column" | "array_combine" | "array_diff" + | "eval" | "array_fill" | "array_fill_keys" | "array_intersect" @@ -1431,6 +1923,14 @@ fn builtin_call_result_owns_storage_as_temporary(name: &str) -> bool { ) } +/// Returns true when eval can replace a local value with an arbitrary boxed cell. +fn eval_barrier_can_widen(php_type: &PhpType) -> bool { + !matches!( + php_type.codegen_repr(), + PhpType::Never | PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) + ) +} + /// Converts an owner function name into a valid fragment for synthetic closure names. fn closure_name_fragment(value: &str) -> String { value @@ -1469,10 +1969,14 @@ pub(crate) fn type_expr_to_php_type(type_expr: &TypeExpr) -> PhpType { TypeExpr::Never => PhpType::Never, TypeExpr::Iterable => PhpType::Iterable, TypeExpr::Array(inner) => PhpType::Array(Box::new(type_expr_to_php_type(inner))), - TypeExpr::Ptr(name) => PhpType::Pointer(name.as_ref().map(|name| name.as_str().to_string())), + TypeExpr::Ptr(name) => { + PhpType::Pointer(name.as_ref().map(|name| name.as_str().to_string())) + } TypeExpr::Buffer(inner) => PhpType::Buffer(Box::new(type_expr_to_php_type(inner))), TypeExpr::Named(name) => named_type_expr_to_php_type(name.as_str()), - TypeExpr::Nullable(inner) => PhpType::Union(vec![PhpType::Void, type_expr_to_php_type(inner)]), + TypeExpr::Nullable(inner) => { + PhpType::Union(vec![PhpType::Void, type_expr_to_php_type(inner)]) + } TypeExpr::Union(members) => { PhpType::Union(members.iter().map(type_expr_to_php_type).collect()) } @@ -1490,6 +1994,7 @@ fn named_type_expr_to_php_type(name: &str) -> PhpType { "array" => PhpType::Array(Box::new(PhpType::Mixed)), "callable" => PhpType::Callable, "mixed" => PhpType::Mixed, + "object" => PhpType::Object(String::new()), _ => PhpType::Object(name.to_string()), } } diff --git a/src/ir_lower/expr/constants.rs b/src/ir_lower/expr/constants.rs index 6755e7937d..22d26d1f1f 100644 --- a/src/ir_lower/expr/constants.rs +++ b/src/ir_lower/expr/constants.rs @@ -48,6 +48,20 @@ pub(super) fn lower_static_defined_call( return None; }; let exists = ctx.constant_value(constant_name).is_some(); + if !exists && (ctx.has_eval_barrier() || ctx.eval_executed()) { + // Barrier-free AOT evals can still define constants dynamically; the + // probe needs the eval context, so make sure its slot exists. + ctx.declare_eval_context_local(); + let data = ctx.intern_global_name(constant_name); + return Some(ctx.emit_value( + Op::EvalConstantExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalConstantExists.default_effects(), + Some(expr.span), + )); + } Some(emit_typed_constant( ctx, Op::ConstBool, @@ -66,6 +80,20 @@ pub(super) fn lower_const_ref( if let Some((value, php_type)) = ctx.constant_value(name.as_str()) { return lower_constant_value(ctx, value, php_type, expr); } + if ctx.has_eval_barrier() || ctx.eval_executed() { + // Barrier-free AOT evals can still define constants dynamically; the + // fetch needs the eval context, so make sure its slot exists. + ctx.declare_eval_context_local(); + let data = ctx.intern_global_name(name.as_str()); + return ctx.emit_value( + Op::EvalConstantFetch, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalConstantFetch.default_effects(), + Some(expr.span), + ); + } let data = ctx.intern_global_name(name.as_str()); ctx.emit_value( Op::LoadGlobal, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 71a2f70ede..a4cf283cf5 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -11,15 +11,15 @@ //! conservative effects until Phase 04 gives them target-specific meaning. use crate::ir::{ - BlockId, CmpPredicate, Effects, Immediate, IrHeapKind, IrType, LocalSlotId, MixedNumericOp, Op, - Ownership, Terminator, ValueId, + BlockId, CmpPredicate, Effects, Immediate, IrHeapKind, IrType, LocalKind, LocalSlotId, + MixedNumericOp, Op, Ownership, Terminator, ValueId, }; use crate::ir_lower::context::{ value_ir_type, ClosureCapture, LoweredValue, LoweringContext, StaticCallableBinding, }; use crate::ir_lower::effects_lookup; use crate::ir_lower::function; -use crate::names::{php_symbol_key, property_hook_get_method, Name}; +use crate::names::{php_symbol_key, property_hook_get_method, property_hook_set_method, Name}; use crate::parser::ast::{ is_compound_assignment_self_read, BinOp, CallableTarget, CastType, Expr, ExprKind, InstanceOfTarget, MagicConstant, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, @@ -100,6 +100,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -110,6 +111,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ctx, params, variadic.as_deref(), + *variadic_by_ref, return_type.as_ref(), body, captures, @@ -123,6 +125,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::ExprCall { callee, args } => lower_expr_call(ctx, callee, args, expr), ExprKind::ConstRef(name) => constants::lower_const_ref(ctx, name, expr), ExprKind::NewObject { class_name, args } => lower_new_object(ctx, class_name, args, expr), + ExprKind::Clone(inner) => lower_clone(ctx, inner, expr), ExprKind::NewDynamic { name_expr, args } => { lower_new_dynamic(ctx, name_expr, args, expr) } @@ -140,13 +143,24 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::StaticPropertyAccess { receiver, property } => { lower_static_property_get(ctx, receiver, property, expr) } - ExprKind::MethodCall { object, method, args } => lower_method_call(ctx, object, method, args, Op::MethodCall, expr), - ExprKind::NullsafeMethodCall { object, method, args } => { - lower_nullsafe_method_call(ctx, object, method, args, expr) - } - ExprKind::StaticMethodCall { receiver, method, args } => { - lower_static_method_call(ctx, receiver, method, args, expr) + ExprKind::MethodCall { + object, + method, + args, + } => lower_method_call(ctx, object, method, args, Op::MethodCall, expr), + ExprKind::NullsafeMethodCall { + object, + method, + args, + } => lower_nullsafe_method_call(ctx, object, method, args, expr), + ExprKind::NullsafeDynamicMethodCall { .. } => { + unreachable!("nullsafe dynamic method calls are lowered as a nullsafe postfix chain") } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => lower_static_method_call(ctx, receiver, method, args, expr), ExprKind::FirstClassCallable(target) => lower_first_class_callable(ctx, target, expr), ExprKind::This => ctx.load_local("this", Some(expr.span)), ExprKind::PtrCast { target_type, expr: inner } => lower_ptr_cast(ctx, target_type, inner, expr), @@ -679,11 +693,13 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool { | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } | ExprKind::NewObject { .. } | ExprKind::NewDynamic { .. } | ExprKind::NewDynamicObject { .. } | ExprKind::NewScopedObject { .. } + | ExprKind::Clone(_) | ExprKind::Pipe { .. } | ExprKind::Yield { .. } | ExprKind::YieldFrom(_) => true, @@ -1170,7 +1186,9 @@ fn lower_null_coalesce( let result_type = null_coalesce_result_type(ctx, value.value, default); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); let split_initialized = ctx.initialized_slots_snapshot(); - let default_block = ctx.builder.create_named_block("coalesce.default", Vec::new()); + let default_block = ctx + .builder + .create_named_block("coalesce.default", Vec::new()); let value_block = ctx.builder.create_named_block("coalesce.value", Vec::new()); let merge = ctx.builder.create_named_block("coalesce.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { @@ -1285,9 +1303,15 @@ fn lower_short_ternary( let result_type = fallback_expr_type(expr); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); let split_initialized = ctx.initialized_slots_snapshot(); - let value_block = ctx.builder.create_named_block("short_ternary.value", Vec::new()); - let default_block = ctx.builder.create_named_block("short_ternary.default", Vec::new()); - let merge = ctx.builder.create_named_block("short_ternary.merge", Vec::new()); + let value_block = ctx + .builder + .create_named_block("short_ternary.value", Vec::new()); + let default_block = ctx + .builder + .create_named_block("short_ternary.default", Vec::new()); + let merge = ctx + .builder + .create_named_block("short_ternary.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: cond.value, then_target: value_block, @@ -1497,6 +1521,14 @@ fn lower_assignment_expr( } } let static_callable = assigned_name.and_then(|_| static_callable_binding_for_expr(ctx, value)); + let reflected_class = assigned_name.and_then(|_| reflection_class_binding_for_expr(ctx, value)); + let reflected_function = + assigned_name.and_then(|_| reflection_function_binding_for_expr(ctx, value)); + let reflected_property = + assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); + let reflected_method = + assigned_name.and_then(|_| reflection_method_binding_for_expr(ctx, value)); + let reflected_args = assigned_name.and_then(|_| reflection_arg_array_binding_for_expr(value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1531,6 +1563,21 @@ fn lower_assignment_expr( if let Some(target) = static_callable { ctx.bind_static_callable_local(name, target); } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } + if let Some(reflected_function) = reflected_function { + ctx.bind_reflection_function_local(name, reflected_function); + } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } + if let Some((reflected_class, reflected_method)) = reflected_method { + ctx.bind_reflection_method_local(name, reflected_class, reflected_method); + } + if let Some(reflected_args) = reflected_args { + ctx.bind_reflection_arg_array_local(name, reflected_args); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -1785,6 +1832,12 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E if let Some(value) = lower_static_is_callable(ctx, canonical, args, expr) { return value; } + if let Some(value) = lower_eval_function_probe(ctx, canonical, args, expr) { + return value; + } + if let Some(value) = lower_eval_class_probe(ctx, canonical, args, expr) { + return value; + } let sig = call_signature(ctx, canonical, args); let is_extern = ctx.extern_functions.contains_key(canonical); let is_user_function = ctx.functions.contains_key(canonical); @@ -1831,7 +1884,23 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), expr.span); return call; } - emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span) + if ctx.has_eval_barrier() + && plain_positional_call_args(args) + && canonical_builtin_function_name(canonical).is_none() + { + let dynamic_name = php_symbol_key(canonical.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + return ctx.emit_value( + Op::EvalFunctionCall, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCall.default_effects(), + Some(expr.span), + ); + } + let eval_literal = eval_literal_fragment(canonical, args); + emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span, eval_literal) } /// Emits a builtin call and releases owned temporary arguments after the call consumes them. @@ -1841,7226 +1910,11434 @@ fn emit_builtin_call_value( operands: Vec, php_type: PhpType, span: Span, + eval_literal: Option<&str>, ) -> LoweredValue { - let data = ctx.intern_function_name(name); + let (op, immediate, effects) = if let Some(fragment) = eval_literal { + ( + Op::EvalLiteralCall, + Some(Immediate::Data(ctx.intern_string(fragment))), + Op::EvalLiteralCall.default_effects(), + ) + } else { + ( + Op::BuiltinCall, + Some(Immediate::Data(ctx.intern_function_name(name))), + effects_lookup::builtin_effects(name), + ) + }; let call = ctx.emit_value( - Op::BuiltinCall, + op, operands.clone(), - Some(Immediate::Data(data)), + immediate, php_type, - effects_lookup::builtin_effects(name), + effects, Some(span), ); release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), span); + let eval_needs_barrier = match eval_literal { + Some(fragment) => eval_literal_needs_barrier(ctx, fragment), + None => true, + }; + if php_symbol_key(name.trim_start_matches('\\')) == "eval" { + ctx.mark_eval_executed(); + if let Some(widen_targets) = + eval_literal.and_then(|fragment| eval_literal_direct_store_widen_targets(ctx, fragment)) + { + // A direct store that changes a scalar slot's type keeps the + // native path by widening the slot to boxed Mixed storage first; + // codegen re-lowers every load/store with the final slot type. + for name in widen_targets { + ctx.set_local_type(&name, PhpType::Mixed); + } + } + if eval_needs_barrier { + ctx.apply_eval_barrier(); + } else if eval_literal + .is_some_and(|fragment| eval_literal_needs_scope_barrier(ctx, fragment)) + { + ctx.apply_eval_scope_barrier(); + } + } call } -/// Lowers `isset()` as a lazy language construct instead of an eager builtin call. -fn lower_lazy_isset( - ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], - expr: &Expr, -) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "isset" { - return None; +/// Returns true when a literal `eval` call may still need runtime scope/interpreter state. +fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + // Fragments fully handled by the direct-local AOT paths never touch runtime + // eval state; applying the barrier would declare eval locals and drag the + // whole bridge runtime (and magician staticlib) into native-only programs. + if eval_literal_direct_store_supported_by_lowering(ctx, fragment) { + return false; } - if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { - return None; + if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { + return false; } - if args.is_empty() { - return Some(lower_bool_literal(ctx, false, expr)); + if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { + return false; } - - let temp_name = ctx.declare_hidden_temp(PhpType::Bool); - let false_block = ctx.builder.create_named_block("isset.lazy_false", Vec::new()); - let merge = ctx.builder.create_named_block("isset.lazy_merge", Vec::new()); - for (idx, arg) in args.iter().enumerate() { - let checked = lower_lazy_isset_operand(ctx, arg).unwrap_or_else(|| { - let value = lower_expr(ctx, arg); - emit_builtin_call_value(ctx, name, vec![value.value], PhpType::Int, arg.span) - }); - let then_target = if idx + 1 == args.len() { - ctx.builder.create_named_block("isset.lazy_true", Vec::new()) - } else { - ctx.builder.create_named_block("isset.lazy_next", Vec::new()) - }; - ctx.builder.terminate(Terminator::CondBr { - cond: checked.value, - then_target, - then_args: Vec::new(), - else_target: false_block, - else_args: Vec::new(), - }); - ctx.builder.position_at_end(then_target); + let static_call_supported = |name: &str, args: &[Expr]| { + eval_literal_static_function_supported_by_lowering(ctx, name, args) + }; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.source_path(), + static_call_supported, + |receiver, method, args| { + eval_literal_static_method_supported_by_lowering(ctx, receiver, method, args) + }, + ); + if plan.is_fully_static_no_bridge() { + return false; } + if plan.uses_scope_read_params() + && eval_literal_scope_read_params_supported_by_lowering( + ctx, + plan.reads(), + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) + { + return false; + } + if plan.requires_runtime_eval_scope() + && eval_literal_scope_constraints_supported_by_lowering( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) + { + return false; + } + true +} - let true_value = lower_bool_literal(ctx, true, expr); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, true_value, expr.span); - branch_to(ctx, merge); - - ctx.builder.position_at_end(false_block); - let false_value = lower_bool_literal(ctx, false, expr); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, expr.span); - branch_to(ctx, merge); +/// Returns true when a literal `eval` only needs materialized eval-scope state. +fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + // Direct-local AOT fragments also skip the materialized scope: their reads + // and writes go straight to caller locals. + if eval_literal_direct_store_supported_by_lowering(ctx, fragment) { + return false; + } + if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { + return false; + } + if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { + return false; + } + let static_call_supported = |name: &str, args: &[Expr]| { + eval_literal_static_function_supported_by_lowering(ctx, name, args) + }; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.source_path(), + static_call_supported, + |receiver, method, args| { + eval_literal_static_method_supported_by_lowering(ctx, receiver, method, args) + }, + ); + plan.requires_runtime_eval_scope() + && eval_literal_scope_constraints_supported_by_lowering( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) +} - ctx.builder.position_at_end(merge); - Some(take_owned_temp(ctx, &temp_name, expr.span)) +/// Returns true when a direct-store eval fragment fits every caller slot type. +fn eval_literal_direct_store_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + eval_literal_direct_store_widen_targets(ctx, fragment).is_some() } -/// Lowers a single `isset()` operand that has special lazy PHP semantics. -fn lower_lazy_isset_operand( - ctx: &mut LoweringContext<'_, '_>, - arg: &Expr, -) -> Option { - match &arg.kind { - ExprKind::ArrayAccess { array, index } => { - if array_access_expr_satisfies_array_access(ctx, array) { - let synthetic = Expr::new( - ExprKind::MethodCall { - object: array.clone(), - method: "offsetExists".to_string(), - args: vec![(**index).clone()], - }, - arg.span, - ); - return Some(lower_expr(ctx, &synthetic)); - } - if !array_access_expr_supports_native_isset_probe(ctx, array) { - return None; - } - Some(lower_native_isset_offset_probe(ctx, array, index, arg)) +/// Classifies a direct-store eval fragment against caller slots. Returns the +/// locals whose scalar slot must widen to Mixed because the store changes +/// their runtime type (e.g. Int -> Str); `None` when any write target rules +/// out the direct path entirely. +fn eval_literal_direct_store_widen_targets( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> Option> { + let writes = crate::eval_aot::literal_fragment_direct_local_store_writes(fragment)?; + let mut widen_targets = Vec::new(); + for (name, kind) in &writes { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return None; } - // `isset($obj->prop)` on an undeclared property dispatches to `__isset`. - ExprKind::PropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let synthetic = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - Some(lower_expr(ctx, &synthetic)) + if ctx.local_slots.get(name).is_none() { + continue; } - ExprKind::NullsafePropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let synthetic = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - Some(lower_expr(ctx, &synthetic)) + if ctx.is_ref_bound_local(name) + || ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) + { + return None; } - // `isset($this)` inside a static closure always evaluates to `false` - // because static closures have no `$this` binding. PHP allows this - // probe and returns false; elephc must not try to load a missing slot. - ExprKind::This if !ctx.local_slots.contains_key("this") => { - Some(lower_bool_literal(ctx, false, arg)) + let ty = ctx.local_types.get(name)?; + if eval_literal_direct_store_type_supported(ty, *kind) { + continue; + } + if eval_literal_direct_store_type_widenable(ty) { + widen_targets.push(name.clone()); + } else { + return None; } - _ => None, } + Some(widen_targets) } -/// Lowers `empty($obj->magicProp)` with PHP's overloaded-property semantics: -/// `empty` consults `__isset` first and only evaluates `__get` when `__isset` -/// is truthy, so an unset virtual property is empty without ever reading it. -/// Returns `None` for operands the eager `empty` builtin already handles (plain -/// variables, declared properties, array elements), letting that path run. -fn lower_lazy_empty( - ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], - expr: &Expr, -) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "empty" { - return None; +/// Returns true when a scalar slot can widen to Mixed for a type-changing +/// direct eval store. Containers and special storage keep the barrier path. +fn eval_literal_direct_store_type_widenable(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Bool | PhpType::Str | PhpType::TaggedScalar + ) +} + +/// Returns true when a static scalar store kind fits an existing caller slot type. +fn eval_literal_direct_store_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::Str => kind == crate::eval_aot::DirectLocalStoreScalarKind::String, + PhpType::TaggedScalar => matches!( + kind, + crate::eval_aot::DirectLocalStoreScalarKind::Int + | crate::eval_aot::DirectLocalStoreScalarKind::Null + ), + _ => false, } - if args.len() != 1 - || crate::types::call_args::has_named_args(args) - || args.iter().any(is_spread_arg) +} + +/// Returns true when a boxed read/write eval can use direct caller locals. +fn eval_literal_direct_read_write_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_direct_local_read_write_writes(fragment) + else { + return false; + }; + writes + .iter() + .all(|(name, kind)| eval_literal_direct_read_write_name_supported(ctx, name, *kind)) +} + +/// Returns true when one direct read/write target is an initialized scalar local. +fn eval_literal_direct_read_write_name_supported( + ctx: &LoweringContext<'_, '_>, + name: &str, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - return None; + return false; } - let (exists_call, get_call) = lazy_empty_magic_property_calls(ctx, &args[0])?; - - let temp_name = ctx.declare_hidden_temp(PhpType::Bool); - let present_block = ctx.builder.create_named_block("empty.present", Vec::new()); - let absent_block = ctx.builder.create_named_block("empty.absent", Vec::new()); - let merge = ctx.builder.create_named_block("empty.merge", Vec::new()); + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + if !ctx.initialized_slots_snapshot().contains(slot) { + return false; + } + ctx.local_types + .get(name) + .is_some_and(|ty| eval_literal_direct_read_write_type_supported(ty, kind)) +} - // `__isset(prop)` decides whether the property is considered set at all. - let exists = lower_expr(ctx, &exists_call); - ctx.builder.terminate(Terminator::CondBr { - cond: exists.value, - then_target: present_block, - then_args: Vec::new(), - else_target: absent_block, - else_args: Vec::new(), - }); +/// Returns true when a read/write eval result kind fits the caller local type. +fn eval_literal_direct_read_write_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + _ => false, + } +} - // Set: empty is the emptiness of the `__get` value (reuses the eager builtin). - ctx.builder.position_at_end(present_block); - let get_value = lower_expr(ctx, &get_call); - let empty_name = ctx.intern_function_name(name); - let empty_value = ctx.emit_value( - Op::BuiltinCall, - vec![get_value.value], - Some(Immediate::Data(empty_name)), - PhpType::Bool, - effects_lookup::builtin_effects(name), - Some(expr.span), - ); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, empty_value, expr.span); - branch_to(ctx, merge); +/// Returns true when local-scalar eval writes can be synced without eval scope state. +fn eval_literal_local_scalar_direct_sync_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_local_scalar_writes_with_static_calls( + fragment, + |name, args| eval_literal_static_function_supported_by_lowering(ctx, name, args), + ) else { + return false; + }; + writes + .iter() + .all(|(name, kind)| eval_literal_local_scalar_direct_sync_name_supported(ctx, name, *kind)) +} - // Not set: empty is true and `__get` is never called. - ctx.builder.position_at_end(absent_block); - let true_value = lower_bool_literal(ctx, true, expr); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, true_value, expr.span); - branch_to(ctx, merge); +/// Returns true when one local-scalar write can target caller storage directly or be ignored. +fn eval_literal_local_scalar_direct_sync_name_supported( + ctx: &LoweringContext<'_, '_>, + name: &str, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(_slot) = ctx.local_slots.get(name) else { + return true; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_local_scalar_direct_sync_type_supported(ty, kind) +} - ctx.builder.position_at_end(merge); - Some(ctx.load_local(&temp_name, Some(expr.span))) +/// Returns true when a local-scalar write kind fits the caller local type. +fn eval_literal_local_scalar_direct_sync_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::TaggedScalar => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + _ => false, + } } -/// For an `empty()` operand that is an overloaded (magic) property access, -/// returns the `(__isset, __get)` synthetic call expressions PHP would evaluate. -/// The property name is a string literal, so reusing it for both calls is -/// side-effect free. Returns `None` for any other operand shape. -fn lazy_empty_magic_property_calls( +/// Returns true when all scope-read variables can be passed as direct Mixed params. +fn eval_literal_scope_read_params_supported_by_lowering( ctx: &LoweringContext<'_, '_>, - arg: &Expr, -) -> Option<(Expr, Expr)> { - match &arg.kind { - ExprKind::PropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let key = Expr::new(ExprKind::StringLiteral(property.clone()), arg.span); - let exists = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![key.clone()], - }, - arg.span, - ); - let get = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__get".to_string(), - args: vec![key], - }, - arg.span, - ); - Some((exists, get)) - } - ExprKind::NullsafePropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let key = Expr::new(ExprKind::StringLiteral(property.clone()), arg.span); - let exists = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![key.clone()], - }, - arg.span, - ); - let get = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__get".to_string(), - args: vec![key], - }, - arg.span, - ); - Some((exists, get)) - } - _ => None, + read_names: &std::collections::BTreeSet, + array_read_constraints: &std::collections::BTreeSet, + assoc_array_read_constraints: &std::collections::BTreeSet, + float_predicate_read_constraints: &std::collections::BTreeSet, +) -> bool { + read_names + .iter() + .all(|name| eval_literal_scope_read_param_supported_by_lowering(ctx, name)) + && array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_array_param_supported_by_lowering(ctx, name)) + && assoc_array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_assoc_array_param_supported_by_lowering(ctx, name)) + && float_predicate_read_constraints.iter().all(|name| { + eval_literal_scope_read_float_predicate_param_supported_by_lowering(ctx, name) + }) +} + +/// Returns true when all constrained scope reads fit caller local types. +fn eval_literal_scope_constraints_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + array_read_constraints: &std::collections::BTreeSet, + assoc_array_read_constraints: &std::collections::BTreeSet, + float_predicate_read_constraints: &std::collections::BTreeSet, +) -> bool { + array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_array_param_supported_by_lowering(ctx, name)) + && assoc_array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_assoc_array_param_supported_by_lowering(ctx, name)) + && float_predicate_read_constraints.iter().all(|name| { + eval_literal_scope_read_float_predicate_param_supported_by_lowering(ctx, name) + }) +} + +/// Returns true when one read variable has no eval runtime state dependency. +fn eval_literal_scope_read_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return true; + }; + if ctx.is_ref_bound_local(name) { + return false; } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) } -/// Returns the class whose `magic` method (`__isset`/`__unset`) should handle -/// `isset($obj->prop)` / `unset($obj->prop)`: an undeclared property on an -/// object whose class declares the magic method. Returns `None` (normal -/// handling) for declared properties or classes without the hook. -fn property_existence_magic_class( +/// Returns true when one direct read-param is statically known to be array-like. +fn eval_literal_scope_read_array_param_supported_by_lowering( ctx: &LoweringContext<'_, '_>, - object: &Expr, - property: &str, - magic: &str, -) -> Option { - let class_name = instance_callable_object_class(ctx, object)?; - let class_info = ctx.classes.get(&class_name)?; - if class_info.properties.iter().any(|(name, _)| name == property) { - return None; + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; } - class_info.methods.contains_key(magic).then_some(class_name) + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_array_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) } -/// Lowers native array/hash `isset($array[$key])` without reading the element value. -fn lower_native_isset_offset_probe( - ctx: &mut LoweringContext<'_, '_>, - array: &Expr, - index: &Expr, - expr: &Expr, -) -> LoweredValue { - let array_value = lower_expr(ctx, array); - if value_is_nullable(ctx, array_value.value) { - return lower_nullable_native_isset_offset_probe(ctx, array_value, index, expr); +/// Returns true when one direct read-param is statically known to be associative-array-like. +fn eval_literal_scope_read_assoc_array_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; } - lower_native_isset_offset_probe_from_value(ctx, array_value, index, expr) + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_assoc_array_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) } -/// Lowers nullable native array/hash `isset` without evaluating the offset on null receivers. -fn lower_nullable_native_isset_offset_probe( - ctx: &mut LoweringContext<'_, '_>, - array_value: LoweredValue, - index: &Expr, - expr: &Expr, -) -> LoweredValue { - let is_null = ctx.emit_value( - Op::IsNull, - vec![array_value.value], - None, - PhpType::Bool, - Op::IsNull.default_effects(), - Some(expr.span), - ); - let temp_name = ctx.declare_hidden_temp(PhpType::Bool); - let null_block = ctx.builder.create_named_block("isset.native.null", Vec::new()); - let probe_block = ctx.builder.create_named_block("isset.native.probe", Vec::new()); - let merge = ctx.builder.create_named_block("isset.native.merge", Vec::new()); - ctx.builder.terminate(Terminator::CondBr { - cond: is_null.value, - then_target: null_block, - then_args: Vec::new(), - else_target: probe_block, - else_args: Vec::new(), - }); +/// Returns true when one direct read-param can feed float predicate builtins safely. +fn eval_literal_scope_read_float_predicate_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_float_predicate_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) +} - ctx.builder.position_at_end(null_block); - let false_value = emit_bool_literal(ctx, false, Some(expr.span)); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, expr.span); - branch_to(ctx, merge); +/// Returns true when a local type can be boxed to the param-mode Mixed ABI. +fn eval_literal_scope_read_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Void + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + | PhpType::Mixed + | PhpType::Union(_) + ) +} - ctx.builder.position_at_end(probe_block); - let checked = lower_native_isset_offset_probe_from_value(ctx, array_value, index, expr); - store_value_into_temp(ctx, &temp_name, PhpType::Bool, checked, expr.span); - branch_to(ctx, merge); +/// Returns true when a local type satisfies array-only direct read-param semantics. +fn eval_literal_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} - ctx.builder.position_at_end(merge); - take_owned_temp(ctx, &temp_name, expr.span) +/// Returns true when a local type satisfies associative-array direct read-param semantics. +fn eval_literal_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) } -/// Lowers native array/hash `isset` once the receiver has already been evaluated. -fn lower_native_isset_offset_probe_from_value( - ctx: &mut LoweringContext<'_, '_>, - array_value: LoweredValue, - index: &Expr, - expr: &Expr, -) -> LoweredValue { - match array_value.ir_type { - IrType::Heap(IrHeapKind::Array) => { - let mut index_value = lower_expr(ctx, index); - let index_ty = index_expr_key_type(ctx, index); - if index_ty == PhpType::Int { - index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); - ctx.emit_value( - Op::ArrayIsset, - vec![array_value.value, index_value.value], - None, - PhpType::Bool, - Op::ArrayIsset.default_effects(), - Some(expr.span), - ) - } else { - // String or mixed key on indexed storage: read through the - // mixed-key runtime path and check if the result is null. - let read_value = ctx.emit_value( - Op::ArrayGetMixedKey, - vec![array_value.value, index_value.value], - None, - PhpType::Mixed, - Op::ArrayGetMixedKey.default_effects(), - Some(expr.span), - ); - let is_null = ctx.emit_value( - Op::IsNull, - vec![read_value.value], - None, - PhpType::Bool, - Op::IsNull.default_effects(), - Some(expr.span), - ); - let zero = ctx.emit_value( - Op::ConstI64, - Vec::new(), - Some(Immediate::I64(0)), - PhpType::Int, - Op::ConstI64.default_effects(), - Some(expr.span), - ); - ctx.emit_value( - Op::ICmp, - vec![is_null.value, zero.value], - Some(Immediate::CmpPredicate(crate::ir::CmpPredicate::Eq)), - PhpType::Bool, - Op::ICmp.default_effects(), - Some(expr.span), - ) - } - } - IrType::Heap(IrHeapKind::Hash) => { - let index_value = lower_expr(ctx, index); - ctx.emit_value( - Op::HashIsset, - vec![array_value.value, index_value.value], - None, - PhpType::Bool, - Op::HashIsset.default_effects(), - Some(expr.span), - ) - } - _ => { - let read_value = lower_array_access_from_value(ctx, array_value, index, expr, false); - emit_builtin_call_value(ctx, "isset", vec![read_value.value], PhpType::Int, expr.span) - } - } -} - -/// Returns whether a syntactic array receiver can use a non-materializing native `isset` probe. -fn array_access_expr_supports_native_isset_probe( - ctx: &LoweringContext<'_, '_>, - array: &Expr, -) -> bool { - let ty = match &array.kind { - ExprKind::Variable(name) => ctx - .local_types - .get(name) - .cloned() - .unwrap_or_else(|| infer_expr_type_syntactic(array)), - ExprKind::PropertyAccess { object, property } => { - property_access_expr_type_for_ir(ctx, object, property) - .unwrap_or_else(|| infer_expr_type_syntactic(array)) - } - ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, array), - ExprKind::ArrayLiteralAssoc(pairs) => assoc_array_literal_type_for_ir(ctx, pairs, array), - _ => infer_expr_type_syntactic(array), - } - .codegen_repr(); - matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) +/// Returns true when a local type can reach IEEE float predicates without TypeError. +fn eval_literal_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) } -/// Lowers direct function/static-method first-class callable probes for `is_callable()`. -fn lower_static_is_callable( - ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], - expr: &Expr, -) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "is_callable" || args.len() != 1 { - return None; - } - if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { +/// Returns the literal eval fragment when the call is a simple `eval('...')`. +fn eval_literal_fragment<'a>(name: &str, args: &'a [Expr]) -> Option<&'a str> { + if php_symbol_key(name.trim_start_matches('\\')) != "eval" + || args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { return None; } match &args[0].kind { - ExprKind::FirstClassCallable( - CallableTarget::Function(_) | CallableTarget::StaticMethod { .. }, - ) => Some(emit_bool_literal(ctx, true, Some(expr.span))), - ExprKind::ArrayLiteral(items) => { - let is_callable = static_array_callable_is_callable(ctx, items)?; - Some(emit_bool_literal(ctx, is_callable, Some(expr.span))) - } - ExprKind::Variable(name) => ctx.static_callable_local(name).map(|target| { - emit_bool_literal( - ctx, - static_callable_binding_is_callable(ctx, &target), - Some(expr.span), - ) - }), + ExprKind::StringLiteral(fragment) => Some(fragment.as_str()), _ => None, } } -/// Returns whether straight-line callable-local metadata represents a public callable. -fn static_callable_binding_is_callable( +/// Returns true when a literal-eval static function call can avoid the eval barrier. +fn eval_literal_static_function_supported_by_lowering( ctx: &LoweringContext<'_, '_>, - target: &StaticCallableBinding, + name: &str, + args: &[Expr], ) -> bool { - match target { - StaticCallableBinding::StaticMethod { receiver, method } - | StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { - static_receiver_class_name(ctx, receiver) - .is_some_and(|class_name| static_method_callback_is_callable(ctx, &class_name, method)) - } - StaticCallableBinding::UserFunction(_) - | StaticCallableBinding::ExternFunction(_) - | StaticCallableBinding::Builtin(_) - | StaticCallableBinding::Closure { .. } - | StaticCallableBinding::InstanceMethod { .. } => true, + if args.len() > 6 { + return false; } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(signature) = ctx + .functions + .iter() + .find(|(function_name, _)| php_symbol_key(function_name.trim_start_matches('\\')) == key) + .map(|(_, signature)| signature) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) } -/// Lowers static-string `call_user_func*` forms to direct call opcodes when possible. -fn lower_static_call_user_func( - ctx: &mut LoweringContext<'_, '_>, - name: &str, +/// Returns true when a literal-eval static method call can avoid the eval barrier. +fn eval_literal_static_method_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, + method: &str, args: &[Expr], - expr: &Expr, -) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "call_user_func" => { - let callback_expr = args.first()?; - let callback_args = &args[1..]; - let signature = callable_descriptor_signature_for_expr(ctx, callback_expr); - if call_user_func_should_use_descriptor(ctx, callback_expr, callback_args, signature.as_ref()) { - return lower_call_user_func_descriptor_invoke( - ctx, - callback_expr, - callback_args, - signature.as_ref(), - expr, - ); - } - if let Some(callback) = instance_call_user_func_callback(ctx, callback_expr) { - return lower_instance_callable_call_user_func( - ctx, - callback_expr, - callback, - callback_args, - expr, - ); - } - let callback = static_call_user_func_callback(ctx, callback_expr)?; - lower_static_callable_call(ctx, callback, callback_args, expr) - } - "call_user_func_array" => { - let [callback_arg, arg_array] = args else { - return None; - }; - if matches!(arg_array.kind, ExprKind::ArrayLiteralAssoc(_)) - && static_callable_binding_for_expr(ctx, callback_arg) - .is_some_and(|target| matches!(target, StaticCallableBinding::InstanceMethod { .. })) - { - return None; - } - let callback_args = static_call_user_func_array_args(arg_array)?; - if let Some(callback) = instance_call_user_func_callback(ctx, callback_arg) { - return lower_instance_callable_call_user_func( - ctx, - callback_arg, - callback, - &callback_args, - expr, - ); - } - let callback = static_call_user_func_callback(ctx, callback_arg)?; - lower_static_callable_call(ctx, callback, &callback_args, expr) - } - _ => None, +) -> bool { + if args.len() > 6 || !matches!(receiver, StaticReceiver::Named(_)) { + return false; + } + let Some(class_name) = static_receiver_class_name(ctx, receiver) else { + return false; + }; + let method_key = php_symbol_key(method); + let Some(class_info) = ctx.classes.get(class_name.as_str()) else { + return false; + }; + if class_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; } + let Some(signature) = static_method_implementation_signature(ctx, receiver, method) else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) } -/// Lowers `call_user_func*` for receiver-bound first-class callables through `expr_call`. -fn lower_instance_callable_call_user_func( - ctx: &mut LoweringContext<'_, '_>, - callback_expr: &Expr, - callback: StaticCallableBinding, - callback_args: &[Expr], - expr: &Expr, -) -> Option { - let result_type = static_callable_return_type(ctx, &callback); - let signature = instance_callable_signature(&callback).cloned(); - let mut operands = vec![lower_expr(ctx, callback_expr).value]; - operands.extend(lower_args_with_signature(ctx, signature.as_ref(), callback_args)); - Some(ctx.emit_value( - Op::ExprCall, - operands, - None, - result_type, - Op::ExprCall.default_effects(), - Some(expr.span), - )) +/// Returns true when a dynamic eval fallback can preserve simple positional call semantics. +fn plain_positional_call_args(args: &[Expr]) -> bool { + !crate::types::call_args::has_named_args(args) + && !args.iter().any(is_spread_arg) } -/// Lowers dynamic `call_user_func()` callbacks through descriptor invocation. -fn lower_dynamic_call_user_func( +/// Lowers post-eval function-name probes through the eval context's dynamic table. +fn lower_eval_function_probe( ctx: &mut LoweringContext<'_, '_>, name: &str, args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "call_user_func" || args.is_empty() { + let probe_name = php_symbol_key(name.trim_start_matches('\\')); + if probe_name != "function_exists" && probe_name != "is_callable" { return None; } - if matches!(args[0].kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_)) { + if !ctx.has_eval_barrier() + || args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { return None; } - let signature = callable_descriptor_signature_for_expr(ctx, &args[0]); - let callback = lower_expr(ctx, &args[0]); - if descriptor_callback_php_type_supported(&ctx.builder.value_php_type(callback.value).codegen_repr()) { - return lower_call_user_func_descriptor_invoke_from_value( - ctx, - callback, - &args[1..], - signature.as_ref(), - expr, - ); - } - if crate::types::call_args::has_named_args(&args[1..]) || args[1..].iter().any(is_spread_arg) { + let ExprKind::StringLiteral(function_name) = &args[0].kind else { + return None; + }; + if function_name.contains("::") + || resolve_static_string_callable(ctx, function_name).is_some() + { return None; } - let mut operands = Vec::with_capacity(args.len()); - operands.push(callback.value); - operands.extend(lower_args(ctx, &args[1..])); + let dynamic_name = php_symbol_key(function_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); Some(ctx.emit_value( - Op::ExprCall, - operands, - None, - PhpType::Mixed, - Op::ExprCall.default_effects(), + Op::EvalFunctionExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalFunctionExists.default_effects(), Some(expr.span), )) } -/// Lowers dynamic `call_user_func_array()` through the descriptor-invoker EIR path. -fn lower_dynamic_call_user_func_array( +/// Lowers post-eval class-name probes through the eval context's dynamic class table. +fn lower_eval_class_probe( ctx: &mut LoweringContext<'_, '_>, name: &str, args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "call_user_func_array" { + let probe_name = php_symbol_key(name.trim_start_matches('\\')); + if probe_name != "class_exists" { return None; } - let [callback_expr, arg_array_expr] = args else { + if !ctx.has_eval_barrier() + || args.is_empty() + || args.len() > 2 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + let ExprKind::StringLiteral(class_name) = &args[0].kind else { return None; }; - if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { + if aot_class_exists_for_eval_probe(ctx, class_name) { return None; } - let signature = callable_descriptor_signature_for_expr(ctx, callback_expr); - let callback = lower_expr(ctx, callback_expr); - let arg_array = - lower_descriptor_invoker_arg_array_for_call_user_func_array( - ctx, - arg_array_expr, - signature.as_ref(), - ) - .unwrap_or_else(|| lower_expr(ctx, arg_array_expr)); - Some(emit_callable_descriptor_invoke( - ctx, - callback, - arg_array, - PhpType::Mixed, - expr.span, - )) -} - -/// Returns the callable signature available to descriptor-invoker argument lowering. -fn callable_descriptor_signature_for_expr( - ctx: &LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - match &callback.kind { - ExprKind::Ternary { then_expr, else_expr, .. } => { - let left = callable_descriptor_signature_for_expr(ctx, then_expr)?; - let right = callable_descriptor_signature_for_expr(ctx, else_expr)?; - compatible_descriptor_signature(left, &right) - } - ExprKind::ShortTernary { value, default } => { - let left = callable_descriptor_signature_for_expr(ctx, value)?; - let right = callable_descriptor_signature_for_expr(ctx, default)?; - compatible_descriptor_signature(left, &right) - } - ExprKind::Variable(name) => ctx - .callable_param_signature(name) - .cloned() - .or_else(|| ctx.static_callable_local(name).and_then(|target| { - signature_for_static_callable_binding(ctx, target) - })), - _ => static_callable_binding_for_expr(ctx, callback) - .and_then(|target| signature_for_static_callable_binding(ctx, target)) - .or_else(|| invokable_object_signature_for_expr(ctx, callback)), - } -} - -/// Returns the `__invoke` signature for an invokable object callback expression. -fn invokable_object_signature_for_expr( - ctx: &LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - let class_name = instance_callable_object_class(ctx, callback)?; - class_method_signature(ctx, &class_name, "__invoke").cloned() -} - -/// Keeps a descriptor signature only when two runtime branches have the same callable ABI. -fn compatible_descriptor_signature(left: FunctionSig, right: &FunctionSig) -> Option { - (left == *right).then_some(left) -} - -/// Extracts a callable signature from a statically understood callable binding. -fn signature_for_static_callable_binding( - ctx: &LoweringContext<'_, '_>, - target: StaticCallableBinding, -) -> Option { - match target { - StaticCallableBinding::UserFunction(name) => ctx.functions.get(&name).cloned(), - StaticCallableBinding::ExternFunction(name) => ctx - .extern_functions - .get(&name) - .map(function_sig_from_extern_for_descriptor), - StaticCallableBinding::Builtin(_) => None, - StaticCallableBinding::Closure { signature, .. } => Some(signature), - StaticCallableBinding::StaticMethod { receiver, method } - | StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { - static_method_implementation_signature(ctx, &receiver, &method).cloned() - } - StaticCallableBinding::InstanceMethod { signature, .. } => Some(signature), + if let Some(autoload) = args.get(1) { + lower_expr(ctx, autoload); } + let data = ctx.intern_class_name(class_name); + Some(ctx.emit_value( + Op::EvalClassExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalClassExists.default_effects(), + Some(expr.span), + )) } -/// Converts an extern signature into the PHP-facing descriptor invoker signature. -fn function_sig_from_extern_for_descriptor(sig: &ExternFunctionSig) -> FunctionSig { - FunctionSig { - params: sig.params.clone(), - defaults: vec![None; sig.params.len()], - return_type: sig.return_type.clone(), - declared_return: true, - by_ref_return: false, - ref_params: vec![false; sig.params.len()], - declared_params: vec![true; sig.params.len()], - variadic: None, - deprecation: None, - } +/// Returns true when an AOT class already satisfies a native class_exists probe. +fn aot_class_exists_for_eval_probe(ctx: &LoweringContext<'_, '_>, class_name: &str) -> bool { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.classes + .keys() + .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) } -/// Builds an invoker argument array that preserves by-reference literal variables. -fn lower_descriptor_invoker_arg_array_for_call_user_func_array( +/// Lowers `isset()` as a lazy language construct instead of an eager builtin call. +fn lower_lazy_isset( ctx: &mut LoweringContext<'_, '_>, - arg_array: &Expr, - sig: Option<&FunctionSig>, + name: &str, + args: &[Expr], + expr: &Expr, ) -> Option { - let ExprKind::ArrayLiteral(items) = &arg_array.kind else { + if php_symbol_key(name.trim_start_matches('\\')) != "isset" { return None; - }; - if items.iter().any(is_spread_arg) || !items.iter().enumerate().any(|(index, item)| { - invoker_ref_arg_variable(ctx, sig, index, item).is_some() - }) { + } + if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { return None; } + if args.is_empty() { + return Some(lower_bool_literal(ctx, false, expr)); + } - let elem_ty = PhpType::Mixed; - let array_ty = PhpType::Array(Box::new(elem_ty.clone())); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - array_ty.clone(), - Op::ArrayNew.default_effects(), - Some(arg_array.span), - ); - for (index, item) in items.iter().enumerate() { - let value = if let Some(var_name) = invoker_ref_arg_variable(ctx, sig, index, item) { - lower_invoker_ref_arg_marker(ctx, var_name, item.span) + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let false_block = ctx.builder.create_named_block("isset.lazy_false", Vec::new()); + let merge = ctx.builder.create_named_block("isset.lazy_merge", Vec::new()); + for (idx, arg) in args.iter().enumerate() { + let checked = lower_lazy_isset_operand(ctx, arg).unwrap_or_else(|| { + let value = lower_expr(ctx, arg); + emit_builtin_call_value(ctx, name, vec![value.value], PhpType::Int, arg.span, None) + }); + let then_target = if idx + 1 == args.len() { + ctx.builder.create_named_block("isset.lazy_true", Vec::new()) } else { - let value = lower_expr(ctx, item); - coerce_variadic_tail_value(ctx, value, &array_ty, item.span) + ctx.builder.create_named_block("isset.lazy_next", Vec::new()) }; - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(item.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, item.span); + ctx.builder.terminate(Terminator::CondBr { + cond: checked.value, + then_target, + then_args: Vec::new(), + else_target: false_block, + else_args: Vec::new(), + }); + ctx.builder.position_at_end(then_target); } - Some(array) + + let true_value = lower_bool_literal(ctx, true, expr); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, true_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(false_block); + let false_value = lower_bool_literal(ctx, false, expr); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + Some(take_owned_temp(ctx, &temp_name, expr.span)) } -/// Returns true when `call_user_func()` must keep runtime descriptor semantics. -fn call_user_func_should_use_descriptor( - ctx: &LoweringContext<'_, '_>, - callback: &Expr, - args: &[Expr], - sig: Option<&FunctionSig>, -) -> bool { - let has_named_or_spread = - crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg); - if has_named_or_spread { - return true; - } - if call_user_func_has_incompatible_ref_marker_arg(ctx, args, sig) { - return false; - } - if sig.is_some_and(|sig| sig.ref_params.iter().any(|is_ref| *is_ref)) { - return true; - } - match &callback.kind { - ExprKind::ArrayLiteral(_) - | ExprKind::ArrayLiteralAssoc(_) - | ExprKind::Closure { .. } - | ExprKind::NewObject { .. } - | ExprKind::NewDynamicObject { .. } - | ExprKind::Ternary { .. } - | ExprKind::ShortTernary { .. } - | ExprKind::FirstClassCallable(CallableTarget::Method { .. }) => true, - ExprKind::Variable(name) => { - if let Some(target) = ctx.static_callable_local(name) { - return matches!( - target, - StaticCallableBinding::Closure { .. } - | StaticCallableBinding::StaticMethodDescriptor { .. } - | StaticCallableBinding::InstanceMethod { .. } +/// Lowers a single `isset()` operand that has special lazy PHP semantics. +fn lower_lazy_isset_operand( + ctx: &mut LoweringContext<'_, '_>, + arg: &Expr, +) -> Option { + match &arg.kind { + ExprKind::ArrayAccess { array, index } => { + if array_access_expr_satisfies_array_access(ctx, array) { + let synthetic = Expr::new( + ExprKind::MethodCall { + object: array.clone(), + method: "offsetExists".to_string(), + args: vec![(**index).clone()], + }, + arg.span, ); + return Some(lower_expr(ctx, &synthetic)); } - matches!( - ctx.local_type(name).codegen_repr(), - PhpType::Callable | PhpType::Array(_) | PhpType::Object(_) - ) + if !array_access_expr_supports_native_isset_probe(ctx, array) { + return None; + } + Some(lower_native_isset_offset_probe(ctx, array, index, arg)) } - _ => false, - } -} - -/// Returns true when direct descriptor ref markers cannot represent an argument. -fn call_user_func_has_incompatible_ref_marker_arg( - ctx: &LoweringContext<'_, '_>, - args: &[Expr], - sig: Option<&FunctionSig>, -) -> bool { - let Some(sig) = sig else { - return false; - }; - args.iter().enumerate().any(|(index, arg)| { - if !sig.ref_params.get(index).copied().unwrap_or(false) { - return false; + ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } => { + lower_lazy_property_isset_operand(ctx, object, property, arg) } - let ExprKind::Variable(name) = &arg.kind else { - return false; - }; - !invoker_ref_arg_storage_compatible(ctx, sig, index, name) - }) + // `isset($this)` inside a static closure always evaluates to `false` + // because static closures have no `$this` binding. PHP allows this + // probe and returns false; elephc must not try to load a missing slot. + ExprKind::This if !ctx.local_slots.contains_key("this") => { + Some(lower_bool_literal(ctx, false, arg)) + } + _ => None, + } } -/// Lowers `call_user_func()` into a descriptor invoke when the callback value is supported. -fn lower_call_user_func_descriptor_invoke( +/// Lowers `empty($obj->magicProp)` with PHP's overloaded-property semantics: +/// `empty` consults `__isset` first and only evaluates `__get` when `__isset` +/// is truthy, so an unset virtual property is empty without ever reading it. +/// Returns `None` for operands the eager `empty` builtin already handles (plain +/// variables, declared properties, array elements), letting that path run. +fn lower_lazy_empty( ctx: &mut LoweringContext<'_, '_>, - callback_expr: &Expr, + name: &str, args: &[Expr], - sig: Option<&FunctionSig>, expr: &Expr, ) -> Option { - let callback = lower_expr(ctx, callback_expr); - if !descriptor_callback_php_type_supported(&ctx.builder.value_php_type(callback.value).codegen_repr()) { + if php_symbol_key(name.trim_start_matches('\\')) != "empty" { return None; } - lower_call_user_func_descriptor_invoke_from_value(ctx, callback, args, sig, expr) -} + if args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + let (exists_call, get_call) = lazy_empty_magic_property_calls(ctx, &args[0])?; -/// Emits `CallableDescriptorInvoke` for an already evaluated `call_user_func()` callback. -fn lower_call_user_func_descriptor_invoke_from_value( - ctx: &mut LoweringContext<'_, '_>, - callback: LoweredValue, - args: &[Expr], - sig: Option<&FunctionSig>, - expr: &Expr, -) -> Option { - let arg_container = lower_descriptor_invoker_arg_container_for_call_user_func(ctx, args, sig, expr.span)?; - let result_type = sig - .map(|sig| normalize_value_php_type(sig.return_type.codegen_repr())) - .unwrap_or(PhpType::Mixed); - Some(emit_callable_descriptor_invoke( - ctx, - callback, - arg_container, - result_type, - expr.span, - )) -} + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let present_block = ctx.builder.create_named_block("empty.present", Vec::new()); + let absent_block = ctx.builder.create_named_block("empty.absent", Vec::new()); + let merge = ctx.builder.create_named_block("empty.merge", Vec::new()); -/// Emits a descriptor invoke and releases an owned argument container after the call. -fn emit_callable_descriptor_invoke( - ctx: &mut LoweringContext<'_, '_>, - callback: LoweredValue, - arg_container: LoweredValue, - result_type: PhpType, - span: Span, -) -> LoweredValue { - let result = ctx.emit_value( - Op::CallableDescriptorInvoke, - vec![callback.value, arg_container.value], - None, - result_type, - Op::CallableDescriptorInvoke.default_effects(), - Some(span), + // `__isset(prop)` decides whether the property is considered set at all. + let exists = lower_expr(ctx, &exists_call); + ctx.builder.terminate(Terminator::CondBr { + cond: exists.value, + then_target: present_block, + then_args: Vec::new(), + else_target: absent_block, + else_args: Vec::new(), + }); + + // Set: empty is the emptiness of the `__get` value (reuses the eager builtin). + ctx.builder.position_at_end(present_block); + let get_value = lower_expr(ctx, &get_call); + let empty_name = ctx.intern_function_name(name); + let empty_value = ctx.emit_value( + Op::BuiltinCall, + vec![get_value.value], + Some(Immediate::Data(empty_name)), + PhpType::Bool, + effects_lookup::builtin_effects(name), + Some(expr.span), ); - if ctx.value_is_owning_temporary(arg_container) { - crate::ir_lower::ownership::release_if_owned(ctx, arg_container, Some(span)); - } - result -} + store_value_into_temp(ctx, &temp_name, PhpType::Bool, empty_value, expr.span); + branch_to(ctx, merge); -/// Returns true when the EIR backend has descriptor dispatch for this callback type. -fn descriptor_callback_php_type_supported(php_type: &PhpType) -> bool { - matches!( - php_type, - PhpType::Str | PhpType::Callable | PhpType::Array(_) | PhpType::Object(_) - ) + // Not set: empty is true and `__get` is never called. + ctx.builder.position_at_end(absent_block); + let true_value = lower_bool_literal(ctx, true, expr); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, true_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + Some(ctx.load_local(&temp_name, Some(expr.span))) } -/// Builds the descriptor-invoker argument container for `call_user_func()`. -fn lower_descriptor_invoker_arg_container_for_call_user_func( - ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - sig: Option<&FunctionSig>, - span: Span, -) -> Option { - if crate::types::call_args::has_named_args(args) { - if args.iter().any(is_spread_arg) { - return None; +/// For an `empty()` operand that is an overloaded (magic) property access, +/// returns the `(__isset, __get)` synthetic call expressions PHP would evaluate. +/// The property name is a string literal, so reusing it for both calls is +/// side-effect free. Returns `None` for any other operand shape. +fn lazy_empty_magic_property_calls( + ctx: &LoweringContext<'_, '_>, + arg: &Expr, +) -> Option<(Expr, Expr)> { + match &arg.kind { + ExprKind::PropertyAccess { object, property } => { + property_existence_magic_class(ctx, object, property, "__isset")?; + let key = Expr::new(ExprKind::StringLiteral(property.clone()), arg.span); + let exists = Expr::new( + ExprKind::MethodCall { + object: object.clone(), + method: "__isset".to_string(), + args: vec![key.clone()], + }, + arg.span, + ); + let get = Expr::new( + ExprKind::MethodCall { + object: object.clone(), + method: "__get".to_string(), + args: vec![key], + }, + arg.span, + ); + Some((exists, get)) } - return Some(lower_named_descriptor_invoker_arg_container(ctx, args, sig, span)); + ExprKind::NullsafePropertyAccess { object, property } => { + property_existence_magic_class(ctx, object, property, "__isset")?; + let key = Expr::new(ExprKind::StringLiteral(property.clone()), arg.span); + let exists = Expr::new( + ExprKind::NullsafeMethodCall { + object: object.clone(), + method: "__isset".to_string(), + args: vec![key.clone()], + }, + arg.span, + ); + let get = Expr::new( + ExprKind::NullsafeMethodCall { + object: object.clone(), + method: "__get".to_string(), + args: vec![key], + }, + arg.span, + ); + Some((exists, get)) + } + _ => None, } - Some(lower_indexed_descriptor_invoker_arg_array(ctx, args, sig, span)) } -/// Builds an indexed `array` argument container, expanding positional spreads. -fn lower_indexed_descriptor_invoker_arg_array( +/// Returns the class whose `magic` method (`__isset`/`__unset`) should handle +/// property existence/removal: a property that cannot be accessed normally on an +/// object whose class declares the magic method. +fn property_existence_magic_class( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, + magic: &str, +) -> Option { + let class_name = instance_callable_object_class(ctx, object)?; + let class_info = ctx.classes.get(&class_name)?; + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + return None; + } + class_method_signature(ctx, &class_name, &php_symbol_key(magic)).map(|_| class_name) +} + +/// Lowers native array/hash `isset($array[$key])` without reading the element value. +fn lower_native_isset_offset_probe( ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - sig: Option<&FunctionSig>, - span: Span, + array: &Expr, + index: &Expr, + expr: &Expr, ) -> LoweredValue { - let elem_ty = PhpType::Mixed; - let array_ty = PhpType::Array(Box::new(elem_ty.clone())); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(args.len() as u32)), - array_ty.clone(), - Op::ArrayNew.default_effects(), - Some(span), - ); - let mut positional_index = 0usize; - for arg in args { - if let ExprKind::Spread(inner) = &arg.kind { - let source = lower_expr(ctx, inner); - lower_indexed_array_spread_into_array(ctx, array, source, Some(&elem_ty), arg.span); - continue; - } - let value = if let Some(var_name) = invoker_ref_arg_variable(ctx, sig, positional_index, arg) { - lower_invoker_ref_arg_marker(ctx, var_name, arg.span) - } else { - let value = lower_expr(ctx, arg); - coerce_variadic_tail_value(ctx, value, &array_ty, arg.span) - }; - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(arg.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, arg.span); - positional_index += 1; + let array_value = lower_expr(ctx, array); + if value_is_nullable(ctx, array_value.value) { + return lower_nullable_native_isset_offset_probe(ctx, array_value, index, expr); } - array + lower_native_isset_offset_probe_from_value(ctx, array_value, index, expr) } -/// Builds a boxed hash argument container for named `call_user_func()` args. -fn lower_named_descriptor_invoker_arg_container( +/// Lowers nullable native array/hash `isset` without evaluating the offset on null receivers. +fn lower_nullable_native_isset_offset_probe( ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - sig: Option<&FunctionSig>, - span: Span, + array_value: LoweredValue, + index: &Expr, + expr: &Expr, ) -> LoweredValue { - let hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity(args.len() as u32)), - hash_ty, - Op::HashNew.default_effects(), - Some(span), + let is_null = ctx.emit_value( + Op::IsNull, + vec![array_value.value], + None, + PhpType::Bool, + Op::IsNull.default_effects(), + Some(expr.span), ); - let mut next_positional_key = 0i64; - for arg in args { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - let key = lower_string_literal(ctx, name, arg); - let param_index = sig.and_then(|sig| { - let regular_param_count = crate::types::call_args::regular_param_count(sig); - crate::types::call_args::named_param_index(sig, regular_param_count, name) - }); - let value = if let Some(index) = param_index { - invoker_ref_arg_variable(ctx, sig, index, value) - .map(|var_name| lower_invoker_ref_arg_marker(ctx, var_name, value.span)) - } else { - None - } - .unwrap_or_else(|| lower_expr(ctx, value)); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let null_block = ctx + .builder + .create_named_block("isset.native.null", Vec::new()); + let probe_block = ctx + .builder + .create_named_block("isset.native.probe", Vec::new()); + let merge = ctx + .builder + .create_named_block("isset.native.merge", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: is_null.value, + then_target: null_block, + then_args: Vec::new(), + else_target: probe_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(null_block); + let false_value = emit_bool_literal(ctx, false, Some(expr.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(probe_block); + let checked = lower_native_isset_offset_probe_from_value(ctx, array_value, index, expr); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, checked, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + take_owned_temp(ctx, &temp_name, expr.span) +} + +/// Lowers native array/hash `isset` once the receiver has already been evaluated. +fn lower_native_isset_offset_probe_from_value( + ctx: &mut LoweringContext<'_, '_>, + array_value: LoweredValue, + index: &Expr, + expr: &Expr, +) -> LoweredValue { + match array_value.ir_type { + IrType::Heap(IrHeapKind::Array) => { + let mut index_value = lower_expr(ctx, index); + let index_ty = index_expr_key_type(ctx, index); + if index_ty == PhpType::Int { + index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); + ctx.emit_value( + Op::ArrayIsset, + vec![array_value.value, index_value.value], None, - Op::HashSet.default_effects(), - Some(arg.span), + PhpType::Bool, + Op::ArrayIsset.default_effects(), + Some(expr.span), + ) + } else { + // String or mixed key on indexed storage: read through the + // mixed-key runtime path and check if the result is null. + let read_value = ctx.emit_value( + Op::ArrayGetMixedKey, + vec![array_value.value, index_value.value], + None, + PhpType::Mixed, + Op::ArrayGetMixedKey.default_effects(), + Some(expr.span), ); - } - _ => { - let key = emit_i64_at_span(ctx, next_positional_key, arg.span); - let value = if let Some(var_name) = - invoker_ref_arg_variable(ctx, sig, next_positional_key as usize, arg) - { - lower_invoker_ref_arg_marker(ctx, var_name, arg.span) - } else { - lower_expr(ctx, arg) - }; - next_positional_key += 1; - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], + let is_null = ctx.emit_value( + Op::IsNull, + vec![read_value.value], None, - Op::HashSet.default_effects(), - Some(arg.span), + PhpType::Bool, + Op::IsNull.default_effects(), + Some(expr.span), + ); + let zero = ctx.emit_value( + Op::ConstI64, + Vec::new(), + Some(Immediate::I64(0)), + PhpType::Int, + Op::ConstI64.default_effects(), + Some(expr.span), ); + ctx.emit_value( + Op::ICmp, + vec![is_null.value, zero.value], + Some(Immediate::CmpPredicate(crate::ir::CmpPredicate::Eq)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(expr.span), + ) } } - } - ctx.emit_value( - Op::MixedBox, - vec![hash.value], - None, - PhpType::Mixed, - Op::MixedBox.default_effects(), - Some(span), - ) -} - -/// Returns the variable name when this literal argument should be passed by reference. -fn invoker_ref_arg_variable<'a>( - _ctx: &LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - index: usize, - item: &'a Expr, -) -> Option<&'a str> { - let ExprKind::Variable(name) = &item.kind else { - return None; - }; - if let Some(sig) = sig { - if !sig.ref_params.get(index).copied().unwrap_or(false) { - return None; + IrType::Heap(IrHeapKind::Hash) => { + let index_value = lower_expr(ctx, index); + ctx.emit_value( + Op::HashIsset, + vec![array_value.value, index_value.value], + None, + PhpType::Bool, + Op::HashIsset.default_effects(), + Some(expr.span), + ) + } + _ => { + let read_value = lower_array_access_from_value(ctx, array_value, index, expr, false); + emit_builtin_call_value( + ctx, + "isset", + vec![read_value.value], + PhpType::Int, + expr.span, + None, + ) } } - Some(name.as_str()) } -/// Returns true when a local slot can be passed directly to a descriptor ref param. -fn invoker_ref_arg_storage_compatible( +/// Returns whether a syntactic array receiver can use a non-materializing native `isset` probe. +fn array_access_expr_supports_native_isset_probe( ctx: &LoweringContext<'_, '_>, - sig: &FunctionSig, - index: usize, - var_name: &str, + array: &Expr, ) -> bool { - let Some((_, param_ty)) = sig.params.get(index) else { - return true; - }; - value_ir_type(¶m_ty.codegen_repr()) == value_ir_type(&ctx.local_type(var_name).codegen_repr()) + let ty = match &array.kind { + ExprKind::Variable(name) => ctx + .local_types + .get(name) + .cloned() + .unwrap_or_else(|| infer_expr_type_syntactic(array)), + ExprKind::PropertyAccess { object, property } => { + property_access_expr_type_for_ir(ctx, object, property) + .unwrap_or_else(|| infer_expr_type_syntactic(array)) + } + ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, array), + ExprKind::ArrayLiteralAssoc(pairs) => assoc_array_literal_type_for_ir(ctx, pairs, array), + _ => infer_expr_type_syntactic(array), + } + .codegen_repr(); + matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) } -/// Emits an invoker reference-cell marker for a local variable argument. -fn lower_invoker_ref_arg_marker( +/// Lowers `isset($object->property)` without performing a normal property read first. +fn lower_lazy_property_isset_operand( ctx: &mut LoweringContext<'_, '_>, - var_name: &str, - span: Span, -) -> LoweredValue { - let php_type = ctx.local_type(var_name); - let slot = ctx.declare_local(var_name, php_type); - ctx.emit_value( - Op::InvokerRefArg, - Vec::new(), - Some(Immediate::LocalSlot(slot)), - PhpType::Mixed, - Op::InvokerRefArg.default_effects(), - Some(span), - ) + object: &Expr, + property: &str, + arg: &Expr, +) -> Option { + match property_isset_action(ctx, object, property)? { + IssetPropertyAction::Fallback => None, + IssetPropertyAction::Magic => { + let object = lower_expr(ctx, object); + Some(lower_magic_property_isset(ctx, object, property, arg)) + } + IssetPropertyAction::AlwaysFalse => { + lower_expr(ctx, object); + Some(emit_bool_literal(ctx, false, Some(arg.span))) + } + } } -/// Lowers `array_map()` for a static callback and indexed array literal source. -fn lower_static_array_map( - ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], - expr: &Expr, -) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "array_map" || args.len() != 2 { - return None; +/// Describes how `isset($object->property)` should be lowered for a known receiver class. +enum IssetPropertyAction { + Fallback, + Magic, + AlwaysFalse, +} + +/// Selects the PHP-visible `isset()` behavior for a statically known object property operand. +fn property_isset_action( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> Option { + let (class_name, _) = isset_object_expr_class(ctx, object)?; + if is_builtin_stdclass_name(&class_name) { + return Some(IssetPropertyAction::Fallback); } - if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { - return None; + let class_info = ctx.classes.get(class_name.as_str())?; + if class_info.allow_dynamic_properties { + return Some(IssetPropertyAction::Fallback); } - if matches!(args[0].kind, ExprKind::Variable(_)) { - return None; + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + return Some(IssetPropertyAction::Fallback); } - let callback = static_call_user_func_callback(ctx, &args[0])?; - let ExprKind::ArrayLiteral(items) = &args[1].kind else { - return None; - }; - let elem_type = static_callable_return_type(ctx, &callback); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - PhpType::Array(Box::new(elem_type.clone())), - Op::ArrayNew.default_effects(), - Some(expr.span), - ); - for item in items { - let value = lower_static_callable_call(ctx, callback.clone(), std::slice::from_ref(item), expr)?; - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(item.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_type), value, item.span); + if class_method_signature(ctx, &class_name, &php_symbol_key("__isset")).is_some() { + Some(IssetPropertyAction::Magic) + } else { + Some(IssetPropertyAction::AlwaysFalse) } - Some(array) } -/// Lowers `array_reduce()` for a static callback and immediate indexed-array literal. -fn lower_static_array_reduce( - ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], - expr: &Expr, -) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "array_reduce" || args.len() != 3 { - return None; - } - if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { - return None; - } - if matches!(args[1].kind, ExprKind::Variable(_)) { - return None; - } - let ExprKind::ArrayLiteral(items) = &args[0].kind else { - return None; +/// Returns the single receiver class and whether that receiver may be null. +fn isset_object_expr_class(ctx: &LoweringContext<'_, '_>, object: &Expr) -> Option<(String, bool)> { + let ty = match &object.kind { + ExprKind::Variable(name) => ctx.local_type(name), + ExprKind::This => PhpType::Object(ctx.current_class.clone()?), + ExprKind::NewObject { class_name, .. } => PhpType::Object(class_name.to_string()), + ExprKind::NewDynamicObject { fallback_class, .. } => { + PhpType::Object(fallback_class.to_string()) + } + ExprKind::FunctionCall { name, .. } => ctx + .functions + .get(name.as_str()) + .map(|sig| sig.return_type.clone()) + .unwrap_or_else(|| infer_expr_type_syntactic(object)), + _ => infer_expr_type_syntactic(object), }; - if !items.iter().all(static_callback_array_item_can_inline) { - return None; + let (class_name, nullable) = singular_object_class(&ty)?; + normalized_class_name(class_name).map(|name| (name, nullable)) +} + +/// Returns whether a named property can use normal `isset()` value probing. +fn property_is_accessible_for_ir( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + class_info: &crate::types::ClassInfo, + property: &str, +) -> bool { + if class_info.visible_property(property).is_none() { + return false; } - let callback = static_call_user_func_callback(ctx, &args[1])?; - let result_type = fallback_expr_type(expr); - let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let initial = lower_expr(ctx, &args[2]); - store_value_into_temp(ctx, &temp_name, result_type.clone(), initial, expr.span); - for item in items { - let carry = ctx.load_local(&temp_name, Some(expr.span)); - let item_value = lower_expr(ctx, item); - let reduced = lower_static_callable_value_call( - ctx, - callback.clone(), - vec![carry.value, item_value.value], - expr, - )?; - store_value_into_temp(ctx, &temp_name, result_type.clone(), reduced, expr.span); + class_info + .property_visibilities + .get(property) + .is_none_or(|visibility| { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + ir_can_access_member(ctx, declaring_class, visibility) + }) +} + +/// Checks PHP member visibility from the current lowering class scope. +fn ir_can_access_member( + ctx: &LoweringContext<'_, '_>, + declaring_class: &str, + visibility: &Visibility, +) -> bool { + match visibility { + Visibility::Public => true, + Visibility::Private => ctx + .current_class + .as_deref() + .is_some_and(|current| same_php_class_name(current, declaring_class)), + Visibility::Protected => ctx.current_class.as_deref().is_some_and(|current| { + same_php_class_name(current, declaring_class) + || class_extends_class(ctx, current, declaring_class) + }), } - Some(take_owned_temp(ctx, &temp_name, expr.span)) } -/// Lowers `array_walk()` for a static callback and immediate indexed-array literal. -fn lower_static_array_walk( +/// Returns true when two class metadata names match PHP's case-insensitive class lookup. +fn same_php_class_name(left: &str, right: &str) -> bool { + php_symbol_key(left.trim_start_matches('\\')) == php_symbol_key(right.trim_start_matches('\\')) +} + +/// Lowers a magic `__isset($name)` call and coerces the result to PHP boolean semantics. +fn lower_magic_property_isset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + arg: &Expr, +) -> LoweredValue { + if value_is_nullable(ctx, object.value) { + return lower_nullable_magic_property_isset(ctx, object, property, arg); + } + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + arg.span, + )]; + let result = + lower_method_call_with_receiver(ctx, object, "__isset", &args, Op::MethodCall, arg); + ctx.truthy(result, Some(arg.span)) +} + +/// Lowers `__isset` for nullable receivers, returning false instead of calling on null. +fn lower_nullable_magic_property_isset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + arg: &Expr, +) -> LoweredValue { + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let null_block = ctx + .builder + .create_named_block("isset.property.null", Vec::new()); + let call_block = ctx + .builder + .create_named_block("isset.property.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("isset.property.merge", Vec::new()); + let is_null = ctx.emit_value( + Op::IsNull, + vec![object.value], + None, + PhpType::Bool, + Op::IsNull.default_effects(), + Some(arg.span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: is_null.value, + then_target: null_block, + then_args: Vec::new(), + else_target: call_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(null_block); + let false_value = emit_bool_literal(ctx, false, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(call_block); + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + arg.span, + )]; + let result = + lower_method_call_with_receiver(ctx, object, "__isset", &args, Op::MethodCall, arg); + let result = ctx.truthy(result, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, result, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + ctx.load_local(&temp_name, Some(arg.span)) +} + +/// Lowers direct function/static-method first-class callable probes for `is_callable()`. +fn lower_static_is_callable( ctx: &mut LoweringContext<'_, '_>, name: &str, args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "array_walk" || args.len() != 2 { + if php_symbol_key(name.trim_start_matches('\\')) != "is_callable" || args.len() != 1 { return None; } if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { return None; } - if matches!(args[1].kind, ExprKind::Variable(_)) { - return None; - } - let ExprKind::ArrayLiteral(items) = &args[0].kind else { - return None; - }; - if !items.iter().all(static_callback_array_item_can_inline) { + // Eval can declare callable targets after static metadata has been built. + if ctx.has_eval_barrier() { return None; } - let callback = static_call_user_func_callback(ctx, &args[1])?; - for item in items { - let item_value = lower_expr(ctx, item); - lower_static_callable_value_call(ctx, callback.clone(), vec![item_value.value], expr)?; + match &args[0].kind { + ExprKind::FirstClassCallable( + CallableTarget::Function(_) | CallableTarget::StaticMethod { .. }, + ) => Some(emit_bool_literal(ctx, true, Some(expr.span))), + ExprKind::ArrayLiteral(items) => { + let is_callable = static_array_callable_is_callable(ctx, items)?; + Some(emit_bool_literal(ctx, is_callable, Some(expr.span))) + } + ExprKind::Variable(name) => ctx.static_callable_local(name).map(|target| { + emit_bool_literal( + ctx, + static_callable_binding_is_callable(ctx, &target), + Some(expr.span), + ) + }), + _ => None, } - Some(lower_null(ctx, expr)) -} - -/// Returns whether a literal array element can be reordered around callback invocation safely. -fn static_callback_array_item_can_inline(item: &Expr) -> bool { - matches!( - item.kind, - ExprKind::IntLiteral(_) - | ExprKind::FloatLiteral(_) - | ExprKind::BoolLiteral(_) - | ExprKind::StringLiteral(_) - | ExprKind::Null - ) } -/// Returns the best known element type for a static callback used by `array_map()`. -fn static_callable_return_type( +/// Returns whether straight-line callable-local metadata represents a public callable. +fn static_callable_binding_is_callable( ctx: &LoweringContext<'_, '_>, target: &StaticCallableBinding, -) -> PhpType { +) -> bool { match target { - StaticCallableBinding::UserFunction(name) - | StaticCallableBinding::ExternFunction(name) - | StaticCallableBinding::Builtin(name) => call_return_type(ctx, name, &[]), - StaticCallableBinding::Closure { signature, .. } => { - normalize_value_php_type(signature.return_type.codegen_repr()) - } StaticCallableBinding::StaticMethod { receiver, method } | StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { - static_method_implementation_signature(ctx, receiver, method) - .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or(PhpType::Mixed) - } - StaticCallableBinding::InstanceMethod { signature, .. } => { - normalize_value_php_type(signature.return_type.codegen_repr()) + static_receiver_class_name(ctx, receiver) + .is_some_and(|class_name| static_method_callback_is_callable(ctx, &class_name, method)) } + StaticCallableBinding::UserFunction(_) + | StaticCallableBinding::ExternFunction(_) + | StaticCallableBinding::Builtin(_) + | StaticCallableBinding::Closure { .. } + | StaticCallableBinding::InstanceMethod { .. } => true, } } -/// Lowers one resolved static callable target using already-evaluated positional operands. -fn lower_static_callable_value_call( +/// Lowers static-string `call_user_func*` forms to direct call opcodes when possible. +fn lower_static_call_user_func( ctx: &mut LoweringContext<'_, '_>, - target: StaticCallableBinding, - operands: Vec, + name: &str, + args: &[Expr], expr: &Expr, ) -> Option { - match target { - StaticCallableBinding::UserFunction(function_name) => { - let php_type = call_return_type(ctx, &function_name, &operands); - let data = ctx.intern_function_name(&function_name); - Some(ctx.emit_value( - Op::Call, - operands, - Some(Immediate::Data(data)), - php_type, - effects_lookup::user_call_effects(&function_name), - Some(expr.span), - )) - } - StaticCallableBinding::ExternFunction(function_name) => { - let php_type = call_return_type(ctx, &function_name, &operands); - let data = ctx.intern_function_name(&function_name); - Some(ctx.emit_value( - Op::ExternCall, - operands, - Some(Immediate::Data(data)), - php_type, - Op::ExternCall.default_effects(), - Some(expr.span), - )) - } - StaticCallableBinding::Builtin(function_name) => { - let php_type = call_return_type(ctx, &function_name, &operands); - Some(emit_builtin_call_value(ctx, &function_name, operands, php_type, expr.span)) + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "call_user_func" => { + let callback_expr = args.first()?; + let callback_args = &args[1..]; + let signature = callable_descriptor_signature_for_expr(ctx, callback_expr); + if call_user_func_should_use_descriptor(ctx, callback_expr, callback_args, signature.as_ref()) { + return lower_call_user_func_descriptor_invoke( + ctx, + callback_expr, + callback_args, + signature.as_ref(), + expr, + ); + } + if let Some(callback) = instance_call_user_func_callback(ctx, callback_expr) { + return lower_instance_callable_call_user_func( + ctx, + callback_expr, + callback, + callback_args, + expr, + ); + } + if let Some(callback) = static_call_user_func_callback(ctx, callback_expr) { + return lower_static_callable_call(ctx, callback, callback_args, expr); + } + lower_eval_call_user_func_fallback(ctx, callback_expr, callback_args, expr) } - StaticCallableBinding::Closure { - name, - signature, - captures, - } => { - let mut operands = operands; - append_closure_capture_operands(&mut operands, &captures); - let php_type = normalize_value_php_type(signature.return_type.codegen_repr()); - let data = ctx.intern_function_name(&name); - Some(ctx.emit_value( - Op::Call, - operands, - Some(Immediate::Data(data)), - php_type, - effects_lookup::user_call_effects(&name), - Some(expr.span), - )) + "call_user_func_array" => { + let [callback_arg, arg_array] = args else { + return None; + }; + if matches!(arg_array.kind, ExprKind::ArrayLiteralAssoc(_)) + && static_callable_binding_for_expr(ctx, callback_arg) + .is_some_and(|target| matches!(target, StaticCallableBinding::InstanceMethod { .. })) + { + return None; + } + if let Some(callback_args) = static_call_user_func_array_args(arg_array) { + if let Some(callback) = instance_call_user_func_callback(ctx, callback_arg) { + return lower_instance_callable_call_user_func( + ctx, + callback_arg, + callback, + &callback_args, + expr, + ); + } + if let Some(callback) = static_call_user_func_callback(ctx, callback_arg) { + return lower_static_callable_call(ctx, callback, &callback_args, expr); + } + } + lower_eval_call_user_func_array_fallback(ctx, callback_arg, arg_array, expr) } - StaticCallableBinding::StaticMethod { receiver, method } => { - let sig = static_method_implementation_signature(ctx, &receiver, &method); - let result_type = sig - .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| fallback_expr_type(expr)); - let name = format!("{}::{}", receiver_name(&receiver), method); - let data = ctx.intern_string(&name); - Some(ctx.emit_value( - Op::StaticMethodCall, - operands, - Some(Immediate::Data(data)), - result_type, - Op::StaticMethodCall.default_effects(), - Some(expr.span), - )) - } - StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { - lower_static_method_descriptor_value_call(ctx, &receiver, &method, operands, expr) - } - StaticCallableBinding::InstanceMethod { .. } => None, - } -} - -/// Resolves a compile-time `call_user_func*` callback expression. -fn static_call_user_func_callback( - ctx: &LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - match &callback.kind { - ExprKind::StringLiteral(name) => resolve_static_string_callable(ctx, name), - ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { - resolve_static_string_callable(ctx, name.as_str()) - } - ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { - resolve_static_method_callable(ctx, receiver.clone(), method.clone()) - } - ExprKind::Variable(name) => ctx - .static_callable_local(name) - .and_then(direct_static_callable_binding), - ExprKind::ArrayLiteral(items) => static_array_callable_descriptor_target(ctx, items) - .or_else(|| instance_array_callable_target(ctx, items)), _ => None, } } -/// Resolves `call_user_func*` callbacks that must keep descriptor receiver state. -fn instance_call_user_func_callback( - ctx: &LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - let target = match &callback.kind { - ExprKind::FirstClassCallable(CallableTarget::Method { .. }) => { - static_callable_binding_for_expr(ctx, callback) - } - ExprKind::Variable(name) => ctx.static_callable_local(name), - _ => None, - }?; - if matches!(target, StaticCallableBinding::InstanceMethod { .. }) { - Some(target) - } else { - None +/// Lowers unresolved string callbacks after an eval barrier through the eval function table. +fn lower_eval_call_user_func_fallback( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + callback_args: &[Expr], + expr: &Expr, +) -> Option { + if !ctx.has_eval_barrier() || !plain_positional_call_args(callback_args) { + return None; } -} - -/// Returns signature metadata for receiver-bound callables that still need descriptor state. -fn instance_callable_signature(target: &StaticCallableBinding) -> Option<&FunctionSig> { - match target { - StaticCallableBinding::InstanceMethod { signature, .. } => Some(signature), - _ => None, + let ExprKind::StringLiteral(callback_name) = &callback_expr.kind else { + return None; + }; + if callback_name.contains("::") + || resolve_static_string_callable(ctx, callback_name).is_some() + { + return None; } + let dynamic_name = php_symbol_key(callback_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + let operands = lower_args(ctx, callback_args); + Some(ctx.emit_value( + Op::EvalFunctionCall, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCall.default_effects(), + Some(expr.span), + )) } -/// Resolves a literal first-class callable expression to a static local binding. -pub(crate) fn static_callable_binding_for_expr( - ctx: &LoweringContext<'_, '_>, +/// Lowers unresolved `call_user_func_array()` string callbacks through the eval table. +fn lower_eval_call_user_func_array_fallback( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + arg_array: &Expr, expr: &Expr, -) -> Option { - match &expr.kind { - ExprKind::StringLiteral(name) => resolve_static_string_callable(ctx, name), - ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { - resolve_static_string_callable(ctx, name.as_str()) - } - ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { - resolve_static_method_callable(ctx, receiver.clone(), method.clone()) - } - ExprKind::ArrayLiteral(items) => static_array_callable_descriptor_target(ctx, items) - .or_else(|| instance_array_callable_target(ctx, items)), - ExprKind::FirstClassCallable(CallableTarget::Method { object, method }) => { - resolve_instance_method_callable(ctx, object, method.clone(), false) - } - ExprKind::Variable(name) => ctx.static_callable_local(name), - _ => None, +) -> Option { + if !ctx.has_eval_barrier() { + return None; } -} - -/// EIR value and callable binding produced by a callable-array assignment. -pub(crate) struct LoweredCallableArrayAssignment { - pub(crate) value: LoweredValue, - pub(crate) target: StaticCallableBinding, -} - -/// Lowers a callable-array assignment while preserving its PHP array value. -pub(crate) fn lower_callable_array_for_assignment( - ctx: &mut LoweringContext<'_, '_>, - value: &Expr, - target: Option<&StaticCallableBinding>, -) -> Option { - let ExprKind::ArrayLiteral(items) = &value.kind else { + let ExprKind::StringLiteral(callback_name) = &callback_expr.kind else { return None; }; - let StaticCallableBinding::InstanceMethod { - object, - method, - signature, - .. - } = target? else { + if callback_name.contains("::") + || resolve_static_string_callable(ctx, callback_name).is_some() + { return None; - }; - let receiver = lower_expr(ctx, object); - let receiver_ty = ctx.builder.value_php_type(receiver.value); - let hidden_name = ctx.declare_hidden_temp(receiver_ty.clone()); - let receiver = ctx.store_local(&hidden_name, receiver, receiver_ty, Some(object.span)); - let array = lower_callable_array_literal_with_receiver(ctx, items, value, receiver); - let hidden_object = Expr::new(ExprKind::Variable(hidden_name), object.span); - let target = StaticCallableBinding::InstanceMethod { - object: Box::new(hidden_object), - method: method.clone(), - signature: signature.clone(), - direct_call: true, - }; - Some(LoweredCallableArrayAssignment { value: array, target }) + } + let dynamic_name = php_symbol_key(callback_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + let arg_array = lower_expr(ctx, arg_array); + let arg_array = coerce_eval_function_arg_array(ctx, arg_array, expr.span); + Some(ctx.emit_value( + Op::EvalFunctionCallArray, + vec![arg_array.value], + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCallArray.default_effects(), + Some(expr.span), + )) } -/// Lowers a callable-array literal after its receiver has already been captured. -fn lower_callable_array_literal_with_receiver( +/// Boxes a post-barrier dynamic-call argument container for the eval bridge ABI. +fn coerce_eval_function_arg_array( ctx: &mut LoweringContext<'_, '_>, - items: &[Expr], - expr: &Expr, - receiver: LoweredValue, + value: LoweredValue, + span: Span, ) -> LoweredValue { - let array_ty = array_literal_type_for_ir(ctx, items, expr); - let elem_ty = indexed_array_literal_element_type(&array_ty); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - array_ty, - Op::ArrayNew.default_effects(), - Some(expr.span), - ); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, receiver.value], - None, - Op::ArrayPush.default_effects(), - Some(expr.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), receiver, expr.span); - for item in items.iter().skip(1) { - let value = lower_expr(ctx, item); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(item.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); + if matches!( + ctx.builder.value_php_type(value.value).codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { + return value; } - array + ctx.emit_value( + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) } -/// Resolves a static callable array literal as a descriptor-backed static method. -fn static_array_callable_descriptor_target( - ctx: &LoweringContext<'_, '_>, - items: &[Expr], -) -> Option { - static_array_callable_parts(ctx, items).map(|(receiver, method)| { - StaticCallableBinding::StaticMethodDescriptor { receiver, method } - }) +/// Lowers `call_user_func*` for receiver-bound first-class callables through `expr_call`. +fn lower_instance_callable_call_user_func( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + callback: StaticCallableBinding, + callback_args: &[Expr], + expr: &Expr, +) -> Option { + let result_type = static_callable_return_type(ctx, &callback); + let signature = instance_callable_signature(&callback).cloned(); + let mut operands = vec![lower_expr(ctx, callback_expr).value]; + operands.extend(lower_args_with_signature(ctx, signature.as_ref(), callback_args)); + Some(ctx.emit_value( + Op::ExprCall, + operands, + None, + result_type, + Op::ExprCall.default_effects(), + Some(expr.span), + )) } -/// Resolves a literal `[$object, "method"]` callable array as an instance method. -fn instance_array_callable_target( - ctx: &LoweringContext<'_, '_>, - items: &[Expr], -) -> Option { - let [object, method_expr] = items else { +/// Lowers dynamic `call_user_func()` callbacks through descriptor invocation. +fn lower_dynamic_call_user_func( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "call_user_func" || args.is_empty() { return None; - }; - let ExprKind::StringLiteral(method) = &method_expr.kind else { + } + if matches!(args[0].kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_)) { return None; - }; - resolve_instance_method_callable(ctx, object, method.clone(), true) -} - -/// Resolves the named static receiver and method from a static callable array literal. -fn static_array_callable_parts( - ctx: &LoweringContext<'_, '_>, - items: &[Expr], -) -> Option<(StaticReceiver, String)> { - let [class_expr, method_expr] = items else { + } + let signature = callable_descriptor_signature_for_expr(ctx, &args[0]); + let callback = lower_expr(ctx, &args[0]); + if descriptor_callback_php_type_supported(&ctx.builder.value_php_type(callback.value).codegen_repr()) { + return lower_call_user_func_descriptor_invoke_from_value( + ctx, + callback, + &args[1..], + signature.as_ref(), + expr, + ); + } + if crate::types::call_args::has_named_args(&args[1..]) || args[1..].iter().any(is_spread_arg) { return None; - }; - let class_name = static_callable_class_name(ctx, class_expr)?; - let ExprKind::StringLiteral(method) = &method_expr.kind else { + } + let mut operands = Vec::with_capacity(args.len()); + operands.push(callback.value); + operands.extend(lower_args(ctx, &args[1..])); + Some(ctx.emit_value( + Op::ExprCall, + operands, + None, + PhpType::Mixed, + Op::ExprCall.default_effects(), + Some(expr.span), + )) +} + +/// Lowers dynamic `call_user_func_array()` through the descriptor-invoker EIR path. +fn lower_dynamic_call_user_func_array( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "call_user_func_array" { + return None; + } + let [callback_expr, arg_array_expr] = args else { return None; }; - let class_name = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\'))?; - let receiver = StaticReceiver::Named(Name::from(class_name)); - static_method_implementation_signature(ctx, &receiver, method)?; - Some((receiver, method.clone())) + if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { + return None; + } + let signature = callable_descriptor_signature_for_expr(ctx, callback_expr); + let callback = lower_expr(ctx, callback_expr); + let arg_array = lower_descriptor_invoker_arg_array_for_call_user_func_array( + ctx, + arg_array_expr, + signature.as_ref(), + ) + .unwrap_or_else(|| lower_expr(ctx, arg_array_expr)); + Some(emit_callable_descriptor_invoke( + ctx, + callback, + arg_array, + PhpType::Mixed, + expr.span, + )) } -/// Extracts a compile-time class name for a static callable array. -fn static_callable_class_name( +/// Returns the callable signature available to descriptor-invoker argument lowering. +fn callable_descriptor_signature_for_expr( ctx: &LoweringContext<'_, '_>, - class_expr: &Expr, -) -> Option { - match &class_expr.kind { - ExprKind::StringLiteral(name) => Some(name.clone()), - ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver), - _ => None, + callback: &Expr, +) -> Option { + match &callback.kind { + ExprKind::Ternary { then_expr, else_expr, .. } => { + let left = callable_descriptor_signature_for_expr(ctx, then_expr)?; + let right = callable_descriptor_signature_for_expr(ctx, else_expr)?; + compatible_descriptor_signature(left, &right) + } + ExprKind::ShortTernary { value, default } => { + let left = callable_descriptor_signature_for_expr(ctx, value)?; + let right = callable_descriptor_signature_for_expr(ctx, default)?; + compatible_descriptor_signature(left, &right) + } + ExprKind::Variable(name) => ctx + .callable_param_signature(name) + .cloned() + .or_else(|| ctx.static_callable_local(name).and_then(|target| { + signature_for_static_callable_binding(ctx, target) + })), + _ => static_callable_binding_for_expr(ctx, callback) + .and_then(|target| signature_for_static_callable_binding(ctx, target)) + .or_else(|| invokable_object_signature_for_expr(ctx, callback)), } } -/// Returns the static `is_callable()` result for a literal static-method callback array. -fn static_array_callable_is_callable( +/// Returns the `__invoke` signature for an invokable object callback expression. +fn invokable_object_signature_for_expr( ctx: &LoweringContext<'_, '_>, - items: &[Expr], -) -> Option { - let [class_expr, method_expr] = items else { + callback: &Expr, +) -> Option { + let class_name = instance_callable_object_class(ctx, callback)?; + class_method_signature(ctx, &class_name, "__invoke").cloned() +} + +/// Keeps a descriptor signature only when two runtime branches have the same callable ABI. +fn compatible_descriptor_signature(left: FunctionSig, right: &FunctionSig) -> Option { + (left == *right).then_some(left) +} + +/// Extracts a callable signature from a statically understood callable binding. +fn signature_for_static_callable_binding( + ctx: &LoweringContext<'_, '_>, + target: StaticCallableBinding, +) -> Option { + match target { + StaticCallableBinding::UserFunction(name) => ctx.functions.get(&name).cloned(), + StaticCallableBinding::ExternFunction(name) => ctx + .extern_functions + .get(&name) + .map(function_sig_from_extern_for_descriptor), + StaticCallableBinding::Builtin(_) => None, + StaticCallableBinding::Closure { signature, .. } => Some(signature), + StaticCallableBinding::StaticMethod { receiver, method } + | StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { + static_method_implementation_signature(ctx, &receiver, &method).cloned() + } + StaticCallableBinding::InstanceMethod { signature, .. } => Some(signature), + } +} + +/// Converts an extern signature into the PHP-facing descriptor invoker signature. +fn function_sig_from_extern_for_descriptor(sig: &ExternFunctionSig) -> FunctionSig { + FunctionSig { + params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), + defaults: vec![None; sig.params.len()], + return_type: sig.return_type.clone(), + declared_return: true, + by_ref_return: false, + ref_params: vec![false; sig.params.len()], + declared_params: vec![true; sig.params.len()], + variadic: None, + deprecation: None, + } +} + +/// Builds an invoker argument array that preserves by-reference literal variables. +fn lower_descriptor_invoker_arg_array_for_call_user_func_array( + ctx: &mut LoweringContext<'_, '_>, + arg_array: &Expr, + sig: Option<&FunctionSig>, +) -> Option { + let ExprKind::ArrayLiteral(items) = &arg_array.kind else { return None; }; - let class_name = static_callable_class_name(ctx, class_expr)?; - let ExprKind::StringLiteral(method) = &method_expr.kind else { + if items.iter().any(is_spread_arg) || !items.iter().enumerate().any(|(index, item)| { + invoker_ref_arg_variable(ctx, sig, index, item).is_some() + }) { return None; - }; - Some(static_method_callback_is_callable(ctx, &class_name, method)) + } + + let elem_ty = PhpType::Mixed; + let array_ty = PhpType::Array(Box::new(elem_ty.clone())); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty.clone(), + Op::ArrayNew.default_effects(), + Some(arg_array.span), + ); + for (index, item) in items.iter().enumerate() { + let value = if let Some(var_name) = invoker_ref_arg_variable(ctx, sig, index, item) { + lower_invoker_ref_arg_marker(ctx, var_name, item.span) + } else { + let value = lower_expr(ctx, item); + coerce_variadic_tail_value(ctx, value, &array_ty, item.span) + }; + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, item.span); + } + Some(array) } -/// Returns true when a compile-time class/method pair names a public static method. -fn static_method_callback_is_callable( +/// Returns true when `call_user_func()` must keep runtime descriptor semantics. +fn call_user_func_should_use_descriptor( ctx: &LoweringContext<'_, '_>, - class_name: &str, - method: &str, + callback: &Expr, + args: &[Expr], + sig: Option<&FunctionSig>, ) -> bool { - let Some(class_name) = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\')) else { - return false; - }; - let Some(class_info) = ctx.classes.get(&class_name) else { - return false; - }; - let method_key = php_symbol_key(method); - if !class_info.static_methods.contains_key(&method_key) { + let has_named_or_spread = + crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg); + if has_named_or_spread { + return true; + } + if call_user_func_has_incompatible_ref_marker_arg(ctx, args, sig) { return false; } - class_info.static_method_visibilities.get(&method_key) == Some(&Visibility::Public) -} - -/// Converts a static `call_user_func_array()` argument array into call arguments. -fn static_call_user_func_array_args(arg_array: &Expr) -> Option> { - match &arg_array.kind { - ExprKind::ArrayLiteral(items) => Some(items.clone()), - ExprKind::ArrayLiteralAssoc(pairs) => static_call_user_func_array_assoc_args(pairs), - _ => None, + if sig.is_some_and(|sig| sig.ref_params.iter().any(|is_ref| *is_ref)) { + return true; } -} - -/// Converts literal associative callback arrays into positional or named call args. -fn static_call_user_func_array_assoc_args(pairs: &[(Expr, Expr)]) -> Option> { - let mut args = Vec::with_capacity(pairs.len()); - for (key, value) in pairs { - match &key.kind { - ExprKind::StringLiteral(name) => { - args.push(Expr::new( - ExprKind::NamedArg { - name: name.clone(), - value: Box::new(value.clone()), - }, - value.span, - )); + match &callback.kind { + ExprKind::ArrayLiteral(_) + | ExprKind::ArrayLiteralAssoc(_) + | ExprKind::Closure { .. } + | ExprKind::NewObject { .. } + | ExprKind::NewDynamicObject { .. } + | ExprKind::Ternary { .. } + | ExprKind::ShortTernary { .. } + | ExprKind::FirstClassCallable(CallableTarget::Method { .. }) => true, + ExprKind::Variable(name) => { + if let Some(target) = ctx.static_callable_local(name) { + return matches!( + target, + StaticCallableBinding::Closure { .. } + | StaticCallableBinding::StaticMethodDescriptor { .. } + | StaticCallableBinding::InstanceMethod { .. } + ); } - ExprKind::IntLiteral(_) => args.push(value.clone()), - _ => return None, + matches!( + ctx.local_type(name).codegen_repr(), + PhpType::Callable | PhpType::Array(_) | PhpType::Object(_) + ) } + _ => false, } - Some(args) } -/// Lowers one resolved static callable target to the corresponding EIR call opcode. -fn lower_static_callable_call( - ctx: &mut LoweringContext<'_, '_>, - target: StaticCallableBinding, - callback_args: &[Expr], - expr: &Expr, -) -> Option { - match target { - StaticCallableBinding::UserFunction(function_name) => { - let sig = ctx.functions.get(&function_name).cloned(); - let operands = lower_args_with_signature(ctx, sig.as_ref(), callback_args); - let php_type = call_return_type(ctx, &function_name, &operands); - let data = ctx.intern_function_name(&function_name); - Some(ctx.emit_value( - Op::Call, - operands, - Some(Immediate::Data(data)), - php_type, - effects_lookup::user_call_effects(&function_name), - Some(expr.span), - )) - } - StaticCallableBinding::ExternFunction(function_name) => { - let sig = ctx - .extern_functions - .get(&function_name) - .map(function_sig_from_extern_for_descriptor); - let operands = lower_args_with_signature(ctx, sig.as_ref(), callback_args); - let php_type = call_return_type(ctx, &function_name, &operands); - let data = ctx.intern_function_name(&function_name); - Some(ctx.emit_value( - Op::ExternCall, - operands, - Some(Immediate::Data(data)), - php_type, - Op::ExternCall.default_effects(), - Some(expr.span), - )) - } - StaticCallableBinding::Builtin(function_name) => { - let sig = call_signature(ctx, &function_name, callback_args); - let operands = lower_builtin_call_args(ctx, &function_name, sig.as_ref(), callback_args); - let php_type = call_return_type(ctx, &function_name, &operands); - Some(emit_builtin_call_value(ctx, &function_name, operands, php_type, expr.span)) - } - StaticCallableBinding::Closure { - name, - signature, - captures, - } => { - let mut operands = lower_args_with_signature(ctx, Some(&signature), callback_args); - append_closure_capture_operands(&mut operands, &captures); - let php_type = normalize_value_php_type(signature.return_type.codegen_repr()); - let data = ctx.intern_function_name(&name); - Some(ctx.emit_value( - Op::Call, - operands, - Some(Immediate::Data(data)), - php_type, - effects_lookup::user_call_effects(&name), - Some(expr.span), - )) - } - StaticCallableBinding::StaticMethod { receiver, method } => { - Some(lower_static_method_call(ctx, &receiver, &method, callback_args, expr)) - } - StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { - Some(lower_static_method_descriptor_call( - ctx, - &receiver, - &method, - callback_args, - expr, - )) - } - StaticCallableBinding::InstanceMethod { - object, - method, - direct_call: true, - .. - } => Some(lower_method_call(ctx, &object, &method, callback_args, Op::MethodCall, expr)), - StaticCallableBinding::InstanceMethod { .. } => None, - } -} - -/// Resolves a PHP string callback using case-insensitive function lookup rules. -fn resolve_static_string_callable( - ctx: &LoweringContext<'_, '_>, - callback: &str, -) -> Option { - let callback = callback.trim_start_matches('\\'); - if let Some((class_name, method)) = callback.rsplit_once("::") { - let class_name = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\'))?; - return resolve_static_method_callable( - ctx, - StaticReceiver::Named(Name::from(class_name)), - method.to_string(), - ); - } - if let Some(function_name) = lookup_folded_name(ctx.extern_functions.keys(), callback) { - return Some(StaticCallableBinding::ExternFunction(function_name)); - } - if let Some(function_name) = lookup_folded_name(ctx.functions.keys(), callback) { - return Some(StaticCallableBinding::UserFunction(function_name)); - } - canonical_builtin_function_name(callback).map(StaticCallableBinding::Builtin) -} - -/// Appends captured closure values after caller-visible operands for hidden ABI params. -fn append_closure_capture_operands(operands: &mut Vec, captures: &[ClosureCapture]) { - operands.extend(captures.iter().map(|capture| capture.value)); -} - -/// Resolves a static method callback when class and method are compile-time known. -fn resolve_static_method_callable( - ctx: &LoweringContext<'_, '_>, - receiver: StaticReceiver, - method: String, -) -> Option { - static_method_implementation_signature(ctx, &receiver, &method)?; - Some(StaticCallableBinding::StaticMethod { receiver, method }) -} - -/// Resolves a first-class instance-method callable to signature metadata only. -fn resolve_instance_method_callable( +/// Returns true when direct descriptor ref markers cannot represent an argument. +fn call_user_func_has_incompatible_ref_marker_arg( ctx: &LoweringContext<'_, '_>, - object: &Expr, - method: String, - direct_call: bool, -) -> Option { - let class_name = instance_callable_object_class(ctx, object)?; - let method_key = php_symbol_key(&method); - let signature = class_method_signature(ctx, &class_name, &method_key)?.clone(); - Some(StaticCallableBinding::InstanceMethod { - object: Box::new(object.clone()), - method, - signature, - direct_call, + args: &[Expr], + sig: Option<&FunctionSig>, +) -> bool { + let Some(sig) = sig else { + return false; + }; + args.iter().enumerate().any(|(index, arg)| { + if !sig.ref_params.get(index).copied().unwrap_or(false) { + return false; + } + let ExprKind::Variable(name) = &arg.kind else { + return false; + }; + !invoker_ref_arg_storage_compatible(ctx, sig, index, name) }) } -/// Returns a static callable only when it can be lowered without descriptor state. -fn direct_static_callable_binding(target: StaticCallableBinding) -> Option { - if matches!(target, StaticCallableBinding::InstanceMethod { .. }) { - None - } else { - Some(target) - } -} - -/// Resolves the concrete class for an object expression used in an instance FCC. -/// Returns the property type referenced by a `Closure::bind(fn () => $this->prop, $newThis, …)` -/// when the closure body is exactly `return $this->prop`: the bound object's property type. -/// The closure's own `$this` is Mixed (it is bound dynamically), so the type comes from the -/// bind's receiver argument. -fn closure_bind_property_return_type( - ctx: &LoweringContext<'_, '_>, - callee: &Expr, -) -> Option { - let ExprKind::StaticMethodCall { receiver, method, args } = &callee.kind else { - return None; - }; - if !method.eq_ignore_ascii_case("bind") { - return None; - } - let crate::parser::ast::StaticReceiver::Named(name) = receiver else { - return None; - }; - if !name - .as_str() - .trim_start_matches('\\') - .eq_ignore_ascii_case("Closure") - { - return None; - } - let ExprKind::Closure { body, .. } = &args.first()?.kind else { - return None; - }; - let new_this_class = instance_callable_object_class(ctx, args.get(1)?)?; - let [stmt] = body.as_slice() else { - return None; - }; - let StmtKind::Return(Some(ret)) = &stmt.kind else { - return None; - }; - let ExprKind::PropertyAccess { object, property } = &ret.kind else { - return None; - }; - if !matches!(object.kind, ExprKind::This) { +/// Lowers `call_user_func()` into a descriptor invoke when the callback value is supported. +fn lower_call_user_func_descriptor_invoke( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + args: &[Expr], + sig: Option<&FunctionSig>, + expr: &Expr, +) -> Option { + let callback = lower_expr(ctx, callback_expr); + if !descriptor_callback_php_type_supported(&ctx.builder.value_php_type(callback.value).codegen_repr()) { return None; } - let info = ctx.classes.get(new_this_class.trim_start_matches('\\'))?; - info.properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| ty.clone()) + lower_call_user_func_descriptor_invoke_from_value(ctx, callback, args, sig, expr) } -/// Lowers `Closure::bind(fn &() => $this->prop, $newThis, scope)()` as a direct call to the -/// closure with `$newThis` boxed as its `$this` capture. -/// -/// `Closure::bind` rebinds the closure's receiver; invoking the result through the generic -/// runtime descriptor invoker boxes the closure's return value as Mixed, which cannot carry a -/// by-reference property cell pointer. Calling the closure directly (as `$f()` does) passes the -/// cell pointer through. The call result is typed from the bound receiver's property so a -/// by-reference array return binds correctly. Only the auto-captured `$this` shape (the -/// `fn &() => $this->prop` form) is handled; other captures fall back to the generic path. -fn lower_bound_closure_immediate_call( +/// Emits `CallableDescriptorInvoke` for an already evaluated `call_user_func()` callback. +fn lower_call_user_func_descriptor_invoke_from_value( ctx: &mut LoweringContext<'_, '_>, - callee: &Expr, + callback: LoweredValue, args: &[Expr], + sig: Option<&FunctionSig>, expr: &Expr, ) -> Option { - let (bound, _closure_value) = build_bound_closure_binding(ctx, callee, expr)?; - lower_static_callable_call(ctx, bound, args, expr) + let arg_container = lower_descriptor_invoker_arg_container_for_call_user_func(ctx, args, sig, expr.span)?; + let result_type = sig + .map(|sig| normalize_value_php_type(sig.return_type.codegen_repr())) + .unwrap_or(PhpType::Mixed); + Some(emit_callable_descriptor_invoke( + ctx, + callback, + arg_container, + result_type, + expr.span, + )) } -/// Builds the static-callable binding for `Closure::bind(fn &() => $this->prop, $newThis, scope)`. -/// -/// Lowers the closure literal (once), boxes `$newThis` as the closure's `$this` capture, and -/// overrides the binding's return type with the bound receiver's property type so a -/// by-reference return binds correctly. Returns the binding together with the lowered closure -/// descriptor value (the still-unbound `closure_new`), which callers may store in the assigned -/// variable. `None` unless the call is the single auto-captured `$this` shape — the only form -/// whose `$this` is fully known at compile time. Shared by the immediate-invoke path -/// (`Closure::bind(...)()`) and the variable-assignment path (`$b = Closure::bind(...)`). -fn build_bound_closure_binding( +/// Emits a descriptor invoke and releases an owned argument container after the call. +fn emit_callable_descriptor_invoke( ctx: &mut LoweringContext<'_, '_>, - callee: &Expr, - expr: &Expr, -) -> Option<(StaticCallableBinding, LoweredValue)> { - let result_type = closure_bind_property_return_type(ctx, callee)?; - let ExprKind::StaticMethodCall { args: bind_args, .. } = &callee.kind else { - return None; - }; - let closure_lit = bind_args.first()?; - if !matches!(closure_lit.kind, ExprKind::Closure { .. }) { - return None; - } - let new_this = bind_args.get(1)?.clone(); - // Lower the closure literal to obtain its static binding (function name + captures). - let closure_value = lower_expr(ctx, closure_lit); - let Some(StaticCallableBinding::Closure { - name, - mut signature, - captures, - }) = ctx.take_pending_static_callable_result() - else { - return None; - }; - // Only the single auto-captured `$this` shape is supported here. - if captures.len() != 1 { - return None; - } - let new_this_value = lower_expr(ctx, &new_this); - let boxed_this = ctx.emit_value( - Op::MixedBox, - vec![new_this_value.value], + callback: LoweredValue, + arg_container: LoweredValue, + result_type: PhpType, + span: Span, +) -> LoweredValue { + let result = ctx.emit_value( + Op::CallableDescriptorInvoke, + vec![callback.value, arg_container.value], None, - PhpType::Mixed, - Op::MixedBox.default_effects(), - Some(expr.span), + result_type, + Op::CallableDescriptorInvoke.default_effects(), + Some(span), ); - signature.return_type = result_type; - let bound = StaticCallableBinding::Closure { - name, - signature, - captures: vec![ClosureCapture { - value: boxed_this.value, - }], - }; - Some((bound, closure_value)) + if ctx.value_is_owning_temporary(arg_container) { + crate::ir_lower::ownership::release_if_owned(ctx, arg_container, Some(span)); + } + result } -/// Returns true when an assignment value is a by-reference `Closure::bind` of the auto-`$this` -/// shape, so the assignment should track the result as a static callable (routing a later -/// `$b()` through the direct-call path that carries the by-reference cell pointer). -/// -/// Read-only structural check (no IR emitted) used to set `direct_closure` before lowering. -pub(crate) fn is_bound_closure_assignment_shape(ctx: &LoweringContext<'_, '_>, value: &Expr) -> bool { - let ExprKind::StaticMethodCall { args, .. } = &value.kind else { - return false; - }; - let Some(ExprKind::Closure { by_ref_return, .. }) = args.first().map(|arg| &arg.kind) else { - return false; - }; - *by_ref_return && closure_bind_property_return_type(ctx, value).is_some() +/// Returns true when the EIR backend has descriptor dispatch for this callback type. +fn descriptor_callback_php_type_supported(php_type: &PhpType) -> bool { + matches!( + php_type, + PhpType::Str | PhpType::Callable | PhpType::Array(_) | PhpType::Object(_) + ) } -/// Lowers `$b = Closure::bind(fn &() => $this->prop, $newThis, scope)` for assignment: builds -/// the bound-closure binding, publishes it as the pending static callable so the assignment -/// registers `$b` for later direct `$b()` calls, and returns the closure descriptor to store -/// in `$b`. `None` for any non-matching shape so normal assignment lowering applies. -pub(crate) fn lower_bound_closure_for_assignment( +/// Builds the descriptor-invoker argument container for `call_user_func()`. +fn lower_descriptor_invoker_arg_container_for_call_user_func( ctx: &mut LoweringContext<'_, '_>, - value: &Expr, + args: &[Expr], + sig: Option<&FunctionSig>, + span: Span, ) -> Option { - let (bound, closure_value) = build_bound_closure_binding(ctx, value, value)?; - ctx.set_pending_static_callable_result(bound); - Some(closure_value) -} - -/// Resolves the statically-known class name of an object expression used as the -/// receiver of an instance first-class callable (`$obj->m(...)`). -/// -/// Returns the normalized class name for `$var` (from `local_types`), `$this` -/// (the current class), and `new` expressions; `None` when the receiver class -/// cannot be determined statically. -fn instance_callable_object_class( - ctx: &LoweringContext<'_, '_>, - object: &Expr, -) -> Option { - match &object.kind { - ExprKind::Variable(name) => ctx - .local_types - .get(name) - .and_then(class_name_from_php_type), - ExprKind::This => ctx.current_class.as_deref().and_then(normalized_class_name), - ExprKind::NewObject { class_name, .. } => normalized_class_name(class_name.as_str()), - ExprKind::NewDynamicObject { fallback_class, .. } => { - normalized_class_name(fallback_class.as_str()) + if crate::types::call_args::has_named_args(args) { + if args.iter().any(is_spread_arg) { + return None; } - ExprKind::FunctionCall { name, .. } => ctx - .functions - .get(name.as_str()) - .and_then(|sig| class_name_from_php_type(&sig.return_type)), - _ => class_name_from_php_type(&infer_expr_type_syntactic(object)), - } -} - -/// Returns a non-empty normalized class name for an object PHP type. -fn class_name_from_php_type(ty: &PhpType) -> Option { - match ty.codegen_repr() { - PhpType::Object(class_name) => normalized_class_name(&class_name), - _ => None, - } -} - -/// Trims PHP's optional leading namespace separator from class metadata names. -fn normalized_class_name(class_name: &str) -> Option { - let class_name = class_name.trim_start_matches('\\'); - if class_name.is_empty() { - None - } else { - Some(class_name.to_string()) + return Some(lower_named_descriptor_invoker_arg_container(ctx, args, sig, span)); } + Some(lower_indexed_descriptor_invoker_arg_array(ctx, args, sig, span)) } -/// Looks up a PHP function name case-insensitively and returns the canonical candidate. -fn lookup_folded_name<'a, I>(names: I, requested: &str) -> Option -where - I: IntoIterator, -{ - let requested = php_symbol_key(requested); - names - .into_iter() - .find(|candidate| php_symbol_key(candidate) == requested) - .cloned() -} - -/// Returns the caller-visible signature used to normalize direct call operands. -fn call_signature( - ctx: &LoweringContext<'_, '_>, - name: &str, +/// Builds an indexed `array` argument container, expanding positional spreads. +fn lower_indexed_descriptor_invoker_arg_array( + ctx: &mut LoweringContext<'_, '_>, args: &[Expr], -) -> Option { - if let Some(sig) = ctx.functions.get(name) { - return Some(sig.clone()); - } - if let Some(sig) = ctx.extern_functions.get(name) { - return Some(function_sig_from_extern_for_descriptor(sig)); - } - if crate::types::call_args::has_named_args(args) { - return builtin_call_signature(name); + sig: Option<&FunctionSig>, + span: Span, +) -> LoweredValue { + let elem_ty = PhpType::Mixed; + let array_ty = PhpType::Array(Box::new(elem_ty.clone())); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(args.len() as u32)), + array_ty.clone(), + Op::ArrayNew.default_effects(), + Some(span), + ); + let mut positional_index = 0usize; + for arg in args { + if let ExprKind::Spread(inner) = &arg.kind { + let source = lower_expr(ctx, inner); + lower_indexed_array_spread_into_array(ctx, array, source, Some(&elem_ty), arg.span); + continue; + } + let value = if let Some(var_name) = invoker_ref_arg_variable(ctx, sig, positional_index, arg) { + lower_invoker_ref_arg_marker(ctx, var_name, arg.span) + } else { + let value = lower_expr(ctx, arg); + coerce_variadic_tail_value(ctx, value, &array_ty, arg.span) + }; + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(arg.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, arg.span); + positional_index += 1; } - None -} - -/// Looks up a PHP builtin call signature using the normalized global builtin name. -fn builtin_call_signature(name: &str) -> Option { - crate::types::builtin_call_sig(&php_symbol_key(name.trim_start_matches('\\'))) -} - -/// Looks up precise first-class builtin metadata using the normalized global builtin name. -fn first_class_builtin_signature(name: &str) -> Option { - crate::types::first_class_callable_builtin_sig(&php_symbol_key(name.trim_start_matches('\\'))) + array } -/// Lowers supported `unset(...)` targets without evaluating them as ordinary call args. -fn lower_unset_locals( +/// Builds a boxed hash argument container for named `call_user_func()` args. +fn lower_named_descriptor_invoker_arg_container( ctx: &mut LoweringContext<'_, '_>, args: &[Expr], - expr: &Expr, -) -> Option { - if !args.iter().all(|arg| unset_target_supported(ctx, arg)) { - return None; - } - let null = lower_null(ctx, expr); + sig: Option<&FunctionSig>, + span: Span, +) -> LoweredValue { + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(args.len() as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(span), + ); + let mut next_positional_key = 0i64; for arg in args { match &arg.kind { - ExprKind::Variable(name) => { - ctx.unset_local(name, null, Some(arg.span)); - } - ExprKind::ArrayAccess { array, index } => { - lower_unset_array_access(ctx, array, index, arg); + ExprKind::NamedArg { name, value } => { + let key = lower_string_literal(ctx, name, arg); + let param_index = sig.and_then(|sig| { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + crate::types::call_args::named_param_index(sig, regular_param_count, name) + }); + let value = if let Some(index) = param_index { + invoker_ref_arg_variable(ctx, sig, index, value) + .map(|var_name| lower_invoker_ref_arg_marker(ctx, var_name, value.span)) + } else { + None + } + .unwrap_or_else(|| lower_expr(ctx, value)); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(arg.span), + ); } - // `unset($obj->prop)` on an undeclared property dispatches to `__unset`. - ExprKind::PropertyAccess { object, property } => { - let synthetic = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__unset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - lower_expr(ctx, &synthetic); - } - ExprKind::NullsafePropertyAccess { object, property } => { - let synthetic = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__unset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, + _ => { + let key = emit_i64_at_span(ctx, next_positional_key, arg.span); + let value = if let Some(var_name) = + invoker_ref_arg_variable(ctx, sig, next_positional_key as usize, arg) + { + lower_invoker_ref_arg_marker(ctx, var_name, arg.span) + } else { + lower_expr(ctx, arg) + }; + next_positional_key += 1; + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(arg.span), ); - lower_expr(ctx, &synthetic); } - _ => {} - } - } - crate::ir_lower::ownership::collect_cycles(ctx, Some(expr.span)); - Some(null) -} - -/// Returns true when an `unset(...)` target has direct EIR lowering. -fn unset_target_supported(ctx: &LoweringContext<'_, '_>, arg: &Expr) -> bool { - match &arg.kind { - ExprKind::Variable(_) => true, - ExprKind::ArrayAccess { array, .. } => { - unset_array_access_has_object_receiver(ctx, array) - || unset_array_access_has_local_array_receiver(ctx, array) - } - ExprKind::PropertyAccess { object, property } - | ExprKind::NullsafePropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__unset").is_some() } - _ => false, } + ctx.emit_value( + Op::MixedBox, + vec![hash.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) } -/// Returns true when an array-access unset receiver is a plain array/hash local whose element the -/// EIR backend can remove. -/// -/// Associative arrays remove the element directly; packed indexed arrays are converted to a hash at -/// the unset site (PHP `unset()` leaves a sparse array). By-reference locals are excluded: their -/// storage is aliased to a caller whose static type would no longer match after a representation -/// change. -fn unset_array_access_has_local_array_receiver( - ctx: &LoweringContext<'_, '_>, - array: &Expr, -) -> bool { - let ExprKind::Variable(name) = &array.kind else { - return false; +/// Returns the variable name when this literal argument should be passed by reference. +fn invoker_ref_arg_variable<'a>( + _ctx: &LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + index: usize, + item: &'a Expr, +) -> Option<&'a str> { + let ExprKind::Variable(name) = &item.kind else { + return None; }; - if ctx.is_ref_bound_local(name) { - return false; + if let Some(sig) = sig { + if !sig.ref_params.get(index).copied().unwrap_or(false) { + return None; + } } - matches!( - ctx.local_type(name).codegen_repr(), - PhpType::AssocArray { .. } | PhpType::Array(_) - ) + Some(name.as_str()) } -/// Returns true when an array-access unset receiver is a static ArrayAccess object. -fn unset_array_access_has_object_receiver( +/// Returns true when a local slot can be passed directly to a descriptor ref param. +fn invoker_ref_arg_storage_compatible( ctx: &LoweringContext<'_, '_>, - array: &Expr, + sig: &FunctionSig, + index: usize, + var_name: &str, ) -> bool { - let ty = match &array.kind { - ExprKind::Variable(name) => ctx - .local_types - .get(name) - .cloned() - .unwrap_or_else(|| infer_expr_type_syntactic(array)), - _ => infer_expr_type_syntactic(array), + let Some((_, param_ty)) = sig.params.get(index) else { + return true; }; - type_satisfies_array_access_for_ir(ctx, &ty) -} - -/// Lowers `unset($array[$key])`, dispatching on the receiver kind. -/// -/// An associative-array local removes the element in place through `Op::HashUnset`. A packed -/// indexed-array local is first converted to a hash (PHP keeps the surviving keys without -/// renumbering) and then removed. An `ArrayAccess` object dispatches to its `offsetUnset($key)` -/// method like before. By-reference array locals fall through to the object path. -fn lower_unset_array_access( - ctx: &mut LoweringContext<'_, '_>, - array: &Expr, - index: &Expr, - expr: &Expr, -) { - if let ExprKind::Variable(name) = &array.kind { - if !ctx.is_ref_bound_local(name) { - match ctx.local_type(name).codegen_repr() { - PhpType::AssocArray { .. } => { - lower_unset_hash_element(ctx, name, array.span, index, expr); - return; - } - PhpType::Array(elem_ty) => { - let elem_ty = if *elem_ty == PhpType::Never { - PhpType::Mixed - } else { - *elem_ty - }; - lower_unset_indexed_element(ctx, name, elem_ty, array.span, index, expr); - return; - } - _ => {} - } - } - } - let synthetic = Expr::new( - ExprKind::MethodCall { - object: Box::new(array.clone()), - method: "offsetUnset".to_string(), - args: vec![index.clone()], - }, - expr.span, - ); - lower_expr(ctx, &synthetic); + value_ir_type(¶m_ty.codegen_repr()) == value_ir_type(&ctx.local_type(var_name).codegen_repr()) } -/// Lowers `unset($hash[$key])` for an associative-array local as a `HashUnset` instruction. -/// -/// Loads the array local, lowers the key, and emits the removal. The backend (`lower_hash_unset`) -/// copy-on-write splits the table, releases the removed key/value payloads, and stores the unique -/// table pointer back into the local slot, so no explicit store-back is needed here. -fn lower_unset_hash_element( +/// Emits an invoker reference-cell marker for a local variable argument. +fn lower_invoker_ref_arg_marker( ctx: &mut LoweringContext<'_, '_>, - name: &str, - array_span: Span, - index: &Expr, - expr: &Expr, -) { - let array_value = ctx.load_local(name, Some(array_span)); - let index_value = lower_expr(ctx, index); - ctx.emit_void( - Op::HashUnset, - vec![array_value.value, index_value.value], - None, - Op::HashUnset.default_effects(), - Some(expr.span), - ); + var_name: &str, + span: Span, +) -> LoweredValue { + let php_type = ctx.local_type(var_name); + let slot = ctx.declare_local(var_name, php_type); + ctx.emit_value( + Op::InvokerRefArg, + Vec::new(), + Some(Immediate::LocalSlot(slot)), + PhpType::Mixed, + Op::InvokerRefArg.default_effects(), + Some(span), + ) } -/// Lowers `unset($arr[$key])` for a packed indexed-array local. -/// -/// PHP's `unset()` removes a key without renumbering, so the array can no longer be a contiguous -/// packed list (e.g. `unset([1,2,3][1])` leaves keys `0` and `2`). The local is converted to a hash -/// (`Op::ArrayToHash`) and retyped as `AssocArray`, after which the element is removed -/// through `HashUnset`. Subsequent uses of the local therefore see the associative representation. -fn lower_unset_indexed_element( +/// Lowers `array_map()` for a static callback and indexed array literal source. +fn lower_static_array_map( ctx: &mut LoweringContext<'_, '_>, name: &str, - elem_ty: PhpType, - array_span: Span, - index: &Expr, + args: &[Expr], expr: &Expr, -) { - let array_value = ctx.load_local(name, Some(array_span)); - let assoc_ty = PhpType::AssocArray { - key: Box::new(PhpType::Int), - value: Box::new(elem_ty), +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "array_map" || args.len() != 2 { + return None; + } + if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { + return None; + } + if matches!(args[0].kind, ExprKind::Variable(_)) { + return None; + } + let callback = static_call_user_func_callback(ctx, &args[0])?; + let ExprKind::ArrayLiteral(items) = &args[1].kind else { + return None; }; - let hash = ctx.emit_value( - Op::ArrayToHash, - vec![array_value.value], - None, - assoc_ty.clone(), - Op::ArrayToHash.default_effects(), - Some(array_span), + let elem_type = static_callable_return_type(ctx, &callback); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + PhpType::Array(Box::new(elem_type.clone())), + Op::ArrayNew.default_effects(), + Some(expr.span), ); - ctx.store_mutated_local(name, hash, assoc_ty, Some(array_span)); - lower_unset_hash_element(ctx, name, array_span, index, expr); + for item in items { + let value = lower_static_callable_call(ctx, callback.clone(), std::slice::from_ref(item), expr)?; + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_type), value, item.span); + } + Some(array) } -/// Lowers `array_push($local, $value)` as a direct indexed-array mutation. -fn lower_static_array_push( +/// Lowers `array_reduce()` for a static callback and immediate indexed-array literal. +fn lower_static_array_reduce( ctx: &mut LoweringContext<'_, '_>, name: &str, args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "array_push" || args.len() != 2 { + if php_symbol_key(name.trim_start_matches('\\')) != "array_reduce" || args.len() != 3 { return None; } if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { return None; } - let ExprKind::Variable(array_name) = &args[0].kind else { - return None; - }; - if !matches!(ctx.local_type(array_name).codegen_repr(), PhpType::Array(_)) { + if matches!(args[1].kind, ExprKind::Variable(_)) { return None; } - let array_value = ctx.load_local(array_name, Some(args[0].span)); - if array_value.ir_type != IrType::Heap(IrHeapKind::Array) { + let ExprKind::ArrayLiteral(items) = &args[0].kind else { + return None; + }; + if !items.iter().all(static_callback_array_item_can_inline) { return None; } - let value = lower_expr(ctx, &args[1]); - let (array_value, updated_ty, needs_storeback) = - if super::stmt::ref_bound_mixed_indexed_array_write(ctx, array_name, value) { - (array_value, Some(ctx.local_type(array_name)), true) - } else { - super::stmt::prepare_indexed_array_local_write(ctx, array_value, value, expr.span) - }; - ctx.emit_void( - Op::ArrayPush, - vec![array_value.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(expr.span), - ); - let elem_ty = super::stmt::indexed_array_write_element_type( - ctx, - array_value, - updated_ty.as_ref(), - ); - super::stmt::finish_indexed_array_local_write( - ctx, - array_name, - array_value, - updated_ty, - needs_storeback, - expr.span, - ); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, expr.span); - Some(lower_null(ctx, expr)) + let callback = static_call_user_func_callback(ctx, &args[1])?; + let result_type = fallback_expr_type(expr); + let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); + let initial = lower_expr(ctx, &args[2]); + store_value_into_temp(ctx, &temp_name, result_type.clone(), initial, expr.span); + for item in items { + let carry = ctx.load_local(&temp_name, Some(expr.span)); + let item_value = lower_expr(ctx, item); + let reduced = lower_static_callable_value_call( + ctx, + callback.clone(), + vec![carry.value, item_value.value], + expr, + )?; + store_value_into_temp(ctx, &temp_name, result_type.clone(), reduced, expr.span); + } + Some(take_owned_temp(ctx, &temp_name, expr.span)) } -/// Lowers builtin call operands, applying builtin-specific preservation where source order matters. -fn lower_builtin_call_args( +/// Lowers `array_walk()` for a static callback and immediate indexed-array literal. +fn lower_static_array_walk( ctx: &mut LoweringContext<'_, '_>, name: &str, - sig: Option<&FunctionSig>, args: &[Expr], -) -> Vec { - if is_empty_static_indexed_spread_arg(args) && zero_arity_call_signature(name, sig) { - return Vec::new(); + expr: &Expr, +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "array_walk" || args.len() != 2 { + return None; } - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "date" => lower_date_args(ctx, sig, args), - "json_decode" => lower_json_decode_args(ctx, sig, args), - "preg_replace_callback" - if !crate::types::call_args::has_named_args(args) - && !args.iter().any(is_spread_arg) => - { - lower_preg_replace_callback_args(ctx, sig, args) - } - "preg_match" | "preg_split" - if !crate::types::call_args::has_named_args(args) - && !args.iter().any(is_spread_arg) => - { - lower_args(ctx, args) - } - "usort" | "uasort" - if !crate::types::call_args::has_named_args(args) - && !args.iter().any(is_spread_arg) => - { - lower_user_value_sort_args(ctx, sig, args) - } - _ => lower_args_with_signature(ctx, sig, args), + if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { + return None; } -} - -/// Lowers `usort`/`uasort` arguments, typing an unannotated comparator closure -/// against the array's object element type. -/// -/// `usort`/`uasort` compare values, so a comparator over an array of objects must -/// see each element as the object handle — for `<=>` instant comparison and for -/// property/method access — not the raw pointer-sized integer the runtime stores -/// in each slot. The array operand is lowered exactly as the default positional -/// path would (positional builtin calls reach here with no signature); only an -/// unannotated closure comparator over an object-element array is specialized, -/// matching the element-type hint the checker applied to the comparator body. -fn lower_user_value_sort_args( - ctx: &mut LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - args: &[Expr], -) -> Vec { - if args.len() != 2 || !matches!(&args[1].kind, ExprKind::Closure { .. }) { - return lower_args_with_signature(ctx, sig, args); + if matches!(args[1].kind, ExprKind::Variable(_)) { + return None; } - // The mutating sort keeps its by-reference local storeback in the EIR backend, - // so the array operand only has to resolve to the array's value here. - let array = match sig { - Some(sig) => lower_arg_with_signature(ctx, sig, 0, &args[0]), - None => lower_expr(ctx, &args[0]).value, - }; - let elem_ty = match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::Array(elem) => elem.codegen_repr(), - _ => PhpType::Int, - }; - // Only an object-element array needs the comparator parameters re-typed; scalar - // comparators already lower correctly through the default path. - let callback = if matches!(elem_ty, PhpType::Object(_)) { - lower_value_sort_comparator_closure(ctx, &args[1], elem_ty) - } else { - match sig { - Some(sig) => lower_arg_with_signature(ctx, sig, 1, &args[1]), - None => lower_expr(ctx, &args[1]).value, - } + let ExprKind::ArrayLiteral(items) = &args[0].kind else { + return None; }; - vec![array, callback] + if !items.iter().all(static_callback_array_item_can_inline) { + return None; + } + let callback = static_call_user_func_callback(ctx, &args[1])?; + for item in items { + let item_value = lower_expr(ctx, item); + lower_static_callable_value_call(ctx, callback.clone(), vec![item_value.value], expr)?; + } + Some(lower_null(ctx, expr)) } -/// Lowers a value-sort comparator closure with both parameters typed as the array element. -/// -/// Falls back to the plain closure lowering for any non-closure callback operand, -/// though callers only reach this path with a closure comparator. -fn lower_value_sort_comparator_closure( - ctx: &mut LoweringContext<'_, '_>, - callback: &Expr, - elem_ty: PhpType, -) -> crate::ir::ValueId { - let ExprKind::Closure { - params, - variadic, - return_type, - body, - captures, - capture_refs, - is_static, - .. - } = &callback.kind - else { - return lower_expr(ctx, callback).value; - }; - lower_closure_with_context( - ctx, - params, - variadic.as_deref(), - return_type.as_ref(), - body, - captures, - capture_refs, - callback, - &[elem_ty.clone(), elem_ty], - None, - *is_static, +/// Returns whether a literal array element can be reordered around callback invocation safely. +fn static_callback_array_item_can_inline(item: &Expr) -> bool { + matches!( + item.kind, + ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::Null ) - .value } -/// Returns true when the call uses exactly one static empty indexed spread. -fn is_empty_static_indexed_spread_arg(args: &[Expr]) -> bool { - let [arg] = args else { - return false; - }; - let ExprKind::Spread(inner) = &arg.kind else { - return false; - }; - matches!(&inner.kind, ExprKind::ArrayLiteral(items) if items.is_empty()) -} - -/// Returns true when the callable signature accepts no visible operands. -fn zero_arity_call_signature(name: &str, sig: Option<&FunctionSig>) -> bool { - if let Some(sig) = sig { - return is_zero_arity_signature(sig); +/// Returns the best known element type for a static callback used by `array_map()`. +fn static_callable_return_type( + ctx: &LoweringContext<'_, '_>, + target: &StaticCallableBinding, +) -> PhpType { + match target { + StaticCallableBinding::UserFunction(name) + | StaticCallableBinding::ExternFunction(name) + | StaticCallableBinding::Builtin(name) => call_return_type(ctx, name, &[]), + StaticCallableBinding::Closure { signature, .. } => { + normalize_value_php_type(signature.return_type.codegen_repr()) + } + StaticCallableBinding::StaticMethod { receiver, method } + | StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { + static_method_implementation_signature(ctx, receiver, method) + .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) + .unwrap_or(PhpType::Mixed) + } + StaticCallableBinding::InstanceMethod { signature, .. } => { + normalize_value_php_type(signature.return_type.codegen_repr()) + } } - builtin_call_signature(name) - .as_ref() - .is_some_and(is_zero_arity_signature) -} - -/// Returns true when a signature has no regular or variadic parameters. -fn is_zero_arity_signature(sig: &FunctionSig) -> bool { - crate::types::call_args::regular_param_count(sig) == 0 && sig.variadic.is_none() } -/// Lowers `settype($local, "type")` and updates subsequent local type facts. -fn lower_static_settype( +/// Lowers one resolved static callable target using already-evaluated positional operands. +fn lower_static_callable_value_call( ctx: &mut LoweringContext<'_, '_>, - name: &str, - args: &[Expr], + target: StaticCallableBinding, + operands: Vec, expr: &Expr, ) -> Option { - if php_symbol_key(name.trim_start_matches('\\')) != "settype" { - return None; - } - let (var_arg, type_arg) = static_settype_arg_exprs(ctx, name, args)?; - let ExprKind::Variable(local_name) = &var_arg.kind else { - return None; - }; - let target_ty = static_settype_target_type(&type_arg)?; - let sig = call_signature(ctx, name, args); - let operands = lower_builtin_call_args(ctx, name, sig.as_ref(), args); - let result = emit_builtin_call_value(ctx, name, operands, PhpType::Bool, expr.span); - ctx.set_local_type(local_name, target_ty); - Some(result) -} - -/// Returns canonical `settype()` argument expressions for static local mutation lowering. -fn static_settype_arg_exprs( - ctx: &LoweringContext<'_, '_>, - name: &str, - args: &[Expr], -) -> Option<(Expr, Expr)> { - if args.len() != 2 || args.iter().any(is_spread_arg) { - return None; - } - if !crate::types::call_args::has_named_args(args) { - return Some((args[0].clone(), args[1].clone())); - } - let sig = call_signature(ctx, name, args)?; - let call_span = args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let regular_param_count = crate::types::call_args::regular_param_count(&sig); - let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - &sig, - args, - call_span, - regular_param_count, - false, - true, - &assoc_spread_sources(ctx, args), - ) - .ok()?; - if plan.has_spread_args() || plan.regular_args.len() != 2 { - return None; + match target { + StaticCallableBinding::UserFunction(function_name) => { + let php_type = call_return_type(ctx, &function_name, &operands); + let data = ctx.intern_function_name(&function_name); + Some(ctx.emit_value( + Op::Call, + operands, + Some(Immediate::Data(data)), + php_type, + effects_lookup::user_call_effects(&function_name), + Some(expr.span), + )) + } + StaticCallableBinding::ExternFunction(function_name) => { + let php_type = call_return_type(ctx, &function_name, &operands); + let data = ctx.intern_function_name(&function_name); + Some(ctx.emit_value( + Op::ExternCall, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ExternCall.default_effects(), + Some(expr.span), + )) + } + StaticCallableBinding::Builtin(function_name) => { + let php_type = call_return_type(ctx, &function_name, &operands); + Some(emit_builtin_call_value( + ctx, + &function_name, + operands, + php_type, + expr.span, + None, + )) + } + StaticCallableBinding::Closure { + name, + signature, + captures, + } => { + let mut operands = operands; + append_closure_capture_operands(&mut operands, &captures); + let php_type = normalize_value_php_type(signature.return_type.codegen_repr()); + let data = ctx.intern_function_name(&name); + Some(ctx.emit_value( + Op::Call, + operands, + Some(Immediate::Data(data)), + php_type, + effects_lookup::user_call_effects(&name), + Some(expr.span), + )) + } + StaticCallableBinding::StaticMethod { receiver, method } => { + let sig = static_method_implementation_signature(ctx, &receiver, &method); + let result_type = sig + .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) + .unwrap_or_else(|| fallback_expr_type(expr)); + let name = format!("{}::{}", receiver_name(&receiver), method); + let data = ctx.intern_string(&name); + Some(ctx.emit_value( + Op::StaticMethodCall, + operands, + Some(Immediate::Data(data)), + result_type, + Op::StaticMethodCall.default_effects(), + Some(expr.span), + )) + } + StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { + lower_static_method_descriptor_value_call(ctx, &receiver, &method, operands, expr) + } + StaticCallableBinding::InstanceMethod { .. } => None, } - let var_arg = planned_regular_arg_expr(&plan.regular_args[0])?.clone(); - let type_arg = planned_regular_arg_expr(&plan.regular_args[1])?.clone(); - Some((var_arg, type_arg)) } -/// Returns the source expression assigned to a planned regular parameter. -fn planned_regular_arg_expr( - arg: &crate::types::call_args::PlannedRegularArg, -) -> Option<&Expr> { - match arg { - crate::types::call_args::PlannedRegularArg::Source { expr, .. } => Some(expr), - crate::types::call_args::PlannedRegularArg::Default(_) - | crate::types::call_args::PlannedRegularArg::SpreadElement { .. } => None, +/// Resolves a compile-time `call_user_func*` callback expression. +fn static_call_user_func_callback( + ctx: &LoweringContext<'_, '_>, + callback: &Expr, +) -> Option { + match &callback.kind { + ExprKind::StringLiteral(name) => resolve_static_string_callable(ctx, name), + ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { + resolve_static_string_callable(ctx, name.as_str()) + } + ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { + resolve_static_method_callable(ctx, receiver.clone(), method.clone()) + } + ExprKind::Variable(name) => ctx + .static_callable_local(name) + .and_then(direct_static_callable_binding), + ExprKind::ArrayLiteral(items) => static_array_callable_descriptor_target(ctx, items) + .or_else(|| instance_array_callable_target(ctx, items)), + _ => None, } } -/// Returns the PHP type named by a literal `settype()` second argument. -fn static_settype_target_type(arg: &Expr) -> Option { - let ExprKind::StringLiteral(name) = &arg.kind else { - return None; - }; - match php_symbol_key(name).as_str() { - "int" | "integer" => Some(PhpType::Int), - "float" | "double" => Some(PhpType::Float), - "string" => Some(PhpType::Str), - "bool" | "boolean" => Some(PhpType::Bool), +/// Resolves `call_user_func*` callbacks that must keep descriptor receiver state. +fn instance_call_user_func_callback( + ctx: &LoweringContext<'_, '_>, + callback: &Expr, +) -> Option { + let target = match &callback.kind { + ExprKind::FirstClassCallable(CallableTarget::Method { .. }) => { + static_callable_binding_for_expr(ctx, callback) + } + ExprKind::Variable(name) => ctx.static_callable_local(name), _ => None, + }?; + if matches!(target, StaticCallableBinding::InstanceMethod { .. }) { + Some(target) + } else { + None } } -/// Lowers static function callbacks for `preg_replace_callback()`. -fn lower_preg_replace_callback_args( - ctx: &mut LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - args: &[Expr], -) -> Vec { - if args.len() != 3 { - return lower_args_with_signature(ctx, sig, args); - } - if matches!(&args[1].kind, ExprKind::Closure { .. }) { - let pattern = lower_expr(ctx, &args[0]); - let callback = lower_preg_replace_callback_closure(ctx, &args[1]) - .expect("preg_replace_callback closure check must match lowering"); - let subject = lower_expr(ctx, &args[2]); - let subject = persist_call_arg_if_string(ctx, subject, args[2].span); - return vec![pattern.value, callback.value, subject.value]; +/// Returns signature metadata for receiver-bound callables that still need descriptor state. +fn instance_callable_signature(target: &StaticCallableBinding) -> Option<&FunctionSig> { + match target { + StaticCallableBinding::InstanceMethod { signature, .. } => Some(signature), + _ => None, } - let Some(callback) = preg_replace_static_callback(ctx, &args[1]) else { - return lower_args_with_signature(ctx, sig, args); - }; - let pattern = lower_expr(ctx, &args[0]); - let callback = lower_string_literal(ctx, &callback, &args[1]); - let subject = lower_expr(ctx, &args[2]); - let subject = persist_call_arg_if_string(ctx, subject, args[2].span); - vec![pattern.value, callback.value, subject.value] -} - -/// Lowers a `preg_replace_callback()` closure with match-array parameter context. -fn lower_preg_replace_callback_closure( - ctx: &mut LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - let ExprKind::Closure { - params, - variadic, - return_type, - body, - captures, - capture_refs, - is_static, - .. - } = &callback.kind - else { - return None; - }; - Some(lower_closure_with_context( - ctx, - params, - variadic.as_deref(), - return_type.as_ref(), - body, - captures, - capture_refs, - callback, - &[PhpType::Array(Box::new(PhpType::Str))], - None, - *is_static, - )) } -/// Returns the userland callback name accepted by the current regex runtime helper. -fn preg_replace_static_callback( +/// Resolves a literal first-class callable expression to a static local binding. +pub(crate) fn static_callable_binding_for_expr( ctx: &LoweringContext<'_, '_>, - callback: &Expr, -) -> Option { - match &callback.kind { + expr: &Expr, +) -> Option { + match &expr.kind { + ExprKind::StringLiteral(name) => resolve_static_string_callable(ctx, name), ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { - Some(name.as_str().to_string()) + resolve_static_string_callable(ctx, name.as_str()) } - ExprKind::Variable(name) => match ctx.static_callable_local(name)? { - StaticCallableBinding::UserFunction(function_name) => Some(function_name), - _ => None, - }, + ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { + resolve_static_method_callable(ctx, receiver.clone(), method.clone()) + } + ExprKind::ArrayLiteral(items) => static_array_callable_descriptor_target(ctx, items) + .or_else(|| instance_array_callable_target(ctx, items)), + ExprKind::FirstClassCallable(CallableTarget::Method { object, method }) => { + resolve_instance_method_callable(ctx, object, method.clone(), false) + } + ExprKind::Variable(name) => ctx.static_callable_local(name), _ => None, } } -/// Lowers simple positional `date` operands while stabilizing the format string before timestamp evaluation. -fn lower_date_args( - ctx: &mut LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - args: &[Expr], -) -> Vec { - if args.len() != 2 - || crate::types::call_args::has_named_args(args) - || args.iter().any(is_spread_arg) - { - return lower_args_with_signature(ctx, sig, args); - } - let format = lower_expr(ctx, &args[0]); - let format = persist_call_arg_if_string(ctx, format, args[0].span); - vec![format.value, lower_expr(ctx, &args[1]).value] +/// Returns the reflected class captured by a statically-known `ReflectionClass` expression. +pub(crate) fn reflection_class_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option { + reflection_class_new_instance_reflected_class(ctx, expr) } -/// Lowers simple positional `json_decode` operands while stabilizing string sources early. -fn lower_json_decode_args( - ctx: &mut LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - args: &[Expr], -) -> Vec { - if args.is_empty() - || crate::types::call_args::has_named_args(args) - || args.iter().any(is_spread_arg) - { - return lower_args_with_signature(ctx, sig, args); - } - let source = lower_expr(ctx, &args[0]); - let source = persist_call_arg_if_string(ctx, source, args[0].span); - let mut operands = Vec::with_capacity(args.len()); - operands.push(source.value); - for arg in &args[1..] { - operands.push(lower_expr(ctx, arg).value); - } - operands +/// Returns the reflected function captured by a statically-known `ReflectionFunction` expression. +pub(crate) fn reflection_function_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option { + reflection_function_reflected_target(ctx, expr) } -/// Emits `StrPersist` for already-string call operands before later arguments can reuse string scratch storage. -fn persist_call_arg_if_string( - ctx: &mut LoweringContext<'_, '_>, - source: LoweredValue, - span: crate::span::Span, -) -> LoweredValue { - if source.ir_type != IrType::Str { - return source; - } - ctx.emit_value( - Op::StrPersist, - vec![source.value], - None, - PhpType::Str, - Op::StrPersist.default_effects(), - Some(span), - ) +/// Returns the reflected property captured by a statically-known `ReflectionProperty` expression. +pub(crate) fn reflection_property_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option<(String, String)> { + reflection_property_reflected_target(ctx, expr) } -/// Lowers positional/named/spread call arguments in source order. -fn lower_args(ctx: &mut LoweringContext<'_, '_>, args: &[Expr]) -> Vec { - args.iter().map(|arg| lower_expr(ctx, arg).value).collect() +/// Returns the reflected method captured by a statically-known `ReflectionMethod` expression. +pub(crate) fn reflection_method_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option<(String, String)> { + reflection_method_reflected_target(ctx, expr) } -/// Lowers one argument while applying by-reference storage normalization from a signature. -fn lower_arg_with_signature( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - index: usize, - arg: &Expr, -) -> crate::ir::ValueId { - if let Some(value) = lower_by_ref_array_element_arg_with_signature(ctx, sig, index, arg) { - return value; - } - if let Some(value) = lower_by_ref_array_arg_with_signature(ctx, sig, index, arg) { - return value; +/// Returns a safe static argument array that can be replayed for reflection forwarding. +pub(crate) fn reflection_arg_array_binding_for_expr(expr: &Expr) -> Option> { + let args = reflection_class_new_instance_args_value_without_locals(expr)?; + if args.iter().all(reflection_arg_expr_can_track) { + Some(args) + } else { + None } - let lowered = lower_expr(ctx, arg); - coerce_scalar_arg_to_param_storage(ctx, sig, index, lowered, arg).value } -/// Coerces a positional argument's storage to match a declared scalar parameter type. -/// -/// EIR passes each call argument in its natural storage. A declared `float` parameter is -/// materialized into the callee's floating-point register/slot, so an integer argument must be -/// converted with `IToF` first: without it the raw 64-bit integer bit-pattern lands in the -/// float slot and the callee reads garbage (and, when other float arguments are present, the -/// unconverted slot is overwritten by a neighbouring float argument). Only the int→float case -/// is adjusted; every other argument/parameter storage combination is passed through unchanged. -fn coerce_scalar_arg_to_param_storage( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - index: usize, - value: LoweredValue, - arg: &Expr, -) -> LoweredValue { - let Some((_, param_ty)) = sig.params.get(index) else { - return value; - }; - if value.ir_type == IrType::I64 && param_ty.codegen_repr() == PhpType::Float { - return coerce_to_float(ctx, value, arg); +/// Returns true when replaying an argument expression cannot duplicate side effects. +fn reflection_arg_expr_can_track(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::ConstRef(_) + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => true, + ExprKind::Negate(inner) => matches!( + &inner.kind, + ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) + ), + ExprKind::NamedArg { value, .. } => reflection_arg_expr_can_track(value), + ExprKind::ArrayLiteral(items) => items.iter().all(reflection_arg_expr_can_track), + ExprKind::ArrayLiteralAssoc(entries) => entries.iter().all(|(key, value)| { + reflection_arg_array_key_can_track(key) && reflection_arg_expr_can_track(value) + }), + _ => false, } - value } -/// Widens positional call operands to their declared scalar parameter types. -/// -/// The C/native ABI places an argument in an integer or floating-point register based -/// on the *value's* type, while the callee reads each parameter from the register class -/// of the *parameter's* type. Without this step an `int` (or `bool`) argument passed to -/// a `float` parameter is deposited in an integer register and then read back as garbage -/// from a floating-point slot. Only pure `float` parameters receiving an integer/bool -/// operand are rewritten with an int→float conversion; by-reference parameters and the -/// variadic tail operand are left untouched. -fn coerce_operands_to_params( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - mut operands: Vec, -) -> Vec { - let regular_param_count = crate::types::call_args::regular_param_count(sig); - let limit = operands.len().min(regular_param_count); - for index in 0..limit { - if sig.ref_params.get(index).copied().unwrap_or(false) { - continue; - } - let Some((_, param_ty)) = sig.params.get(index) else { - continue; - }; - if param_ty.codegen_repr() != PhpType::Float { - continue; - } - let value = operands[index]; - let operand_ty = ctx.builder.value_php_type(value).codegen_repr(); - if !matches!(operand_ty, PhpType::Int | PhpType::Bool) { - continue; - } - let lowered = LoweredValue { value, ir_type: IrType::I64 }; - operands[index] = coerce_to_float_at_span(ctx, lowered, None).value; - } - operands +/// Returns true when an associative array key is stable enough for replay. +fn reflection_arg_array_key_can_track(expr: &Expr) -> bool { + matches!( + expr.kind, + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::FloatLiteral(_) + ) } -/// Widens local indexed-array storage before passing it to an `array` ref parameter. -fn lower_by_ref_array_arg_with_signature( +/// EIR value and callable binding produced by a callable-array assignment. +pub(crate) struct LoweredCallableArrayAssignment { + pub(crate) value: LoweredValue, + pub(crate) target: StaticCallableBinding, +} + +/// Lowers a callable-array assignment while preserving its PHP array value. +pub(crate) fn lower_callable_array_for_assignment( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - index: usize, - arg: &Expr, -) -> Option { - if !sig.ref_params.get(index).copied().unwrap_or(false) { - return None; - } - let (_, param_ty) = sig.params.get(index)?; - let ExprKind::Variable(name) = &arg.kind else { + value: &Expr, + target: Option<&StaticCallableBinding>, +) -> Option { + let ExprKind::ArrayLiteral(items) = &value.kind else { return None; }; - if !by_ref_array_arg_needs_mixed_storage(ctx, name, param_ty) { + let StaticCallableBinding::InstanceMethod { + object, + method, + signature, + .. + } = target? else { return None; - } - let array_ty = PhpType::Array(Box::new(PhpType::Mixed)); - let local = ctx.load_local(name, Some(arg.span)); - let converted = ctx.emit_value( - Op::ArrayToMixed, - vec![local.value], + }; + let receiver = lower_expr(ctx, object); + let receiver_ty = ctx.builder.value_php_type(receiver.value); + let hidden_name = ctx.declare_hidden_temp(receiver_ty.clone()); + let receiver = ctx.store_local(&hidden_name, receiver, receiver_ty, Some(object.span)); + let array = lower_callable_array_literal_with_receiver(ctx, items, value, receiver); + let hidden_object = Expr::new(ExprKind::Variable(hidden_name), object.span); + let target = StaticCallableBinding::InstanceMethod { + object: Box::new(hidden_object), + method: method.clone(), + signature: signature.clone(), + direct_call: true, + }; + Some(LoweredCallableArrayAssignment { value: array, target }) +} + +/// Lowers a callable-array literal after its receiver has already been captured. +fn lower_callable_array_literal_with_receiver( + ctx: &mut LoweringContext<'_, '_>, + items: &[Expr], + expr: &Expr, + receiver: LoweredValue, +) -> LoweredValue { + let array_ty = array_literal_type_for_ir(ctx, items, expr); + let elem_ty = indexed_array_literal_element_type(&array_ty); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty, + Op::ArrayNew.default_effects(), + Some(expr.span), + ); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, receiver.value], None, - array_ty.clone(), - Op::ArrayToMixed.default_effects(), - Some(arg.span), + Op::ArrayPush.default_effects(), + Some(expr.span), ); - ctx.store_mutated_local(name, converted, array_ty, Some(arg.span)); - Some(ctx.load_local(name, Some(arg.span)).value) + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), receiver, expr.span); + for item in items.iter().skip(1) { + let value = lower_expr(ctx, item); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); + } + array } -/// Lowers `$array[$index]` as a direct by-reference argument cell address. -fn lower_by_ref_array_element_arg_with_signature( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - index: usize, - arg: &Expr, -) -> Option { - if !sig.ref_params.get(index).copied().unwrap_or(false) { +/// Resolves a static callable array literal as a descriptor-backed static method. +fn static_array_callable_descriptor_target( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], +) -> Option { + static_array_callable_parts(ctx, items).map(|(receiver, method)| { + StaticCallableBinding::StaticMethodDescriptor { receiver, method } + }) +} + +/// Resolves a literal `[$object, "method"]` callable array as an instance method. +fn instance_array_callable_target( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], +) -> Option { + let [object, method_expr] = items else { return None; - } - let ExprKind::ArrayAccess { array, index: element_index } = &arg.kind else { + }; + let ExprKind::StringLiteral(method) = &method_expr.kind else { return None; }; - let ExprKind::Variable(array_name) = &array.kind else { + resolve_instance_method_callable(ctx, object, method.clone(), true) +} + +/// Resolves the named static receiver and method from a static callable array literal. +fn static_array_callable_parts( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], +) -> Option<(StaticReceiver, String)> { + let [class_expr, method_expr] = items else { return None; }; - let PhpType::Array(elem_ty) = ctx.local_type(array_name).codegen_repr() else { + let class_name = static_callable_class_name(ctx, class_expr)?; + let ExprKind::StringLiteral(method) = &method_expr.kind else { return None; }; - let (_, param_ty) = sig.params.get(index)?; - let element_ty = match normalize_value_php_type(*elem_ty) { - PhpType::Void => normalize_value_php_type(param_ty.codegen_repr()), - other => other, - }; - let array_value = ctx.load_local(array_name, Some(array.span)); - let element_index = lower_expr(ctx, element_index); - let element_index = coerce_to_int_at_span(ctx, element_index, Some(arg.span)); - let value = ctx - .builder - .emit_with_effects( - Op::ArrayElemAddr, - vec![array_value.value, element_index.value], - None, - IrType::I64, - element_ty, - Ownership::NonHeap, - Op::ArrayElemAddr.default_effects(), - Some(arg.span), - ) - .expect("array_elem_addr produces a value"); - Some(value) + let class_name = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\'))?; + let receiver = StaticReceiver::Named(Name::from(class_name)); + static_method_implementation_signature(ctx, &receiver, method)?; + Some((receiver, method.clone())) } -/// Returns true when a local array must be converted before a by-reference call. -fn by_ref_array_arg_needs_mixed_storage( +/// Extracts a compile-time class name for a static callable array. +fn static_callable_class_name( ctx: &LoweringContext<'_, '_>, - name: &str, - param_ty: &PhpType, -) -> bool { - let PhpType::Array(param_elem) = param_ty.codegen_repr() else { - return false; - }; - if param_elem.codegen_repr() != PhpType::Mixed { - return false; + class_expr: &Expr, +) -> Option { + match &class_expr.kind { + ExprKind::StringLiteral(name) => Some(name.clone()), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver), + _ => None, } - let PhpType::Array(local_elem) = ctx.local_type(name).codegen_repr() else { - return false; - }; - local_elem.codegen_repr() != PhpType::Mixed } -/// Lowers positional call arguments with omitted optional defaults and variadic tail packing. -fn lower_args_with_signature( - ctx: &mut LoweringContext<'_, '_>, - sig: Option<&FunctionSig>, - args: &[Expr], -) -> Vec { - let Some(sig) = sig else { - return lower_args(ctx, args); - }; - if crate::types::call_args::has_named_args(args) { - let operands = lower_named_args_with_signature(ctx, sig, args); - return coerce_operands_to_params(ctx, sig, operands); - } - if let Some(operands) = lower_positional_spread_args_with_signature(ctx, sig, args) { - return coerce_operands_to_params(ctx, sig, operands); - } - let static_spread_args = if has_static_call_spread_args(args) { - Some(expand_static_call_spread_args(args)) - } else { - None +/// Returns the static `is_callable()` result for a literal static-method callback array. +fn static_array_callable_is_callable( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], +) -> Option { + let [class_expr, method_expr] = items else { + return None; }; - let args = static_spread_args.as_deref().unwrap_or(args); - if let Some(operands) = lower_assoc_spread_only_args(ctx, sig, args) { - return coerce_operands_to_params(ctx, sig, operands); - } - if args.iter().any(is_spread_arg) { - return lower_args(ctx, args); - } - let regular_param_count = crate::types::call_args::regular_param_count(sig); - let fixed_arg_count = if sig.variadic.is_some() { - args.len().min(regular_param_count) - } else { - args.len() + let class_name = static_callable_class_name(ctx, class_expr)?; + let ExprKind::StringLiteral(method) = &method_expr.kind else { + return None; }; - if sig.variadic.is_none() && fixed_arg_count >= regular_param_count { - let operands = args - .iter() - .enumerate() - .map(|(index, arg)| lower_arg_with_signature(ctx, sig, index, arg)) - .collect(); - return coerce_operands_to_params(ctx, sig, operands); - } - let mut operands: Vec = args[..fixed_arg_count] - .iter() - .enumerate() - .map(|(index, arg)| lower_arg_with_signature(ctx, sig, index, arg)) - .collect(); - for idx in fixed_arg_count..regular_param_count { - let Some(Some(default)) = sig.defaults.get(idx) else { - break; - }; - operands.push(lower_expr(ctx, default).value); - } - if sig.variadic.is_some() { - let tail = if args.len() > regular_param_count { - &args[regular_param_count..] - } else { - &[] - }; - operands.push(lower_variadic_tail_array(ctx, sig, tail).value); - } - coerce_operands_to_params(ctx, sig, operands) + Some(static_method_callback_is_callable(ctx, &class_name, method)) } -/// Lowers one trailing indexed spread in a fixed-arity positional call. -fn lower_positional_spread_args_with_signature( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - args: &[Expr], -) -> Option> { - if sig.variadic.is_some() { - return None; - } - let spread_idx = single_trailing_indexed_spread_arg(ctx, args)?; - let regular_param_count = crate::types::call_args::regular_param_count(sig); - if spread_idx > regular_param_count { - return None; - } - let first_spread_param_idx = spread_idx; - let required_len = required_positional_spread_len(sig, first_spread_param_idx, regular_param_count); - let ExprKind::Spread(inner) = &args[spread_idx].kind else { - return None; +/// Returns true when a compile-time class/method pair names a public static method. +fn static_method_callback_is_callable( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, +) -> bool { + let Some(class_name) = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\')) else { + return false; }; - if static_indexed_spread_len(inner).is_some_and(|len| len >= required_len) { - return None; - } - - let mut operands = Vec::with_capacity(regular_param_count); - for (index, arg) in args[..spread_idx].iter().enumerate() { - operands.push(lower_arg_with_signature(ctx, sig, index, arg)); - } - - let spread_type = indexed_spread_source_type(ctx, inner)?; - let spread = lower_expr(ctx, inner); - let temp_name = ctx.declare_hidden_temp(spread_type.clone()); - store_value_into_temp(ctx, &temp_name, spread_type, spread, args[spread_idx].span); - let spread_expr = Expr::new(ExprKind::Variable(temp_name), inner.span); - let spread_value = lower_expr(ctx, &spread_expr); - emit_positional_spread_min_len_guard( - ctx, - spread_value.value, - required_len, - args[spread_idx].span, - ); - - for param_idx in first_spread_param_idx..regular_param_count { - let element_idx = param_idx - first_spread_param_idx; - let default = sig.defaults.get(param_idx).and_then(|default| default.as_ref()); - let expr = if let Some(default) = default { - if element_idx < required_len { - spread_element_expr_for_ir( - &spread_expr, - element_idx, - None, - false, - args[spread_idx].span, - ) - } else { - spread_element_or_default_expr_for_ir( - &spread_expr, - element_idx, - None, - false, - default.clone(), - args[spread_idx].span, - ) - } - } else { - spread_element_expr_for_ir( - &spread_expr, - element_idx, - None, - false, - args[spread_idx].span, - ) - }; - operands.push(lower_expr(ctx, &expr).value); + let Some(class_info) = ctx.classes.get(&class_name) else { + return false; + }; + let method_key = php_symbol_key(method); + if !class_info.static_methods.contains_key(&method_key) { + return false; } - - Some(operands) + class_info.static_method_visibilities.get(&method_key) == Some(&Visibility::Public) } -/// Returns the element count for a statically-known indexed spread source. -fn static_indexed_spread_len(expr: &Expr) -> Option { - match &expr.kind { - ExprKind::ArrayLiteral(items) => Some(items.len()), +/// Converts a static `call_user_func_array()` argument array into call arguments. +fn static_call_user_func_array_args(arg_array: &Expr) -> Option> { + match &arg_array.kind { + ExprKind::ArrayLiteral(items) => Some(items.clone()), + ExprKind::ArrayLiteralAssoc(pairs) => static_call_user_func_array_assoc_args(pairs), _ => None, } } -/// Returns the index of a single trailing positional spread that EIR can materialize. -fn single_trailing_indexed_spread_arg( - ctx: &LoweringContext<'_, '_>, - args: &[Expr], -) -> Option { - let spread_indices = args - .iter() - .enumerate() - .filter_map(|(idx, arg)| matches!(arg.kind, ExprKind::Spread(_)).then_some(idx)) - .collect::>(); - let [spread_idx] = spread_indices.as_slice() else { - return None; - }; - if *spread_idx + 1 != args.len() { - return None; +/// Converts literal associative callback arrays into positional or named call args. +fn static_call_user_func_array_assoc_args(pairs: &[(Expr, Expr)]) -> Option> { + let mut args = Vec::with_capacity(pairs.len()); + for (key, value) in pairs { + match &key.kind { + ExprKind::StringLiteral(name) => { + args.push(Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(value.clone()), + }, + value.span, + )); + } + ExprKind::IntLiteral(_) => args.push(value.clone()), + _ => return None, + } } - let ExprKind::Spread(inner) = &args[*spread_idx].kind else { - return None; - }; - indexed_spread_source_type(ctx, inner)?; - Some(*spread_idx) + Some(args) } -/// Returns the indexed-array source type for spread-only EIR lowering. -fn indexed_spread_source_type( - ctx: &LoweringContext<'_, '_>, +/// Lowers one resolved static callable target to the corresponding EIR call opcode. +fn lower_static_callable_call( + ctx: &mut LoweringContext<'_, '_>, + target: StaticCallableBinding, + callback_args: &[Expr], expr: &Expr, -) -> Option { - let ty = match &expr.kind { - ExprKind::Variable(name) => ctx.local_type(name), - ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, expr), - _ => infer_expr_type_syntactic(expr), +) -> Option { + match target { + StaticCallableBinding::UserFunction(function_name) => { + let sig = ctx.functions.get(&function_name).cloned(); + let operands = lower_args_with_signature(ctx, sig.as_ref(), callback_args); + let php_type = call_return_type(ctx, &function_name, &operands); + let data = ctx.intern_function_name(&function_name); + Some(ctx.emit_value( + Op::Call, + operands, + Some(Immediate::Data(data)), + php_type, + effects_lookup::user_call_effects(&function_name), + Some(expr.span), + )) + } + StaticCallableBinding::ExternFunction(function_name) => { + let sig = ctx + .extern_functions + .get(&function_name) + .map(function_sig_from_extern_for_descriptor); + let operands = lower_args_with_signature(ctx, sig.as_ref(), callback_args); + let php_type = call_return_type(ctx, &function_name, &operands); + let data = ctx.intern_function_name(&function_name); + Some(ctx.emit_value( + Op::ExternCall, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ExternCall.default_effects(), + Some(expr.span), + )) + } + StaticCallableBinding::Builtin(function_name) => { + let sig = call_signature(ctx, &function_name, callback_args); + let operands = lower_builtin_call_args(ctx, &function_name, sig.as_ref(), callback_args); + let php_type = call_return_type(ctx, &function_name, &operands); + Some(emit_builtin_call_value( + ctx, + &function_name, + operands, + php_type, + expr.span, + None, + )) + } + StaticCallableBinding::Closure { + name, + signature, + captures, + } => { + let mut operands = lower_args_with_signature(ctx, Some(&signature), callback_args); + append_closure_capture_operands(&mut operands, &captures); + let php_type = normalize_value_php_type(signature.return_type.codegen_repr()); + let data = ctx.intern_function_name(&name); + Some(ctx.emit_value( + Op::Call, + operands, + Some(Immediate::Data(data)), + php_type, + effects_lookup::user_call_effects(&name), + Some(expr.span), + )) + } + StaticCallableBinding::StaticMethod { receiver, method } => { + Some(lower_static_method_call(ctx, &receiver, &method, callback_args, expr)) + } + StaticCallableBinding::StaticMethodDescriptor { receiver, method } => { + Some(lower_static_method_descriptor_call( + ctx, + &receiver, + &method, + callback_args, + expr, + )) + } + StaticCallableBinding::InstanceMethod { + object, + method, + direct_call: true, + .. + } => Some(lower_method_call(ctx, &object, &method, callback_args, Op::MethodCall, expr)), + StaticCallableBinding::InstanceMethod { .. } => None, } - .codegen_repr(); - if matches!(ty, PhpType::Array(_)) { - Some(ty) - } else { - None +} + +/// Resolves a PHP string callback using case-insensitive function lookup rules. +fn resolve_static_string_callable( + ctx: &LoweringContext<'_, '_>, + callback: &str, +) -> Option { + let callback = callback.trim_start_matches('\\'); + if let Some((class_name, method)) = callback.rsplit_once("::") { + let class_name = lookup_folded_name(ctx.classes.keys(), class_name.trim_start_matches('\\'))?; + return resolve_static_method_callable( + ctx, + StaticReceiver::Named(Name::from(class_name)), + method.to_string(), + ); + } + if let Some(function_name) = lookup_folded_name(ctx.extern_functions.keys(), callback) { + return Some(StaticCallableBinding::ExternFunction(function_name)); + } + if let Some(function_name) = canonical_builtin_function_name(callback) { + return Some(StaticCallableBinding::Builtin(function_name)); } + if let Some(function_name) = lookup_folded_name(ctx.functions.keys(), callback) { + return Some(StaticCallableBinding::UserFunction(function_name)); + } + None } -/// Returns how many spread elements must exist to satisfy required parameters. -fn required_positional_spread_len( - sig: &FunctionSig, - start_param_idx: usize, - regular_param_count: usize, -) -> usize { - (start_param_idx..regular_param_count) - .rfind(|idx| sig.defaults.get(*idx).and_then(|default| default.as_ref()).is_none()) - .map(|idx| idx - start_param_idx + 1) - .unwrap_or(0) +/// Appends captured closure values after caller-visible operands for hidden ABI params. +fn append_closure_capture_operands(operands: &mut Vec, captures: &[ClosureCapture]) { + operands.extend(captures.iter().map(|capture| capture.value)); } -/// Emits a fatal guard when a positional spread is shorter than required parameters. -fn emit_positional_spread_min_len_guard( - ctx: &mut LoweringContext<'_, '_>, - spread: crate::ir::ValueId, - min_len: usize, - span: crate::span::Span, -) { - if min_len == 0 { - return; - } - let len = ctx.emit_value( - Op::ArrayLen, - vec![spread], - None, - PhpType::Int, - Op::ArrayLen.default_effects(), - Some(span), - ); - let min = emit_i64_at_span(ctx, min_len as i64, span); - let has_required_args = ctx.emit_value( - Op::ICmp, - vec![len.value, min.value], - Some(Immediate::CmpPredicate(CmpPredicate::Sge)), - PhpType::Bool, - Op::ICmp.default_effects(), - Some(span), - ); - let ok = ctx.builder.create_named_block("call.spread.len.ok", Vec::new()); - let fatal = ctx.builder.create_named_block("call.spread.len.fatal", Vec::new()); - ctx.builder.terminate(Terminator::CondBr { - cond: has_required_args.value, - then_target: ok, - then_args: Vec::new(), - else_target: fatal, - else_args: Vec::new(), - }); +/// Resolves a static method callback when class and method are compile-time known. +fn resolve_static_method_callable( + ctx: &LoweringContext<'_, '_>, + receiver: StaticReceiver, + method: String, +) -> Option { + static_method_implementation_signature(ctx, &receiver, &method)?; + Some(StaticCallableBinding::StaticMethod { receiver, method }) +} - ctx.builder.position_at_end(fatal); - let message = ctx.intern_string("Fatal error: too few arguments for spread call\n"); - ctx.builder.terminate(Terminator::Fatal { message }); +/// Resolves a first-class instance-method callable to signature metadata only. +fn resolve_instance_method_callable( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + method: String, + direct_call: bool, +) -> Option { + let class_name = instance_callable_object_class(ctx, object)?; + let method_key = php_symbol_key(&method); + let signature = class_method_signature(ctx, &class_name, &method_key)?.clone(); + Some(StaticCallableBinding::InstanceMethod { + object: Box::new(object.clone()), + method, + signature, + direct_call, + }) +} - ctx.builder.position_at_end(ok); +/// Returns a static callable only when it can be lowered without descriptor state. +fn direct_static_callable_binding(target: StaticCallableBinding) -> Option { + if matches!(target, StaticCallableBinding::InstanceMethod { .. }) { + None + } else { + Some(target) + } } -/// Lowers named arguments in source order, then returns operands in signature order. -fn lower_named_args_with_signature( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - args: &[Expr], -) -> Vec { - let call_span = args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let assoc_spread_sources = assoc_spread_sources(ctx, args); - let regular_param_count = crate::types::call_args::regular_param_count(sig); - let Ok(plan) = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - sig, - args, - call_span, - regular_param_count, - false, - true, - &assoc_spread_sources, - ) else { - return lower_args(ctx, args); +/// Resolves the concrete class for an object expression used in an instance FCC. +/// Returns the property type referenced by a `Closure::bind(fn () => $this->prop, $newThis, …)` +/// when the closure body is exactly `return $this->prop`: the bound object's property type. +/// The closure's own `$this` is Mixed (it is bound dynamically), so the type comes from the +/// bind's receiver argument. +fn closure_bind_property_return_type( + ctx: &LoweringContext<'_, '_>, + callee: &Expr, +) -> Option { + let ExprKind::StaticMethodCall { receiver, method, args } = &callee.kind else { + return None; }; - if plan.has_spread_args() { - if let Some(operands) = lower_named_args_with_spread_plan(ctx, sig, &plan, &assoc_spread_sources) { - return operands; - } - if let Some(operands) = lower_dynamic_named_spread_variadic_args(ctx, sig, &plan) { - return operands; - } - let normalized = plan.normalized_args(); - return lower_args(ctx, &normalized); + if !method.eq_ignore_ascii_case("bind") { + return None; } - let mut source_values = Vec::with_capacity(plan.source_args.len()); - for source_arg in &plan.source_args { - source_values.push(lower_call_source_arg(ctx, source_arg)); - } - - let mut operands = Vec::with_capacity(plan.regular_args.len() + usize::from(sig.variadic.is_some())); - for arg in &plan.regular_args { - match arg { - crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { - operands.push(source_values[*source_index]); - } - crate::types::call_args::PlannedRegularArg::Default(default) => { - operands.push(lower_expr(ctx, default).value); - } - crate::types::call_args::PlannedRegularArg::SpreadElement { .. } => { - return lower_args(ctx, args); - } - } + let crate::parser::ast::StaticReceiver::Named(name) = receiver else { + return None; + }; + if !name + .as_str() + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return None; } - if sig.variadic.is_some() { - operands.push(lower_named_variadic_tail_array(ctx, sig, &plan.source_values, &source_values).value); + let ExprKind::Closure { body, .. } = &args.first()?.kind else { + return None; + }; + let new_this_class = instance_callable_object_class(ctx, args.get(1)?)?; + let [stmt] = body.as_slice() else { + return None; + }; + let StmtKind::Return(Some(ret)) = &stmt.kind else { + return None; + }; + let ExprKind::PropertyAccess { object, property } = &ret.kind else { + return None; + }; + if !matches!(object.kind, ExprKind::This) { + return None; } - operands + let info = ctx.classes.get(new_this_class.trim_start_matches('\\'))?; + info.properties + .iter() + .find(|(name, _)| name == property) + .map(|(_, ty)| ty.clone()) } -/// Lowers dynamic associative prefix spreads for variadic calls far enough to preserve duplicate fatals. -fn lower_dynamic_named_spread_variadic_args( +/// Lowers `Closure::bind(fn &() => $this->prop, $newThis, scope)()` as a direct call to the +/// closure with `$newThis` boxed as its `$this` capture. +/// +/// `Closure::bind` rebinds the closure's receiver; invoking the result through the generic +/// runtime descriptor invoker boxes the closure's return value as Mixed, which cannot carry a +/// by-reference property cell pointer. Calling the closure directly (as `$f()` does) passes the +/// cell pointer through. The call result is typed from the bound receiver's property so a +/// by-reference array return binds correctly. Only the auto-captured `$this` shape (the +/// `fn &() => $this->prop` form) is handled; other captures fall back to the generic path. +fn lower_bound_closure_immediate_call( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - plan: &crate::types::call_args::CallArgPlan, -) -> Option> { - if sig.variadic.is_none() || !plan.prefix_has_dynamic_named_spread { + callee: &Expr, + args: &[Expr], + expr: &Expr, +) -> Option { + let (bound, _closure_value) = build_bound_closure_binding(ctx, callee, expr)?; + lower_static_callable_call(ctx, bound, args, expr) +} + +/// Builds the static-callable binding for `Closure::bind(fn &() => $this->prop, $newThis, scope)`. +/// +/// Lowers the closure literal (once), boxes `$newThis` as the closure's `$this` capture, and +/// overrides the binding's return type with the bound receiver's property type so a +/// by-reference return binds correctly. Returns the binding together with the lowered closure +/// descriptor value (the still-unbound `closure_new`), which callers may store in the assigned +/// variable. `None` unless the call is the single auto-captured `$this` shape — the only form +/// whose `$this` is fully known at compile time. Shared by the immediate-invoke path +/// (`Closure::bind(...)()`) and the variable-assignment path (`$b = Closure::bind(...)`). +fn build_bound_closure_binding( + ctx: &mut LoweringContext<'_, '_>, + callee: &Expr, + expr: &Expr, +) -> Option<(StaticCallableBinding, LoweredValue)> { + let result_type = closure_bind_property_return_type(ctx, callee)?; + let ExprKind::StaticMethodCall { args: bind_args, .. } = &callee.kind else { + return None; + }; + let closure_lit = bind_args.first()?; + if !matches!(closure_lit.kind, ExprKind::Closure { .. }) { return None; } - let call_span = plan - .source_args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let first_named_pos = plan.first_named_pos?; - let prefix_expr = plan.positional_prefix_expr(call_span)?; - let prefix = lower_expr(ctx, &prefix_expr); - if !matches!(ctx.builder.value_php_type(prefix.value).codegen_repr(), PhpType::AssocArray { .. }) { + let new_this = bind_args.get(1)?.clone(); + // Lower the closure literal to obtain its static binding (function name + captures). + let closure_value = lower_expr(ctx, closure_lit); + let Some(StaticCallableBinding::Closure { + name, + mut signature, + captures, + }) = ctx.take_pending_static_callable_result() + else { + return None; + }; + // Only the single auto-captured `$this` shape is supported here. + if captures.len() != 1 { return None; } - let prefix_type = ctx.builder.value_php_type(prefix.value); - let prefix_temp_name = ctx.declare_hidden_temp(prefix_type.clone()); - store_value_into_temp(ctx, &prefix_temp_name, prefix_type, prefix, prefix_expr.span); - let prefix_temp = Expr::new(ExprKind::Variable(prefix_temp_name), prefix_expr.span); + let new_this_value = lower_expr(ctx, &new_this); + let boxed_this = ctx.emit_value( + Op::MixedBox, + vec![new_this_value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(expr.span), + ); + signature.return_type = result_type; + let bound = StaticCallableBinding::Closure { + name, + signature, + captures: vec![ClosureCapture { + value: boxed_this.value, + }], + }; + Some((bound, closure_value)) +} - let mut source_values = vec![None; plan.source_args.len()]; - for (source_index, source_arg) in plan.source_args.iter().enumerate().skip(first_named_pos) { - if matches!(source_arg.kind, ExprKind::Spread(_)) { - return None; +/// Returns true when an assignment value is a by-reference `Closure::bind` of the auto-`$this` +/// shape, so the assignment should track the result as a static callable (routing a later +/// `$b()` through the direct-call path that carries the by-reference cell pointer). +/// +/// Read-only structural check (no IR emitted) used to set `direct_closure` before lowering. +pub(crate) fn is_bound_closure_assignment_shape(ctx: &LoweringContext<'_, '_>, value: &Expr) -> bool { + let ExprKind::StaticMethodCall { args, .. } = &value.kind else { + return false; + }; + let Some(ExprKind::Closure { by_ref_return, .. }) = args.first().map(|arg| &arg.kind) else { + return false; + }; + *by_ref_return && closure_bind_property_return_type(ctx, value).is_some() +} + +/// Lowers `$b = Closure::bind(fn &() => $this->prop, $newThis, scope)` for assignment: builds +/// the bound-closure binding, publishes it as the pending static callable so the assignment +/// registers `$b` for later direct `$b()` calls, and returns the closure descriptor to store +/// in `$b`. `None` for any non-matching shape so normal assignment lowering applies. +pub(crate) fn lower_bound_closure_for_assignment( + ctx: &mut LoweringContext<'_, '_>, + value: &Expr, +) -> Option { + let (bound, closure_value) = build_bound_closure_binding(ctx, value, value)?; + ctx.set_pending_static_callable_result(bound); + Some(closure_value) +} + +/// Resolves the statically-known class name of an object expression used as the +/// receiver of an instance first-class callable (`$obj->m(...)`). +/// +/// Returns the normalized class name for `$var` (from `local_types`), `$this` +/// (the current class), and `new` expressions; `None` when the receiver class +/// cannot be determined statically. +fn instance_callable_object_class( + ctx: &LoweringContext<'_, '_>, + object: &Expr, +) -> Option { + match &object.kind { + ExprKind::Variable(name) => ctx + .local_types + .get(name) + .and_then(class_name_from_php_type), + ExprKind::This => ctx.current_class.as_deref().and_then(normalized_class_name), + ExprKind::NewObject { class_name, .. } => normalized_class_name(class_name.as_str()), + ExprKind::NewDynamicObject { fallback_class, .. } => { + normalized_class_name(fallback_class.as_str()) } - source_values[source_index] = Some(lower_call_source_arg(ctx, source_arg)); + ExprKind::FunctionCall { name, .. } => ctx + .functions + .get(name.as_str()) + .and_then(|sig| class_name_from_php_type(&sig.return_type)), + _ => class_name_from_php_type(&infer_expr_type_syntactic(object)), } - emit_dynamic_named_prefix_duplicate_guards(ctx, sig, plan, &prefix_temp, first_named_pos); +} - let mut operands = Vec::with_capacity(plan.regular_args.len() + 1); - for arg in &plan.regular_args { - match arg { - crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { - operands.push(source_values.get(*source_index).copied().flatten()?); - } - crate::types::call_args::PlannedRegularArg::Default(default) => { - operands.push(lower_expr(ctx, default).value); - } - crate::types::call_args::PlannedRegularArg::SpreadElement { - prefix_element_idx, - param_name, - prefer_named_key, - default, - guaranteed_present, - spread_span, - .. - } => { - let expr = if let Some(default) = default { - if *guaranteed_present { - spread_element_expr_for_ir( - &prefix_temp, - *prefix_element_idx, - param_name.as_deref(), - *prefer_named_key, - *spread_span, - ) - } else { - spread_element_or_default_expr_for_ir( - &prefix_temp, - *prefix_element_idx, - param_name.as_deref(), - *prefer_named_key, - default.clone(), - *spread_span, - ) - } - } else { - spread_element_expr_for_ir( - &prefix_temp, - *prefix_element_idx, - param_name.as_deref(), - *prefer_named_key, - *spread_span, - ) - }; - operands.push(lower_expr(ctx, &expr).value); - } - } +/// Returns a non-empty normalized class name for an object PHP type. +fn class_name_from_php_type(ty: &PhpType) -> Option { + match ty.codegen_repr() { + PhpType::Object(class_name) => normalized_class_name(&class_name), + _ => None, } - operands.push(lower_variadic_tail_array(ctx, sig, &[]).value); - Some(operands) } -/// Emits duplicate checks for numeric prefix keys overwritten by later named parameters. -fn emit_dynamic_named_prefix_duplicate_guards( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - plan: &crate::types::call_args::CallArgPlan, - prefix_temp: &Expr, - first_named_pos: usize, -) { - for source in &plan.source_values { - if source.source_index() < first_named_pos { - continue; - } - let Some(param_idx) = source.param_idx() else { - continue; - }; - let Some((param_name, _)) = sig.params.get(param_idx) else { - continue; - }; - emit_dynamic_named_prefix_duplicate_guard( - ctx, - prefix_temp, - param_idx, - param_name, - source.expr().span, - ); +/// Trims PHP's optional leading namespace separator from class metadata names. +fn normalized_class_name(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if class_name.is_empty() { + None + } else { + Some(class_name.to_string()) } } -/// Emits one duplicate guard for a numeric key in a dynamic associative prefix. -fn emit_dynamic_named_prefix_duplicate_guard( - ctx: &mut LoweringContext<'_, '_>, - prefix_temp: &Expr, - param_idx: usize, - param_name: &str, - span: crate::span::Span, -) { - let exists_expr = Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified("array_key_exists"), - args: vec![ - Expr::new(ExprKind::IntLiteral(param_idx as i64), span), - prefix_temp.clone(), - ], - }, - span, - ); - let exists = lower_expr(ctx, &exists_expr); - let ok = ctx.builder.create_named_block("call.dynamic_named_prefix.ok", Vec::new()); - let fatal = ctx.builder.create_named_block("call.dynamic_named_prefix.fatal", Vec::new()); - ctx.builder.terminate(Terminator::CondBr { - cond: exists.value, - then_target: fatal, - then_args: Vec::new(), - else_target: ok, - else_args: Vec::new(), - }); +/// Looks up a PHP function name case-insensitively and returns the canonical candidate. +fn lookup_folded_name<'a, I>(names: I, requested: &str) -> Option +where + I: IntoIterator, +{ + let requested = php_symbol_key(requested); + names + .into_iter() + .find(|candidate| php_symbol_key(candidate) == requested) + .cloned() +} - ctx.builder.position_at_end(fatal); - let message = format!( - "Fatal error: Named parameter ${} overwrites previous argument\n", - param_name - ); - let message = ctx.intern_string(&message); - ctx.builder.terminate(Terminator::Fatal { message }); +/// Returns the caller-visible signature used to normalize direct call operands. +fn call_signature( + ctx: &LoweringContext<'_, '_>, + name: &str, + args: &[Expr], +) -> Option { + if let Some(sig) = ctx.functions.get(name) { + return Some(sig.clone()); + } + if let Some(sig) = ctx.extern_functions.get(name) { + return Some(function_sig_from_extern_for_descriptor(sig)); + } + if crate::types::call_args::has_named_args(args) { + return builtin_call_signature(name); + } + None +} - ctx.builder.position_at_end(ok); +/// Looks up a PHP builtin call signature using the normalized global builtin name. +fn builtin_call_signature(name: &str) -> Option { + crate::types::builtin_call_sig(&php_symbol_key(name.trim_start_matches('\\'))) } -/// Lowers named/spread argument plans without re-evaluating dynamic spread expressions. -fn lower_named_args_with_spread_plan( +/// Looks up precise first-class builtin metadata using the normalized global builtin name. +fn first_class_builtin_signature(name: &str) -> Option { + crate::types::first_class_callable_builtin_sig(&php_symbol_key(name.trim_start_matches('\\'))) +} + +/// Lowers supported `unset(...)` targets without evaluating them as ordinary call args. +fn lower_unset_locals( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - plan: &crate::types::call_args::CallArgPlan, - assoc_spread_sources: &[bool], -) -> Option> { - if assoc_spread_sources.iter().any(|is_assoc| *is_assoc) { - return None; - } - let call_span = plan - .source_args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let first_named_pos = plan.first_named_pos?; - let prefix_expr = plan.positional_prefix_expr(call_span)?; - let static_variadic_prefix_len = static_indexed_variadic_prefix_len(&prefix_expr); - if sig.variadic.is_some() && static_variadic_prefix_len.is_none() { + args: &[Expr], + expr: &Expr, +) -> Option { + if !args.iter().all(|arg| unset_target_supported(ctx, arg)) { return None; } - let prefix = lower_expr(ctx, &prefix_expr); - let prefix_type = ctx.builder.value_php_type(prefix.value); - let prefix_temp_name = ctx.declare_hidden_temp(prefix_type.clone()); - store_value_into_temp(ctx, &prefix_temp_name, prefix_type, prefix, prefix_expr.span); - let single_prefix_spread = !matches!(prefix_expr.kind, ExprKind::ArrayLiteral(_)); - let prefix_temp = Expr::new(ExprKind::Variable(prefix_temp_name), prefix_expr.span); - - let mut source_values = vec![None; plan.source_args.len()]; - for (source_index, source_arg) in plan.source_args.iter().enumerate().skip(first_named_pos) { - if matches!(source_arg.kind, ExprKind::Spread(_)) { - return None; - } - source_values[source_index] = Some(lower_call_source_arg(ctx, source_arg)); - } - if single_prefix_spread { - if let [check] = plan.spread_bounds_checks.as_slice() { - let prefix_value = lower_expr(ctx, &prefix_temp); - emit_named_spread_bounds_guard(ctx, prefix_value.value, check, call_span); - } - } - - let mut operands = Vec::with_capacity(plan.regular_args.len()); - for (param_idx, arg) in plan.regular_args.iter().enumerate() { - match arg { - crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { - if *source_index < first_named_pos { - let expr = spread_element_expr_for_ir( - &prefix_temp, - param_idx, - None, - false, - plan.source_args.get(*source_index).map(|arg| arg.span).unwrap_or(call_span), - ); - operands.push(lower_expr(ctx, &expr).value); - } else { - operands.push(source_values.get(*source_index).copied().flatten()?); - } + let null = lower_null(ctx, expr); + for arg in args { + match &arg.kind { + ExprKind::Variable(name) => { + ctx.unset_local(name, null, Some(arg.span)); } - crate::types::call_args::PlannedRegularArg::Default(default) => { - operands.push(lower_expr(ctx, default).value); + ExprKind::ArrayAccess { array, index } => { + lower_unset_array_access(ctx, array, index, arg); } - crate::types::call_args::PlannedRegularArg::SpreadElement { - element_idx: _, - prefix_element_idx, - param_name, - prefer_named_key, - default, - guaranteed_present, - spread_span, - .. - } => { - let element_idx = *prefix_element_idx; - let expr = if let Some(default) = default { - if *guaranteed_present { - spread_element_expr_for_ir( - &prefix_temp, - element_idx, - param_name.as_deref(), - *prefer_named_key, - *spread_span, - ) - } else { - spread_element_or_default_expr_for_ir( - &prefix_temp, - element_idx, - param_name.as_deref(), - *prefer_named_key, - default.clone(), - *spread_span, - ) - } - } else { - spread_element_expr_for_ir( - &prefix_temp, - element_idx, - param_name.as_deref(), - *prefer_named_key, - *spread_span, - ) - }; - operands.push(lower_expr(ctx, &expr).value); + ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } => { + lower_unset_property_access(ctx, object, property, arg); } + _ => {} } } - if sig.variadic.is_some() { - let regular_param_count = crate::types::call_args::regular_param_count(sig); - let tail = lower_named_spread_static_variadic_tail_hash( - ctx, - sig, - &prefix_temp, - static_variadic_prefix_len.unwrap_or(regular_param_count), - regular_param_count, - plan, - &source_values, - first_named_pos, - call_span, - ); - operands.push(tail.value); - } - Some(operands) + crate::ir_lower::ownership::collect_cycles(ctx, Some(expr.span)); + Some(null) } -/// Returns a static prefix length only for indexed array literals without nested spreads. -fn static_indexed_variadic_prefix_len(prefix_expr: &Expr) -> Option { - let ExprKind::ArrayLiteral(items) = &prefix_expr.kind else { - return None; - }; - if items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { - return None; +/// Returns true when an `unset(...)` target has direct EIR lowering. +fn unset_target_supported(ctx: &LoweringContext<'_, '_>, arg: &Expr) -> bool { + match &arg.kind { + ExprKind::Variable(_) => true, + ExprKind::ArrayAccess { array, .. } => { + unset_array_access_has_object_receiver(ctx, array) + || unset_array_access_has_local_array_receiver(ctx, array) + } + ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } => { + unset_property_access_has_direct_lowering(ctx, object, property) + } + _ => false, } - Some(items.len()) } -/// Builds a variadic tail hash from static spread overflow plus later named variadics. -#[allow(clippy::too_many_arguments)] -fn lower_named_spread_static_variadic_tail_hash( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - prefix_temp: &Expr, - prefix_len: usize, - regular_param_count: usize, - plan: &crate::types::call_args::CallArgPlan, - source_values: &[Option], - first_named_pos: usize, - span: crate::span::Span, -) -> LoweredValue { - let value_ty = variadic_tail_value_type(sig); - let prefix_tail_len = prefix_len.saturating_sub(regular_param_count); - let named_tail_len = plan - .source_values - .iter() - .filter(|source| source.source_index() >= first_named_pos && source.param_idx().is_none()) - .count(); - let hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(value_ty.clone()), +/// Returns true when an array-access unset receiver is a plain array/hash local whose element the +/// EIR backend can remove. +/// +/// Associative arrays remove the element directly; packed indexed arrays are converted to a hash at +/// the unset site (PHP `unset()` leaves a sparse array). By-reference locals are excluded: their +/// storage is aliased to a caller whose static type would no longer match after a representation +/// change. +fn unset_array_access_has_local_array_receiver( + ctx: &LoweringContext<'_, '_>, + array: &Expr, +) -> bool { + let ExprKind::Variable(name) = &array.kind else { + return false; }; - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity((prefix_tail_len + named_tail_len) as u32)), - hash_ty, - Op::HashNew.default_effects(), - Some(span), - ); - let array_ty = PhpType::Array(Box::new(value_ty.clone())); - let mut next_positional_key = 0usize; - for prefix_idx in regular_param_count..prefix_len { - let key = emit_i64_at_span(ctx, next_positional_key as i64, span); - next_positional_key += 1; - let expr = spread_element_expr_for_ir(prefix_temp, prefix_idx, None, false, span); - let value = lower_expr(ctx, &expr); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, span); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(span), - ); + if ctx.is_ref_bound_local(name) { + return false; } - for source in &plan.source_values { - if source.source_index() < first_named_pos || source.param_idx().is_some() { - continue; + matches!( + ctx.local_type(name).codegen_repr(), + PhpType::AssocArray { .. } | PhpType::Array(_) + ) +} + +/// Returns true when an array-access unset receiver is a static ArrayAccess object. +fn unset_array_access_has_object_receiver( + ctx: &LoweringContext<'_, '_>, + array: &Expr, +) -> bool { + let ty = match &array.kind { + ExprKind::Variable(name) => ctx + .local_types + .get(name) + .cloned() + .unwrap_or_else(|| infer_expr_type_syntactic(array)), + _ => infer_expr_type_syntactic(array), + }; + type_satisfies_array_access_for_ir(ctx, &ty) +} + +/// Lowers `unset($array[$key])`, dispatching on the receiver kind. +/// +/// An associative-array local removes the element in place through `Op::HashUnset`. A packed +/// indexed-array local is first converted to a hash (PHP keeps the surviving keys without +/// renumbering) and then removed. An `ArrayAccess` object dispatches to its `offsetUnset($key)` +/// method like before. By-reference array locals fall through to the object path. +fn lower_unset_array_access( + ctx: &mut LoweringContext<'_, '_>, + array: &Expr, + index: &Expr, + expr: &Expr, +) { + if let ExprKind::Variable(name) = &array.kind { + if !ctx.is_ref_bound_local(name) { + match ctx.local_type(name).codegen_repr() { + PhpType::AssocArray { .. } => { + lower_unset_hash_element(ctx, name, array.span, index, expr); + return; + } + PhpType::Array(elem_ty) => { + let elem_ty = if *elem_ty == PhpType::Never { + PhpType::Mixed + } else { + *elem_ty + }; + lower_unset_indexed_element(ctx, name, elem_ty, array.span, index, expr); + return; + } + _ => {} + } } - let key = if let Some(key) = source.key() { - lower_string_literal(ctx, key, source.expr()) - } else { - let key = emit_i64_at_span(ctx, next_positional_key as i64, source.expr().span); - next_positional_key += 1; - key - }; - let value = source_values[source.source_index()] - .expect("named spread variadic source was not evaluated"); - let value = lowered_value_from_id(ctx, value); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(source.expr().span), - ); } - hash + let synthetic = Expr::new( + ExprKind::MethodCall { + object: Box::new(array.clone()), + method: "offsetUnset".to_string(), + args: vec![index.clone()], + }, + expr.span, + ); + lower_expr(ctx, &synthetic); } -/// Emits named-after-spread min/max checks against the already materialized prefix temp. -fn emit_named_spread_bounds_guard( +/// Lowers `unset($hash[$key])` for an associative-array local as a `HashUnset` instruction. +/// +/// Loads the array local, lowers the key, and emits the removal. The backend (`lower_hash_unset`) +/// copy-on-write splits the table, releases the removed key/value payloads, and stores the unique +/// table pointer back into the local slot, so no explicit store-back is needed here. +fn lower_unset_hash_element( ctx: &mut LoweringContext<'_, '_>, - spread: crate::ir::ValueId, - check: &crate::types::call_args::SpreadBoundsCheck, - span: crate::span::Span, + name: &str, + array_span: Span, + index: &Expr, + expr: &Expr, ) { - if check.min_len == 0 && check.max_len.is_none() { - return; - } - let len = ctx.emit_value( - Op::ArrayLen, - vec![spread], + let array_value = ctx.load_local(name, Some(array_span)); + let index_value = lower_expr(ctx, index); + ctx.emit_void( + Op::HashUnset, + vec![array_value.value, index_value.value], None, - PhpType::Int, - Op::ArrayLen.default_effects(), - Some(span), - ); - emit_named_spread_min_len_guard(ctx, len.value, check.min_len, span); - emit_named_spread_max_len_guard( - ctx, - len.value, - check.max_len, - check.max_len_param_name.as_deref(), - span, + Op::HashUnset.default_effects(), + Some(expr.span), ); } -/// Emits the underflow branch for a named-after-spread bounds check. -fn emit_named_spread_min_len_guard( +/// Lowers `unset($arr[$key])` for a packed indexed-array local. +/// +/// PHP's `unset()` removes a key without renumbering, so the array can no longer be a contiguous +/// packed list (e.g. `unset([1,2,3][1])` leaves keys `0` and `2`). The local is converted to a hash +/// (`Op::ArrayToHash`) and retyped as `AssocArray`, after which the element is removed +/// through `HashUnset`. Subsequent uses of the local therefore see the associative representation. +fn lower_unset_indexed_element( ctx: &mut LoweringContext<'_, '_>, - len: crate::ir::ValueId, - min_len: usize, - span: crate::span::Span, + name: &str, + elem_ty: PhpType, + array_span: Span, + index: &Expr, + expr: &Expr, ) { - if min_len == 0 { - return; - } - let min = emit_i64_at_span(ctx, min_len as i64, span); - let has_required_args = ctx.emit_value( - Op::ICmp, - vec![len, min.value], - Some(Immediate::CmpPredicate(CmpPredicate::Sge)), - PhpType::Bool, - Op::ICmp.default_effects(), - Some(span), + let array_value = ctx.load_local(name, Some(array_span)); + let assoc_ty = PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(elem_ty), + }; + let hash = ctx.emit_value( + Op::ArrayToHash, + vec![array_value.value], + None, + assoc_ty.clone(), + Op::ArrayToHash.default_effects(), + Some(array_span), ); - let ok = ctx.builder.create_named_block("call.named_spread.min.ok", Vec::new()); - let fatal = ctx.builder.create_named_block("call.named_spread.min.fatal", Vec::new()); - ctx.builder.terminate(Terminator::CondBr { - cond: has_required_args.value, - then_target: ok, - then_args: Vec::new(), - else_target: fatal, - else_args: Vec::new(), - }); - - ctx.builder.position_at_end(fatal); - let message = ctx.intern_string("Fatal error: named argument spread length mismatch\n"); - ctx.builder.terminate(Terminator::Fatal { message }); + ctx.store_mutated_local(name, hash, assoc_ty, Some(array_span)); + lower_unset_hash_element(ctx, name, array_span, index, expr); +} - ctx.builder.position_at_end(ok); +/// Returns true when a property unset target can be lowered without normal property storage support. +fn unset_property_access_has_direct_lowering( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> bool { + matches!( + property_unset_action(ctx, object, property), + Some(UnsetPropertyAction::Magic | UnsetPropertyAction::Noop) + ) } -/// Emits the overflow branch for a named-after-spread bounds check. -fn emit_named_spread_max_len_guard( +/// Lowers `unset($object->property)` for magic and no-op property targets. +fn lower_unset_property_access( ctx: &mut LoweringContext<'_, '_>, - len: crate::ir::ValueId, - max_len: Option, - param_name: Option<&str>, - span: crate::span::Span, + object: &Expr, + property: &str, + expr: &Expr, ) { - let Some(max_len) = max_len else { - return; - }; - let max = emit_i64_at_span(ctx, max_len as i64, span); - let within_bound = ctx.emit_value( - Op::ICmp, - vec![len, max.value], - Some(Immediate::CmpPredicate(CmpPredicate::Sle)), + match property_unset_action(ctx, object, property) { + Some(UnsetPropertyAction::Magic) => { + let object = lower_expr(ctx, object); + lower_magic_property_unset(ctx, object, property, expr); + } + Some(UnsetPropertyAction::Noop) => { + lower_expr(ctx, object); + } + Some(UnsetPropertyAction::Fallback) | None => {} + } +} + +/// Describes how `unset($object->property)` should be lowered for a known receiver class. +enum UnsetPropertyAction { + Fallback, + Magic, + Noop, +} + +/// Selects the PHP-visible `unset()` behavior for a statically known object property operand. +fn property_unset_action( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> Option { + let (class_name, _) = isset_object_expr_class(ctx, object)?; + if is_builtin_stdclass_name(&class_name) { + return Some(UnsetPropertyAction::Fallback); + } + let class_info = ctx.classes.get(class_name.as_str())?; + if class_info.allow_dynamic_properties { + return Some(UnsetPropertyAction::Fallback); + } + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + return Some(UnsetPropertyAction::Fallback); + } + if class_method_signature(ctx, &class_name, &php_symbol_key("__unset")).is_some() { + Some(UnsetPropertyAction::Magic) + } else { + Some(UnsetPropertyAction::Noop) + } +} + +/// Lowers a magic `__unset($name)` call, guarding nullable receivers as a no-op. +fn lower_magic_property_unset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + expr: &Expr, +) { + if value_is_nullable(ctx, object.value) { + lower_nullable_magic_property_unset(ctx, object, property, expr); + return; + } + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + expr.span, + )]; + lower_method_call_with_receiver(ctx, object, "__unset", &args, Op::MethodCall, expr); +} + +/// Lowers `__unset` for nullable receivers, doing nothing when the receiver is null. +fn lower_nullable_magic_property_unset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + expr: &Expr, +) { + let null_block = ctx + .builder + .create_named_block("unset.property.null", Vec::new()); + let call_block = ctx + .builder + .create_named_block("unset.property.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("unset.property.merge", Vec::new()); + let is_null = ctx.emit_value( + Op::IsNull, + vec![object.value], + None, PhpType::Bool, - Op::ICmp.default_effects(), - Some(span), + Op::IsNull.default_effects(), + Some(expr.span), ); - let ok = ctx.builder.create_named_block("call.named_spread.max.ok", Vec::new()); - let fatal = ctx.builder.create_named_block("call.named_spread.max.fatal", Vec::new()); ctx.builder.terminate(Terminator::CondBr { - cond: within_bound.value, - then_target: ok, + cond: is_null.value, + then_target: null_block, then_args: Vec::new(), - else_target: fatal, + else_target: call_block, else_args: Vec::new(), }); - ctx.builder.position_at_end(fatal); - let message = if let Some(param_name) = param_name { - format!( - "Fatal error: Named parameter ${} overwrites previous argument\n", - param_name - ) - } else { - "Fatal error: named argument spread length mismatch\n".to_string() - }; - let message = ctx.intern_string(&message); - ctx.builder.terminate(Terminator::Fatal { message }); + ctx.builder.position_at_end(null_block); + branch_to(ctx, merge); - ctx.builder.position_at_end(ok); + ctx.builder.position_at_end(call_block); + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + expr.span, + )]; + lower_method_call_with_receiver(ctx, object, "__unset", &args, Op::MethodCall, expr); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); } -/// Lowers a single associative spread as named parameter reads by key. -fn lower_assoc_spread_only_args( +/// Lowers `array_push($local, $value)` as a direct indexed-array mutation. +fn lower_static_array_push( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, + name: &str, args: &[Expr], -) -> Option> { - let [arg] = args else { + expr: &Expr, +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "array_push" || args.len() != 2 { return None; - }; - let ExprKind::Spread(inner) = &arg.kind else { + } + if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { + return None; + } + let ExprKind::Variable(array_name) = &args[0].kind else { return None; }; - if !is_assoc_spread_source(ctx, inner) || sig.variadic.is_some() { + if !matches!(ctx.local_type(array_name).codegen_repr(), PhpType::Array(_)) { return None; } - let spread = lower_expr(ctx, inner); - let spread_type = ctx.builder.value_php_type(spread.value); - let temp_name = ctx.declare_hidden_temp(spread_type.clone()); - store_value_into_temp(ctx, &temp_name, spread_type, spread, arg.span); - let spread_expr = Expr::new(ExprKind::Variable(temp_name), inner.span); - let mut operands = Vec::with_capacity(sig.params.len()); - for (idx, (param_name, _)) in sig.params.iter().enumerate() { - let default = sig.defaults.get(idx).and_then(|default| default.as_ref()); - let param_expr = assoc_spread_param_expr(&spread_expr, param_name, default, arg.span); - operands.push(lower_expr(ctx, ¶m_expr).value); + let array_value = ctx.load_local(array_name, Some(args[0].span)); + if array_value.ir_type != IrType::Heap(IrHeapKind::Array) { + return None; } - Some(operands) + let value = lower_expr(ctx, &args[1]); + let (array_value, updated_ty, needs_storeback) = + if super::stmt::ref_bound_mixed_indexed_array_write(ctx, array_name, value) { + (array_value, Some(ctx.local_type(array_name)), true) + } else { + super::stmt::prepare_indexed_array_local_write(ctx, array_value, value, expr.span) + }; + ctx.emit_void( + Op::ArrayPush, + vec![array_value.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(expr.span), + ); + let elem_ty = + super::stmt::indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); + super::stmt::finish_indexed_array_local_write( + ctx, + array_name, + array_value, + updated_ty, + needs_storeback, + expr.span, + ); + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, expr.span); + Some(lower_null(ctx, expr)) } -/// Builds an expression that reads one named parameter from an associative spread. -fn assoc_spread_param_expr( - spread_expr: &Expr, - param_name: &str, - default: Option<&Expr>, - span: crate::span::Span, -) -> Expr { - let key = Expr::new(ExprKind::StringLiteral(param_name.to_string()), span); - let access = Expr::new( - ExprKind::ArrayAccess { - array: Box::new(spread_expr.clone()), - index: Box::new(key.clone()), - }, - span, - ); - let Some(default) = default else { - return access; - }; - Expr::new( - ExprKind::Ternary { - condition: Box::new(Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified("array_key_exists"), - args: vec![key, spread_expr.clone()], - }, - span, - )), - then_expr: Box::new(access), - else_expr: Box::new(default.clone()), - }, - span, - ) +/// Lowers builtin call operands, applying builtin-specific preservation where source order matters. +fn lower_builtin_call_args( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if is_empty_static_indexed_spread_arg(args) && zero_arity_call_signature(name, sig) { + return Vec::new(); + } + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "count" => lower_count_args(ctx, sig, args), + "date" => lower_date_args(ctx, sig, args), + "eval" => lower_eval_args(ctx, sig, args), + "json_decode" => lower_json_decode_args(ctx, sig, args), + "preg_replace_callback" + if !crate::types::call_args::has_named_args(args) + && !args.iter().any(is_spread_arg) => + { + lower_preg_replace_callback_args(ctx, sig, args) + } + "preg_match" | "preg_split" + if !crate::types::call_args::has_named_args(args) + && !args.iter().any(is_spread_arg) => + { + lower_args(ctx, args) + } + "usort" | "uasort" + if !crate::types::call_args::has_named_args(args) + && !args.iter().any(is_spread_arg) => + { + lower_user_value_sort_args(ctx, sig, args) + } + _ => lower_args_with_signature(ctx, sig, args), + } } -/// Builds an expression that reads one materialized spread element from a hidden temp. -fn spread_element_expr_for_ir( - spread_expr: &Expr, - element_idx: usize, - param_name: Option<&str>, - prefer_named_key: bool, - span: crate::span::Span, -) -> Expr { - let index = if prefer_named_key { - param_name - .map(|name| Expr::new(ExprKind::StringLiteral(name.to_string()), span)) - .unwrap_or_else(|| Expr::new(ExprKind::IntLiteral(element_idx as i64), span)) - } else { - Expr::new(ExprKind::IntLiteral(element_idx as i64), span) - }; - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(spread_expr.clone()), - index: Box::new(index), - }, - span, - ) +/// Lowers `count()` arguments, dropping a statically-default mode argument. +/// +/// The EIR backend implements only `COUNT_NORMAL`; a literal `0` mode (named +/// or positional) is semantically a no-op and would otherwise trip the unary +/// count contract in codegen. +fn lower_count_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let pruned: Vec = args + .iter() + .enumerate() + .filter(|(index, arg)| !count_arg_is_static_default_mode(*index, arg)) + .map(|(_, arg)| arg.clone()) + .collect(); + let mut operands = lower_args_with_signature(ctx, sig, &pruned); + // Named and spread plans re-materialize the optional `mode` default even + // after the AST prune; a trailing constant-zero mode stays a no-op for + // the unary count contract, so drop the operand (DCE reclaims the const). + if operands.len() == 2 { + let trailing_zero_mode = ctx + .builder + .value_defining_instruction(operands[1]) + .is_some_and(|inst| { + inst.op == Op::ConstI64 + && matches!(inst.immediate, Some(crate::ir::Immediate::I64(0))) + }); + if trailing_zero_mode { + operands.pop(); + } + } + operands } -/// Builds an expression that falls back to a default when a spread element is absent. -fn spread_element_or_default_expr_for_ir( - spread_expr: &Expr, - element_idx: usize, - param_name: Option<&str>, - prefer_named_key: bool, - default_expr: Expr, - span: crate::span::Span, -) -> Expr { - let condition = if prefer_named_key { - if let Some(param_name) = param_name { - Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified("array_key_exists"), - args: vec![ - Expr::new(ExprKind::StringLiteral(param_name.to_string()), span), - spread_expr.clone(), - ], - }, - span, - ) - } else { - spread_len_gt_expr_for_ir(spread_expr, element_idx, span) +/// Returns true when a `count()` argument is a statically-zero mode. +fn count_arg_is_static_default_mode(index: usize, arg: &Expr) -> bool { + match &arg.kind { + ExprKind::NamedArg { name, value } => { + name == "mode" && matches!(value.kind, ExprKind::IntLiteral(0)) } - } else { - spread_len_gt_expr_for_ir(spread_expr, element_idx, span) - }; - Expr::new( - ExprKind::Ternary { - condition: Box::new(condition), - then_expr: Box::new(spread_element_expr_for_ir( - spread_expr, - element_idx, - param_name, - prefer_named_key, - span, - )), - else_expr: Box::new(default_expr), - }, - span, - ) + ExprKind::IntLiteral(0) => index == 1, + _ => false, + } } -/// Builds `count($spread) > element_idx` for optional spread-slot defaults. -fn spread_len_gt_expr_for_ir( - spread_expr: &Expr, - element_idx: usize, - span: crate::span::Span, -) -> Expr { - Expr::new( - ExprKind::BinaryOp { - left: Box::new(Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified("count"), - args: vec![spread_expr.clone()], - }, - span, - )), - op: BinOp::Gt, - right: Box::new(Expr::new(ExprKind::IntLiteral(element_idx as i64), span)), - }, - span, +/// Lowers eval's code operand and coerces it through PHP string-conversion rules. +fn lower_eval_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let operands = lower_args_with_signature(ctx, sig, args); + let Some(code) = operands.first().copied() else { + return operands; + }; + let code_value = LoweredValue { + value: code, + ir_type: ctx.builder.value_type(code), + }; + let span = args.first().map(|arg| arg.span); + vec![coerce_to_string_at_span(ctx, code_value, span).value] +} + +/// Lowers `usort`/`uasort` arguments, typing an unannotated comparator closure +/// against the array's object element type. +/// +/// `usort`/`uasort` compare values, so a comparator over an array of objects must +/// see each element as the object handle — for `<=>` instant comparison and for +/// property/method access — not the raw pointer-sized integer the runtime stores +/// in each slot. The array operand is lowered exactly as the default positional +/// path would (positional builtin calls reach here with no signature); only an +/// unannotated closure comparator over an object-element array is specialized, +/// matching the element-type hint the checker applied to the comparator body. +fn lower_user_value_sort_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if args.len() != 2 || !matches!(&args[1].kind, ExprKind::Closure { .. }) { + return lower_args_with_signature(ctx, sig, args); + } + // The mutating sort keeps its by-reference local storeback in the EIR backend, + // so the array operand only has to resolve to the array's value here. + let array = match sig { + Some(sig) => lower_arg_with_signature(ctx, sig, 0, &args[0]), + None => lower_expr(ctx, &args[0]).value, + }; + let elem_ty = match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::Array(elem) => elem.codegen_repr(), + _ => PhpType::Int, + }; + // Only an object-element array needs the comparator parameters re-typed; scalar + // comparators already lower correctly through the default path. + let callback = if matches!(elem_ty, PhpType::Object(_)) { + lower_value_sort_comparator_closure(ctx, &args[1], elem_ty) + } else { + match sig { + Some(sig) => lower_arg_with_signature(ctx, sig, 1, &args[1]), + None => lower_expr(ctx, &args[1]).value, + } + }; + vec![array, callback] +} + +/// Lowers a value-sort comparator closure with both parameters typed as the array element. +/// +/// Falls back to the plain closure lowering for any non-closure callback operand, +/// though callers only reach this path with a closure comparator. +fn lower_value_sort_comparator_closure( + ctx: &mut LoweringContext<'_, '_>, + callback: &Expr, + elem_ty: PhpType, +) -> crate::ir::ValueId { + let ExprKind::Closure { + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + capture_refs, + is_static, + .. + } = &callback.kind + else { + return lower_expr(ctx, callback).value; + }; + lower_closure_with_context( + ctx, + params, + variadic.as_deref(), + *variadic_by_ref, + return_type.as_ref(), + body, + captures, + capture_refs, + callback, + &[elem_ty.clone(), elem_ty], + None, + *is_static, ) + .value } -/// Marks spread arguments whose source is known to be an associative array. -fn assoc_spread_sources(ctx: &LoweringContext<'_, '_>, args: &[Expr]) -> Vec { - crate::types::call_args::expand_static_assoc_spread_args(args) - .iter() - .map(|arg| match &arg.kind { - ExprKind::Spread(inner) => is_assoc_spread_source(ctx, inner), - _ => false, - }) - .collect() +/// Returns true when the call uses exactly one static empty indexed spread. +fn is_empty_static_indexed_spread_arg(args: &[Expr]) -> bool { + let [arg] = args else { + return false; + }; + let ExprKind::Spread(inner) = &arg.kind else { + return false; + }; + matches!(&inner.kind, ExprKind::ArrayLiteral(items) if items.is_empty()) } -/// Returns true when a spread expression should feed named parameters by key. -fn is_assoc_spread_source(ctx: &LoweringContext<'_, '_>, expr: &Expr) -> bool { - match &expr.kind { - ExprKind::Variable(name) => matches!(ctx.local_types.get(name), Some(PhpType::AssocArray { .. })), - ExprKind::ArrayLiteralAssoc(_) => true, - _ => matches!(infer_expr_type_syntactic(expr), PhpType::AssocArray { .. }), +/// Returns true when the callable signature accepts no visible operands. +fn zero_arity_call_signature(name: &str, sig: Option<&FunctionSig>) -> bool { + if let Some(sig) = sig { + return is_zero_arity_signature(sig); } + builtin_call_signature(name) + .as_ref() + .is_some_and(is_zero_arity_signature) } -/// Lowers one source call argument, unwrapping named syntax while preserving source position. -fn lower_call_source_arg(ctx: &mut LoweringContext<'_, '_>, arg: &Expr) -> crate::ir::ValueId { - match &arg.kind { - ExprKind::NamedArg { value, .. } => lower_expr(ctx, value).value, - _ => lower_expr(ctx, arg).value, - } +/// Returns true when a signature has no regular or variadic parameters. +fn is_zero_arity_signature(sig: &FunctionSig) -> bool { + crate::types::call_args::regular_param_count(sig) == 0 && sig.variadic.is_none() } -/// Builds the variadic tail array for a named-argument call plan. -fn lower_named_variadic_tail_array( +/// Lowers `settype($local, "type")` and updates subsequent local type facts. +fn lower_static_settype( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - tail: &[crate::types::call_args::PlannedSourceValue], - source_values: &[crate::ir::ValueId], -) -> LoweredValue { - if tail.iter().any(|source| source.key().is_some()) { - return lower_named_variadic_tail_hash(ctx, sig, tail, source_values); + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(name.trim_start_matches('\\')) != "settype" { + return None; } - let span = tail + let (var_arg, type_arg) = static_settype_arg_exprs(ctx, name, args)?; + let ExprKind::Variable(local_name) = &var_arg.kind else { + return None; + }; + let target_ty = static_settype_target_type(&type_arg)?; + let sig = call_signature(ctx, name, args); + let operands = lower_builtin_call_args(ctx, name, sig.as_ref(), args); + let result = emit_builtin_call_value(ctx, name, operands, PhpType::Bool, expr.span, None); + ctx.set_local_type(local_name, target_ty); + Some(result) +} + +/// Returns canonical `settype()` argument expressions for static local mutation lowering. +fn static_settype_arg_exprs( + ctx: &LoweringContext<'_, '_>, + name: &str, + args: &[Expr], +) -> Option<(Expr, Expr)> { + if args.len() != 2 || args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(args) { + return Some((args[0].clone(), args[1].clone())); + } + let sig = call_signature(ctx, name, args)?; + let call_span = args .first() - .map(|arg| arg.expr().span) + .map(|arg| arg.span) .unwrap_or_else(crate::span::Span::dummy); - let variadic_count = tail.iter().filter(|source| source.param_idx().is_none()).count(); - let array_ty = variadic_array_type(sig); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(variadic_count as u32)), - array_ty.clone(), - Op::ArrayNew.default_effects(), - Some(span), - ); - let elem_ty = indexed_array_literal_element_type(&array_ty); - for source in tail { - if source.param_idx().is_some() { - continue; - } - let value = source_values[source.source_index()]; - let value = lowered_value_from_id(ctx, value); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(source.expr().span), - ); - super::stmt::release_indexed_array_write_operand( - ctx, - elem_ty.as_ref(), - value, - source.expr().span, - ); + let regular_param_count = crate::types::call_args::regular_param_count(&sig); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + &sig, + args, + call_span, + regular_param_count, + false, + true, + &assoc_spread_sources(ctx, args), + ) + .ok()?; + if plan.has_spread_args() || plan.regular_args.len() != 2 { + return None; } - array + let var_arg = planned_regular_arg_expr(&plan.regular_args[0])?.clone(); + let type_arg = planned_regular_arg_expr(&plan.regular_args[1])?.clone(); + Some((var_arg, type_arg)) } -/// Builds an associative variadic tail when unknown named args must keep string keys. -fn lower_named_variadic_tail_hash( - ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - tail: &[crate::types::call_args::PlannedSourceValue], - source_values: &[crate::ir::ValueId], -) -> LoweredValue { - let span = tail - .first() - .map(|arg| arg.expr().span) - .unwrap_or_else(crate::span::Span::dummy); - let value_ty = variadic_tail_value_type(sig); - let variadic_count = tail.iter().filter(|source| source.param_idx().is_none()).count(); - let hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(value_ty.clone()), - }; - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity(variadic_count as u32)), - hash_ty, - Op::HashNew.default_effects(), - Some(span), - ); - let mut next_positional_key = 0usize; - for source in tail { - if source.param_idx().is_some() { - continue; - } - let key = if let Some(key) = source.key() { - lower_string_literal(ctx, key, source.expr()) - } else { - let key = emit_i64_at_span(ctx, next_positional_key as i64, source.expr().span); - next_positional_key += 1; - key - }; - let value = source_values[source.source_index()]; - let value = lowered_value_from_id(ctx, value); - let array_ty = PhpType::Array(Box::new(value_ty.clone())); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(source.expr().span), - ); +/// Returns the source expression assigned to a planned regular parameter. +fn planned_regular_arg_expr( + arg: &crate::types::call_args::PlannedRegularArg, +) -> Option<&Expr> { + match arg { + crate::types::call_args::PlannedRegularArg::Source { expr, .. } => Some(expr), + crate::types::call_args::PlannedRegularArg::Default(_) + | crate::types::call_args::PlannedRegularArg::SpreadElement { .. } => None, } - hash } -/// Rebuilds lowering metadata for an already emitted value. -fn lowered_value_from_id( - ctx: &LoweringContext<'_, '_>, - value: crate::ir::ValueId, -) -> LoweredValue { - LoweredValue { - value, - ir_type: ctx.builder.value_type(value), +/// Returns the PHP type named by a literal `settype()` second argument. +fn static_settype_target_type(arg: &Expr) -> Option { + let ExprKind::StringLiteral(name) = &arg.kind else { + return None; + }; + match php_symbol_key(name).as_str() { + "int" | "integer" => Some(PhpType::Int), + "float" | "double" => Some(PhpType::Float), + "string" => Some(PhpType::Str), + "bool" | "boolean" => Some(PhpType::Bool), + _ => None, } } -/// Lowers the synthetic variadic tail array using the variadic parameter's storage type. -fn lower_variadic_tail_array( +/// Lowers static function callbacks for `preg_replace_callback()`. +fn lower_preg_replace_callback_args( ctx: &mut LoweringContext<'_, '_>, - sig: &FunctionSig, - tail: &[Expr], -) -> LoweredValue { - let span = tail - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let array_ty = variadic_array_type(sig); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(tail.len() as u32)), - array_ty.clone(), - Op::ArrayNew.default_effects(), - Some(span), - ); - let elem_ty = indexed_array_literal_element_type(&array_ty); - for item in tail { - let value = lower_expr(ctx, item); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, item.span); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(item.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if args.len() != 3 { + return lower_args_with_signature(ctx, sig, args); } - array + if matches!(&args[1].kind, ExprKind::Closure { .. }) { + let pattern = lower_expr(ctx, &args[0]); + let callback = lower_preg_replace_callback_closure(ctx, &args[1]) + .expect("preg_replace_callback closure check must match lowering"); + let subject = lower_expr(ctx, &args[2]); + let subject = persist_call_arg_if_string(ctx, subject, args[2].span); + return vec![pattern.value, callback.value, subject.value]; + } + let Some(callback) = preg_replace_static_callback(ctx, &args[1]) else { + return lower_args_with_signature(ctx, sig, args); + }; + let pattern = lower_expr(ctx, &args[0]); + let callback = lower_string_literal(ctx, &callback, &args[1]); + let subject = lower_expr(ctx, &args[2]); + let subject = persist_call_arg_if_string(ctx, subject, args[2].span); + vec![pattern.value, callback.value, subject.value] } -/// Returns the element type expected inside a variadic tail container. -fn variadic_tail_value_type(sig: &FunctionSig) -> PhpType { - let Some(variadic_name) = sig.variadic.as_ref() else { - return PhpType::Mixed; +/// Lowers a `preg_replace_callback()` closure with match-array parameter context. +fn lower_preg_replace_callback_closure( + ctx: &mut LoweringContext<'_, '_>, + callback: &Expr, +) -> Option { + let ExprKind::Closure { + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + capture_refs, + is_static, + .. + } = &callback.kind + else { + return None; }; - sig.params - .iter() - .find(|(name, _)| name == variadic_name) - .map(|(_, ty)| match ty.codegen_repr() { - PhpType::Array(elem_ty) => variadic_container_element_type(*elem_ty), - other => variadic_container_element_type(other), - }) - .unwrap_or(PhpType::Mixed) + Some(lower_closure_with_context( + ctx, + params, + variadic.as_deref(), + *variadic_by_ref, + return_type.as_ref(), + body, + captures, + capture_refs, + callback, + &[PhpType::Array(Box::new(PhpType::Str))], + None, + *is_static, + )) } -/// Returns the runtime array type used for a variadic parameter slot. -fn variadic_array_type(sig: &FunctionSig) -> PhpType { - let Some(variadic_name) = sig.variadic.as_ref() else { - return PhpType::Array(Box::new(PhpType::Mixed)); - }; - sig.params - .iter() - .find(|(name, _)| name == variadic_name) - .map(|(_, ty)| match ty.codegen_repr() { - PhpType::Array(elem_ty) => { - PhpType::Array(Box::new(variadic_container_element_type(*elem_ty))) - } - other => PhpType::Array(Box::new(variadic_container_element_type(other))), - }) - .unwrap_or_else(|| PhpType::Array(Box::new(PhpType::Mixed))) +/// Returns the userland callback name accepted by the current regex runtime helper. +fn preg_replace_static_callback( + ctx: &LoweringContext<'_, '_>, + callback: &Expr, +) -> Option { + match &callback.kind { + ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { + Some(name.as_str().to_string()) + } + ExprKind::Variable(name) => match ctx.static_callable_local(name)? { + StaticCallableBinding::UserFunction(function_name) => Some(function_name), + _ => None, + }, + _ => None, + } } -/// Maps checker-only variadic container markers to their stored element type. -fn variadic_container_element_type(ty: PhpType) -> PhpType { - if matches!(ty, PhpType::Iterable) { - PhpType::Mixed - } else { - ty +/// Lowers simple positional `date` operands while stabilizing the format string before timestamp evaluation. +fn lower_date_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if args.len() != 2 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return lower_args_with_signature(ctx, sig, args); } + let format = lower_expr(ctx, &args[0]); + let format = persist_call_arg_if_string(ctx, format, args[0].span); + vec![format.value, lower_expr(ctx, &args[1]).value] } -/// Boxes variadic tail values when the callee expects an `array` slot. -fn coerce_variadic_tail_value( +/// Lowers simple positional `json_decode` operands while stabilizing string sources early. +fn lower_json_decode_args( ctx: &mut LoweringContext<'_, '_>, - value: LoweredValue, - array_ty: &PhpType, - span: crate::span::Span, -) -> LoweredValue { - let PhpType::Array(elem_ty) = array_ty.codegen_repr() else { - return value; - }; - if elem_ty.codegen_repr() != PhpType::Mixed { - return value; + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + if args.is_empty() + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return lower_args_with_signature(ctx, sig, args); } - if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { - return value; + let source = lower_expr(ctx, &args[0]); + let source = persist_call_arg_if_string(ctx, source, args[0].span); + let mut operands = Vec::with_capacity(args.len()); + operands.push(source.value); + for arg in &args[1..] { + operands.push(lower_expr(ctx, arg).value); } - ctx.emit_value( - Op::MixedBox, - vec![value.value], - None, - PhpType::Mixed, - Op::MixedBox.default_effects(), - Some(span), - ) + operands } -/// Returns true when a call argument uses unpacking syntax. -fn is_spread_arg(arg: &Expr) -> bool { - matches!(arg.kind, ExprKind::Spread(_)) -} - -/// Returns true when a call contains any static spread that EIR can flatten before lowering. -fn has_static_call_spread_args(args: &[Expr]) -> bool { - has_static_indexed_spread_args(args) || has_static_assoc_spread_args(args) +/// Emits `StrPersist` for already-string call operands before later arguments can reuse string scratch storage. +fn persist_call_arg_if_string( + ctx: &mut LoweringContext<'_, '_>, + source: LoweredValue, + span: crate::span::Span, +) -> LoweredValue { + if source.ir_type != IrType::Str { + return source; + } + ctx.emit_value( + Op::StrPersist, + vec![source.value], + None, + PhpType::Str, + Op::StrPersist.default_effects(), + Some(span), + ) } -/// Returns true when a call contains an indexed-array spread that EIR can flatten statically. -fn has_static_indexed_spread_args(args: &[Expr]) -> bool { - args.iter().any(|arg| match &arg.kind { - ExprKind::Spread(inner) => matches!(inner.kind, ExprKind::ArrayLiteral(_)), - _ => false, - }) +/// Lowers positional/named/spread call arguments in source order. +fn lower_args(ctx: &mut LoweringContext<'_, '_>, args: &[Expr]) -> Vec { + args.iter().map(|arg| lower_expr(ctx, arg).value).collect() } -/// Returns true when a call contains an associative-array spread literal that can be flattened. -fn has_static_assoc_spread_args(args: &[Expr]) -> bool { - args.iter().any(|arg| match &arg.kind { - ExprKind::Spread(inner) => matches!(inner.kind, ExprKind::ArrayLiteralAssoc(_)), - _ => false, - }) +/// Lowers one argument while applying by-reference storage normalization from a signature. +fn lower_arg_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + index: usize, + arg: &Expr, +) -> crate::ir::ValueId { + if let Some(value) = lower_by_ref_array_element_arg_with_signature(ctx, sig, index, arg) { + return value; + } + if let Some(value) = lower_by_ref_array_arg_with_signature(ctx, sig, index, arg) { + return value; + } + let lowered = lower_expr(ctx, arg); + coerce_scalar_arg_to_param_storage(ctx, sig, index, lowered, arg).value } -/// Flattens every statically-known call spread before EIR operand materialization. -fn expand_static_call_spread_args(args: &[Expr]) -> Vec { - let assoc_expanded = crate::types::call_args::expand_static_assoc_spread_args(args); - expand_static_indexed_spread_args(&assoc_expanded) +/// Coerces a positional argument's storage to match a declared scalar parameter type. +/// +/// EIR passes each call argument in its natural storage. A declared `float` parameter is +/// materialized into the callee's floating-point register/slot, so an integer argument must be +/// converted with `IToF` first: without it the raw 64-bit integer bit-pattern lands in the +/// float slot and the callee reads garbage (and, when other float arguments are present, the +/// unconverted slot is overwritten by a neighbouring float argument). Only the int→float case +/// is adjusted; every other argument/parameter storage combination is passed through unchanged. +fn coerce_scalar_arg_to_param_storage( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + index: usize, + value: LoweredValue, + arg: &Expr, +) -> LoweredValue { + let Some((_, param_ty)) = sig.params.get(index) else { + return value; + }; + if value.ir_type == IrType::I64 && param_ty.codegen_repr() == PhpType::Float { + return coerce_to_float(ctx, value, arg); + } + value } -/// Flattens static indexed array spreads into positional call arguments. -fn expand_static_indexed_spread_args(args: &[Expr]) -> Vec { - let mut expanded = Vec::new(); - for arg in args { - match &arg.kind { - ExprKind::Spread(inner) => { - if let ExprKind::ArrayLiteral(items) = &inner.kind { - expanded.extend(items.iter().map(|value| { - Expr::new(value.kind.clone(), arg.span) - })); - } else { - expanded.push(arg.clone()); - } - } - _ => expanded.push(arg.clone()), +/// Widens positional call operands to their declared scalar parameter types. +/// +/// The C/native ABI places an argument in an integer or floating-point register based +/// on the *value's* type, while the callee reads each parameter from the register class +/// of the *parameter's* type. Without this step an `int` (or `bool`) argument passed to +/// a `float` parameter is deposited in an integer register and then read back as garbage +/// from a floating-point slot. Only pure `float` parameters receiving an integer/bool +/// operand are rewritten with an int→float conversion; by-reference parameters and the +/// variadic tail operand are left untouched. +fn coerce_operands_to_params( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + mut operands: Vec, +) -> Vec { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let limit = operands.len().min(regular_param_count); + for index in 0..limit { + if sig.ref_params.get(index).copied().unwrap_or(false) { + continue; + } + let Some((_, param_ty)) = sig.params.get(index) else { + continue; + }; + if param_ty.codegen_repr() != PhpType::Float { + continue; + } + let value = operands[index]; + let operand_ty = ctx.builder.value_php_type(value).codegen_repr(); + if !matches!(operand_ty, PhpType::Int | PhpType::Bool) { + continue; } + let lowered = LoweredValue { + value, + ir_type: IrType::I64, + }; + operands[index] = coerce_to_float_at_span(ctx, lowered, None).value; } - expanded + operands } -/// Returns the best available return type for a function-like call. -fn call_return_type( - ctx: &LoweringContext<'_, '_>, - name: &str, - operands: &[crate::ir::ValueId], -) -> PhpType { - let php_type = if let Some(sig) = ctx.functions.get(name) { - eir_user_function_return_type(sig) - } else if let Some(sig) = ctx.extern_functions.get(name) { - sig.return_type.clone() - } else if let Some(php_type) = builtin_return_type_override(name) { - php_type - } else if let Some(php_type) = pointer_builtin_return_type(ctx, name, operands) { - php_type - } else if let Some(php_type) = numeric_builtin_return_type(ctx, name, operands) { - php_type - } else if let Some(php_type) = pathinfo_builtin_return_type(name, operands) { - php_type - } else if let Some(php_type) = regex_builtin_return_type(name) { - php_type - } else if let Some(php_type) = array_builtin_return_type(ctx, name, operands) { - php_type - } else if let Some(sig) = first_class_builtin_signature(name) { - sig.return_type - } else if let Some(sig) = builtin_call_signature(name) { - sig.return_type - } else { - PhpType::Mixed +/// Widens local indexed-array storage before passing it to an `array` ref parameter. +fn lower_by_ref_array_arg_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + index: usize, + arg: &Expr, +) -> Option { + if !sig.ref_params.get(index).copied().unwrap_or(false) { + return None; + } + let (_, param_ty) = sig.params.get(index)?; + let ExprKind::Variable(name) = &arg.kind else { + return None; }; - normalize_value_php_type(php_type) + if !by_ref_array_arg_needs_mixed_storage(ctx, name, param_ty) { + return None; + } + let array_ty = PhpType::Array(Box::new(PhpType::Mixed)); + let local = ctx.load_local(name, Some(arg.span)); + let converted = ctx.emit_value( + Op::ArrayToMixed, + vec![local.value], + None, + array_ty.clone(), + Op::ArrayToMixed.default_effects(), + Some(arg.span), + ); + ctx.store_mutated_local(name, converted, array_ty, Some(arg.span)); + Some(ctx.load_local(name, Some(arg.span)).value) } -/// Returns the caller-visible EIR return type for a user function signature. -fn eir_user_function_return_type(signature: &FunctionSig) -> PhpType { - if signature.declared_return || !signature_has_dynamic_untyped_param(signature) { - return signature.return_type.clone(); +/// Lowers `$array[$index]` as a direct by-reference argument cell address. +fn lower_by_ref_array_element_arg_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + index: usize, + arg: &Expr, +) -> Option { + if !sig.ref_params.get(index).copied().unwrap_or(false) { + return None; } - dynamic_param_container_return_type(&signature.return_type) + let ExprKind::ArrayAccess { array, index: element_index } = &arg.kind else { + return None; + }; + let ExprKind::Variable(array_name) = &array.kind else { + return None; + }; + let PhpType::Array(elem_ty) = ctx.local_type(array_name).codegen_repr() else { + return None; + }; + let (_, param_ty) = sig.params.get(index)?; + let element_ty = match normalize_value_php_type(*elem_ty) { + PhpType::Void => normalize_value_php_type(param_ty.codegen_repr()), + other => other, + }; + let array_value = ctx.load_local(array_name, Some(array.span)); + let element_index = lower_expr(ctx, element_index); + let element_index = coerce_to_int_at_span(ctx, element_index, Some(arg.span)); + let value = ctx + .builder + .emit_with_effects( + Op::ArrayElemAddr, + vec![array_value.value, element_index.value], + None, + IrType::I64, + element_ty, + Ownership::NonHeap, + Op::ArrayElemAddr.default_effects(), + Some(arg.span), + ) + .expect("array_elem_addr produces a value"); + Some(value) } -/// Returns true when a PHP signature has params that EIR must receive as Mixed. -fn signature_has_dynamic_untyped_param(signature: &FunctionSig) -> bool { - signature.params.iter().enumerate().any(|(index, (name, _))| { - let declared = signature.declared_params.get(index).copied().unwrap_or(false); - let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); - let variadic = signature.variadic.as_deref() == Some(name.as_str()); - !declared && !by_ref && !variadic - }) +/// Returns true when a local array must be converted before a by-reference call. +fn by_ref_array_arg_needs_mixed_storage( + ctx: &LoweringContext<'_, '_>, + name: &str, + param_ty: &PhpType, +) -> bool { + let PhpType::Array(param_elem) = param_ty.codegen_repr() else { + return false; + }; + if param_elem.codegen_repr() != PhpType::Mixed { + return false; + } + let PhpType::Array(local_elem) = ctx.local_type(name).codegen_repr() else { + return false; + }; + local_elem.codegen_repr() != PhpType::Mixed } -/// Widens inferred container return elements that may be built from dynamic params. -fn dynamic_param_container_return_type(return_type: &PhpType) -> PhpType { - match return_type.codegen_repr() { - PhpType::Array(_) => PhpType::Array(Box::new(PhpType::Mixed)), - PhpType::AssocArray { key, .. } => PhpType::AssocArray { - key, - value: Box::new(PhpType::Mixed), - }, - PhpType::Union(members) => PhpType::Union( - members - .iter() - .map(dynamic_param_container_return_type) - .collect(), - ), - other => other, +/// Lowers positional call arguments with omitted optional defaults and variadic tail packing. +fn lower_args_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let Some(sig) = sig else { + return lower_args(ctx, args); + }; + if crate::types::call_args::has_named_args(args) { + let operands = lower_named_args_with_signature(ctx, sig, args); + return coerce_operands_to_params(ctx, sig, operands); + } + if let Some(operands) = lower_positional_spread_args_with_signature(ctx, sig, args) { + return coerce_operands_to_params(ctx, sig, operands); + } + let static_spread_args = if has_static_call_spread_args(args) { + Some(expand_static_call_spread_args(args)) + } else { + None + }; + let args = static_spread_args.as_deref().unwrap_or(args); + if let Some(operands) = lower_assoc_spread_only_args(ctx, sig, args) { + return coerce_operands_to_params(ctx, sig, operands); + } + if args.iter().any(is_spread_arg) { + return lower_args(ctx, args); + } + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let fixed_arg_count = if sig.variadic.is_some() { + args.len().min(regular_param_count) + } else { + args.len() + }; + if sig.variadic.is_none() && fixed_arg_count >= regular_param_count { + let operands = args + .iter() + .enumerate() + .map(|(index, arg)| lower_arg_with_signature(ctx, sig, index, arg)) + .collect(); + return coerce_operands_to_params(ctx, sig, operands); + } + let mut operands: Vec = args[..fixed_arg_count] + .iter() + .enumerate() + .map(|(index, arg)| lower_arg_with_signature(ctx, sig, index, arg)) + .collect(); + for idx in fixed_arg_count..regular_param_count { + let Some(Some(default)) = sig.defaults.get(idx) else { + break; + }; + operands.push(lower_expr(ctx, default).value); + } + if sig.variadic.is_some() { + let tail = if args.len() > regular_param_count { + &args[regular_param_count..] + } else { + &[] + }; + operands.push(lower_variadic_tail_array(ctx, sig, tail).value); } + coerce_operands_to_params(ctx, sig, operands) } -/// Returns argument-sensitive builtin result metadata when AST operands are still available. -fn call_return_type_for_args( - ctx: &LoweringContext<'_, '_>, - name: &str, +/// Lowers one trailing indexed spread in a fixed-arity positional call. +fn lower_positional_spread_args_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, args: &[Expr], - operands: &[crate::ir::ValueId], -) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "array_fill" => array_fill_builtin_return_type_for_args(ctx, args, operands), - "array_map" => array_map_builtin_return_type(ctx, args, operands), - "iterator_to_array" => iterator_to_array_builtin_return_type(ctx, args, operands), - "microtime" => microtime_builtin_return_type_for_args(args), - "print_r" => print_r_builtin_return_type_for_args(args), - _ => None, +) -> Option> { + if sig.variadic.is_some() { + return None; + } + let spread_idx = single_trailing_indexed_spread_arg(ctx, args)?; + let regular_param_count = crate::types::call_args::regular_param_count(sig); + if spread_idx > regular_param_count { + return None; + } + let first_spread_param_idx = spread_idx; + let required_len = required_positional_spread_len(sig, first_spread_param_idx, regular_param_count); + let ExprKind::Spread(inner) = &args[spread_idx].kind else { + return None; + }; + if static_indexed_spread_len(inner).is_some_and(|len| len >= required_len) { + return None; } -} -/// Returns `microtime()` metadata when the literal `as_float` flag is still available. -/// -/// `microtime(true)` is a float; `microtime()` / `microtime(false)` is the "0.NNNNNNNN sec" -/// string; a non-literal flag returns `None` so the result type falls back to the `string|float` -/// union (boxed `Mixed`) declared in `call_return_type`. This must match the checker -/// (`src/types/checker/builtins/system.rs`) and the EIR backend dispatch in `lower_microtime`. -fn microtime_builtin_return_type_for_args(args: &[Expr]) -> Option { - match args.first() { - Some(arg) => match &arg.kind { - ExprKind::BoolLiteral(true) => Some(PhpType::Float), - ExprKind::BoolLiteral(false) => Some(PhpType::Str), - _ => None, - }, - None => Some(PhpType::Str), + let mut operands = Vec::with_capacity(regular_param_count); + for (index, arg) in args[..spread_idx].iter().enumerate() { + operands.push(lower_arg_with_signature(ctx, sig, index, arg)); } -} -/// Returns `print_r()` metadata when the literal `$return` flag is still available. -/// -/// `print_r($v, true)` returns a `Str` (the rendered output); `print_r($v)` / -/// `print_r($v, false)` echoes and returns `Bool` (true). A non-literal flag returns -/// `None` so the result type falls back to the `Mixed` declared in `call_return_type` -/// (`string|bool`, boxed — the mode is selected at run time). This must match the -/// checker hook (`src/builtins/io/print_r.rs`) and the EIR backend dispatch in -/// `lower_print_r`, which switches on this result type. -fn print_r_builtin_return_type_for_args(args: &[Expr]) -> Option { - match args.get(1) { - Some(arg) => match &arg.kind { - ExprKind::BoolLiteral(true) => Some(PhpType::Str), - ExprKind::BoolLiteral(false) => Some(PhpType::Bool), - _ => None, - }, - None => Some(PhpType::Bool), + let spread_type = indexed_spread_source_type(ctx, inner)?; + let spread = lower_expr(ctx, inner); + let temp_name = ctx.declare_hidden_temp(spread_type.clone()); + store_value_into_temp(ctx, &temp_name, spread_type, spread, args[spread_idx].span); + let spread_expr = Expr::new(ExprKind::Variable(temp_name), inner.span); + let spread_value = lower_expr(ctx, &spread_expr); + emit_positional_spread_min_len_guard( + ctx, + spread_value.value, + required_len, + args[spread_idx].span, + ); + + for param_idx in first_spread_param_idx..regular_param_count { + let element_idx = param_idx - first_spread_param_idx; + let default = sig.defaults.get(param_idx).and_then(|default| default.as_ref()); + let expr = if let Some(default) = default { + if element_idx < required_len { + spread_element_expr_for_ir( + &spread_expr, + element_idx, + None, + false, + args[spread_idx].span, + ) + } else { + spread_element_or_default_expr_for_ir( + &spread_expr, + element_idx, + None, + false, + default.clone(), + args[spread_idx].span, + ) + } + } else { + spread_element_expr_for_ir( + &spread_expr, + element_idx, + None, + false, + args[spread_idx].span, + ) + }; + operands.push(lower_expr(ctx, &expr).value); } + + Some(operands) } -/// Returns `array_fill()` metadata when the literal start expression is still available. -fn array_fill_builtin_return_type_for_args( - ctx: &LoweringContext<'_, '_>, - args: &[Expr], - operands: &[crate::ir::ValueId], -) -> Option { - if args.len() != 3 { - return None; - } - let value = operands.get(2)?; - let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); - let start_is_literal_zero = matches!(args[0].kind, ExprKind::IntLiteral(0)); - // A non-literal-zero start builds a keyed Mixed-valued hash (`__rt_array_fill_assoc`, - // keys start..start+count-1). A literal-zero start builds the 0-indexed path: string - // values use the dedicated 16-byte-slot `__rt_array_fill_str` helper, scalars use the - // single-word `__rt_array_fill` / `__rt_array_fill_refcounted` helpers. This must match - // the checker (`src/types/checker/builtins/arrays.rs`) and `infer_local_type`. - if !start_is_literal_zero { - return Some(PhpType::AssocArray { - key: Box::new(PhpType::Int), - value: Box::new(PhpType::Mixed), - }); +/// Returns the element count for a statically-known indexed spread source. +fn static_indexed_spread_len(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::ArrayLiteral(items) => Some(items.len()), + _ => None, } - Some(PhpType::Array(Box::new(array_fill_indexed_element_type(value_ty)))) } -/// Returns the EIR result metadata for `array_map()` when a callable param signature is known. -fn array_map_builtin_return_type( +/// Returns the index of a single trailing positional spread that EIR can materialize. +fn single_trailing_indexed_spread_arg( ctx: &LoweringContext<'_, '_>, args: &[Expr], - operands: &[crate::ir::ValueId], -) -> Option { - if args.len() != 2 { +) -> Option { + let spread_indices = args + .iter() + .enumerate() + .filter_map(|(idx, arg)| matches!(arg.kind, ExprKind::Spread(_)).then_some(idx)) + .collect::>(); + let [spread_idx] = spread_indices.as_slice() else { return None; - } - let callback_sig = callable_expr_signature(ctx, &args[0])?; - let return_ty = normalize_value_php_type(callback_sig.return_type.codegen_repr()); - if return_ty == PhpType::Mixed { + }; + if *spread_idx + 1 != args.len() { return None; } - let array = operands.get(1)?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(_) => Some(PhpType::Array(Box::new(return_ty))), - _ => None, - } + let ExprKind::Spread(inner) = &args[*spread_idx].kind else { + return None; + }; + indexed_spread_source_type(ctx, inner)?; + Some(*spread_idx) } -/// Returns the EIR result metadata for `iterator_to_array()` when preserve_keys is static. -fn iterator_to_array_builtin_return_type( +/// Returns the indexed-array source type for spread-only EIR lowering. +fn indexed_spread_source_type( ctx: &LoweringContext<'_, '_>, - args: &[Expr], - operands: &[crate::ir::ValueId], + expr: &Expr, ) -> Option { - let source = operands.first()?; - let preserve_keys = match args.get(1) { - Some(arg) => static_preserve_keys_expr(arg), - None => Some(true), - }; - preserve_keys - .map(|value| { - iterator_to_array_static_return_type( - &ctx.builder.value_php_type(*source).codegen_repr(), - value, - ) - }) - .or(Some(PhpType::Mixed)) -} - -/// Computes the concrete `iterator_to_array()` container type for one preserve_keys value. -fn iterator_to_array_static_return_type(source_ty: &PhpType, preserve_keys: bool) -> PhpType { - match source_ty.codegen_repr() { - PhpType::Array(elem_ty) => PhpType::Array(elem_ty), - PhpType::AssocArray { key, value } if preserve_keys => PhpType::AssocArray { key, value }, - PhpType::AssocArray { value, .. } => PhpType::Array(value), - _ if preserve_keys => PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }, - _ => PhpType::Array(Box::new(PhpType::Mixed)), + let ty = match &expr.kind { + ExprKind::Variable(name) => ctx.local_type(name), + ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, expr), + _ => infer_expr_type_syntactic(expr), + } + .codegen_repr(); + if matches!(ty, PhpType::Array(_)) { + Some(ty) + } else { + None } } -/// Evaluates literal PHP truthiness used by static `iterator_to_array()` preserve_keys. -fn static_preserve_keys_expr(expr: &Expr) -> Option { - match &expr.kind { - ExprKind::BoolLiteral(value) => Some(*value), - ExprKind::IntLiteral(value) => Some(*value != 0), - ExprKind::FloatLiteral(value) => Some(*value != 0.0), - ExprKind::StringLiteral(value) => Some(!value.is_empty() && value != "0"), - ExprKind::Null => Some(false), - ExprKind::Negate(inner) => match &inner.kind { - ExprKind::IntLiteral(value) => Some(*value != 0), - ExprKind::FloatLiteral(value) => Some(*value != 0.0), - _ => None, - }, - _ => None, - } +/// Returns how many spread elements must exist to satisfy required parameters. +fn required_positional_spread_len( + sig: &FunctionSig, + start_param_idx: usize, + regular_param_count: usize, +) -> usize { + (start_param_idx..regular_param_count) + .rfind(|idx| sig.defaults.get(*idx).and_then(|default| default.as_ref()).is_none()) + .map(|idx| idx - start_param_idx + 1) + .unwrap_or(0) } -/// Resolves callable expression metadata tracked during type checking and lowering. -fn callable_expr_signature<'a>( - ctx: &'a LoweringContext<'_, '_>, - callback: &Expr, -) -> Option<&'a FunctionSig> { - match &callback.kind { - ExprKind::Variable(name) => ctx.callable_param_signature(name), - _ => None, +/// Emits a fatal guard when a positional spread is shorter than required parameters. +fn emit_positional_spread_min_len_guard( + ctx: &mut LoweringContext<'_, '_>, + spread: crate::ir::ValueId, + min_len: usize, + span: crate::span::Span, +) { + if min_len == 0 { + return; } + let len = ctx.emit_value( + Op::ArrayLen, + vec![spread], + None, + PhpType::Int, + Op::ArrayLen.default_effects(), + Some(span), + ); + let min = emit_i64_at_span(ctx, min_len as i64, span); + let has_required_args = ctx.emit_value( + Op::ICmp, + vec![len.value, min.value], + Some(Immediate::CmpPredicate(CmpPredicate::Sge)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(span), + ); + let ok = ctx.builder.create_named_block("call.spread.len.ok", Vec::new()); + let fatal = ctx.builder.create_named_block("call.spread.len.fatal", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: has_required_args.value, + then_target: ok, + then_args: Vec::new(), + else_target: fatal, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(fatal); + let message = ctx.intern_string("Fatal error: too few arguments for spread call\n"); + ctx.builder.terminate(Terminator::Fatal { message }); + + ctx.builder.position_at_end(ok); } -/// Returns precise return metadata for pointer-extension builtins. -fn pointer_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - name: &str, - operands: &[crate::ir::ValueId], -) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "ptr" => Some(PhpType::Pointer(None)), - "ptr_null" => Some(PhpType::Pointer(None)), - "ptr_is_null" => Some(PhpType::Bool), - "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" | "ptr_sizeof" => { - Some(PhpType::Int) +/// Lowers named arguments in source order, then returns operands in signature order. +fn lower_named_args_with_signature( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + args: &[Expr], +) -> Vec { + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let assoc_spread_sources = assoc_spread_sources(ctx, args); + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let Ok(plan) = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + args, + call_span, + regular_param_count, + false, + true, + &assoc_spread_sources, + ) else { + return lower_args(ctx, args); + }; + if plan.has_spread_args() { + if let Some(operands) = lower_named_args_with_spread_plan(ctx, sig, &plan, &assoc_spread_sources) { + return operands; } - "ptr_read_string" => Some(PhpType::Str), - "ptr_set" | "ptr_write8" | "ptr_write16" | "ptr_write32" => Some(PhpType::Void), - "ptr_write_string" => Some(PhpType::Int), - "ptr_offset" => { - let pointer = operands.first()?; - match ctx.builder.value_php_type(*pointer).codegen_repr() { - PhpType::Pointer(tag) => Some(PhpType::Pointer(tag)), - _ => Some(PhpType::Pointer(None)), - } + if let Some(operands) = lower_dynamic_named_spread_variadic_args(ctx, sig, &plan) { + return operands; } - "zval_pack" => Some(PhpType::Pointer(None)), - "zval_unpack" => Some(PhpType::Mixed), - "zval_type" => Some(PhpType::Int), - "zval_free" => Some(PhpType::Void), - _ => None, + let normalized = plan.normalized_args(); + return lower_args(ctx, &normalized); + } + let mut source_values = Vec::with_capacity(plan.source_args.len()); + for source_arg in &plan.source_args { + source_values.push(lower_call_source_arg(ctx, source_arg)); } -} -/// Returns precise return metadata for numeric builtins whose result depends on operands. -fn numeric_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - name: &str, - operands: &[crate::ir::ValueId], -) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "abs" => { - let value = operands.first()?; - let ty = ctx.builder.value_php_type(*value).codegen_repr(); - Some(abs_builtin_return_type(&ty)) - } - "min" | "max" => { - let mut saw_float = false; - for value in operands { - match ctx.builder.value_php_type(*value).codegen_repr() { - PhpType::Float => saw_float = true, - PhpType::Int | PhpType::Bool => {} - _ => return Some(PhpType::Mixed), - } + let mut operands = Vec::with_capacity(plan.regular_args.len() + usize::from(sig.variadic.is_some())); + for arg in &plan.regular_args { + match arg { + crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { + operands.push(source_values[*source_index]); } - Some(if saw_float { - PhpType::Float - } else { - PhpType::Int - }) - } - "clamp" => { - let mut saw_float = false; - let mut all_int = operands.len() == 3; - let mut all_string = operands.len() == 3; - let mut all_numeric = operands.len() == 3; - for value in operands.iter().take(3) { - match ctx.builder.value_php_type(*value).codegen_repr() { - PhpType::Int => { - all_string = false; - } - PhpType::Float => { - saw_float = true; - all_int = false; - all_string = false; - } - PhpType::Str => { - all_int = false; - all_numeric = false; - } - _ => { - all_int = false; - all_string = false; - all_numeric = false; - } - } + crate::types::call_args::PlannedRegularArg::Default(default) => { + operands.push(lower_expr(ctx, default).value); } - if all_string { - Some(PhpType::Str) - } else if all_int { - Some(PhpType::Int) - } else if all_numeric { - Some(if saw_float { - PhpType::Float - } else { - PhpType::Int - }) - } else { - Some(PhpType::Mixed) + crate::types::call_args::PlannedRegularArg::SpreadElement { .. } => { + return lower_args(ctx, args); } } - _ => None, } -} - -/// Returns the EIR storage type for `abs()` after operand-sensitive narrowing. -fn abs_builtin_return_type(ty: &PhpType) -> PhpType { - match ty { - PhpType::Float => PhpType::Float, - PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, - _ => PhpType::Int, + if sig.variadic.is_some() { + operands.push(lower_named_variadic_tail_array(ctx, sig, &plan.source_values, &source_values).value); } + operands } -/// Returns EIR result metadata for `pathinfo()` based on argument shape. -fn pathinfo_builtin_return_type(name: &str, operands: &[crate::ir::ValueId]) -> Option { - if php_symbol_key(name.trim_start_matches('\\')).as_str() != "pathinfo" { +/// Lowers dynamic associative prefix spreads for variadic calls far enough to preserve duplicate fatals. +fn lower_dynamic_named_spread_variadic_args( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + plan: &crate::types::call_args::CallArgPlan, +) -> Option> { + if sig.variadic.is_none() || !plan.prefix_has_dynamic_named_spread { return None; } - if operands.len() == 1 { - return Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Str), - }); + let call_span = plan + .source_args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let first_named_pos = plan.first_named_pos?; + let prefix_expr = plan.positional_prefix_expr(call_span)?; + let prefix = lower_expr(ctx, &prefix_expr); + if !matches!(ctx.builder.value_php_type(prefix.value).codegen_repr(), PhpType::AssocArray { .. }) { + return None; } - Some(PhpType::Mixed) -} + let prefix_type = ctx.builder.value_php_type(prefix.value); + let prefix_temp_name = ctx.declare_hidden_temp(prefix_type.clone()); + store_value_into_temp(ctx, &prefix_temp_name, prefix_type, prefix, prefix_expr.span); + let prefix_temp = Expr::new(ExprKind::Variable(prefix_temp_name), prefix_expr.span); -/// Returns precise EIR result metadata for regex builtins lowered by `codegen`. -fn regex_builtin_return_type(name: &str) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "preg_match" | "preg_match_all" => Some(PhpType::Int), - "preg_replace" => Some(PhpType::Str), - "preg_split" => Some(PhpType::Array(Box::new(PhpType::Mixed))), - _ => None, + let mut source_values = vec![None; plan.source_args.len()]; + for (source_index, source_arg) in plan.source_args.iter().enumerate().skip(first_named_pos) { + if matches!(source_arg.kind, ExprKind::Spread(_)) { + return None; + } + source_values[source_index] = Some(lower_call_source_arg(ctx, source_arg)); } -} + emit_dynamic_named_prefix_duplicate_guards(ctx, sig, plan, &prefix_temp, first_named_pos); -/// Returns precise return metadata for array builtins that preserve operand element type. -fn array_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - name: &str, - operands: &[crate::ir::ValueId], -) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "array_combine" => array_combine_builtin_return_type(ctx, operands), - "array_column" => array_column_builtin_return_type(ctx, operands), - "array_flip" => array_flip_builtin_return_type(ctx, operands), - "array_fill" => array_fill_builtin_return_type(ctx, operands), - "array_fill_keys" => array_fill_keys_builtin_return_type(ctx, operands), - "array_merge" => array_merge_builtin_return_type(ctx, operands), - "array_splice" | "array_filter" | "array_diff" | "array_intersect" | "array_diff_key" - | "array_intersect_key" => array_preserve_first_builtin_return_type(ctx, operands), - "in_array" => Some(PhpType::Bool), - "array_is_list" => Some(PhpType::Bool), - "array_key_first" | "array_key_last" => Some(PhpType::Mixed), - // `array_keys`/`array_slice` produce a fresh indexed array of boxed `Mixed` - // payloads (keys, or the sliced elements). These mirror the result type the - // first-class-callable fallback supplied before they were registered as - // builtins, so the EIR backend keeps receiving a concrete `Array` result - // type rather than the registry's `Mixed` return-type placeholder. - "array_keys" | "array_slice" => Some(PhpType::Array(Box::new(PhpType::Mixed))), - "range" => Some(PhpType::Array(Box::new(PhpType::Int))), - "array_values" => { - let array = operands.first()?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(elem) => Some(PhpType::Array(elem)), - PhpType::AssocArray { value, .. } => Some(PhpType::Array(value)), - other => Some(other), + let mut operands = Vec::with_capacity(plan.regular_args.len() + 1); + for arg in &plan.regular_args { + match arg { + crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { + operands.push(source_values.get(*source_index).copied().flatten()?); } - } - "array_reverse" | "array_unique" | "array_pad" => { - let array = operands.first()?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(elem) => Some(PhpType::Array(elem)), - other => Some(other), + crate::types::call_args::PlannedRegularArg::Default(default) => { + operands.push(lower_expr(ctx, default).value); } - } - "array_chunk" => { - let array = operands.first()?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(elem) => Some(PhpType::Array(Box::new(PhpType::Array(elem)))), - other => Some(other), + crate::types::call_args::PlannedRegularArg::SpreadElement { + prefix_element_idx, + param_name, + prefer_named_key, + default, + guaranteed_present, + spread_span, + .. + } => { + let expr = if let Some(default) = default { + if *guaranteed_present { + spread_element_expr_for_ir( + &prefix_temp, + *prefix_element_idx, + param_name.as_deref(), + *prefer_named_key, + *spread_span, + ) + } else { + spread_element_or_default_expr_for_ir( + &prefix_temp, + *prefix_element_idx, + param_name.as_deref(), + *prefer_named_key, + default.clone(), + *spread_span, + ) + } + } else { + spread_element_expr_for_ir( + &prefix_temp, + *prefix_element_idx, + param_name.as_deref(), + *prefer_named_key, + *spread_span, + ) + }; + operands.push(lower_expr(ctx, &expr).value); } } - "array_replace" | "array_replace_recursive" | "array_diff_assoc" - | "array_intersect_assoc" => two_input_hash_builtin_return_type(ctx, operands), - "array_merge_recursive" => array_merge_recursive_builtin_return_type(ctx, operands), - "array_find" => Some(PhpType::Mixed), - "array_any" | "array_all" => Some(PhpType::Bool), - "array_multisort" => Some(PhpType::Bool), - "array_walk_recursive" => Some(PhpType::Void), - "array_udiff" | "array_uintersect" => { - array_preserve_first_builtin_return_type(ctx, operands) - } - _ => None, } + operands.push(lower_variadic_tail_array(ctx, sig, &[]).value); + Some(operands) } -/// Returns the hash result metadata for the two-input hash builtins (`array_replace`, -/// `array_replace_recursive`, `array_diff_assoc`, `array_intersect_assoc`). -/// -/// Mirrors the type checker's `two_input_hash_result`: the key and value each widen to `Mixed` -/// when the two operands disagree, so the result hash dispatches keys/values correctly at runtime. -fn two_input_hash_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let first = operands.first()?; - let second = operands.get(1)?; - let t1 = ctx.builder.value_php_type(*first).codegen_repr(); - let t2 = ctx.builder.value_php_type(*second).codegen_repr(); - Some(PhpType::two_input_hash_result(&t1, &t2)) +/// Emits duplicate checks for numeric prefix keys overwritten by later named parameters. +fn emit_dynamic_named_prefix_duplicate_guards( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + plan: &crate::types::call_args::CallArgPlan, + prefix_temp: &Expr, + first_named_pos: usize, +) { + for source in &plan.source_values { + if source.source_index() < first_named_pos { + continue; + } + let Some(param_idx) = source.param_idx() else { + continue; + }; + let Some((param_name, _)) = sig.params.get(param_idx) else { + continue; + }; + emit_dynamic_named_prefix_duplicate_guard( + ctx, + prefix_temp, + param_idx, + param_name, + source.expr().span, + ); + } } -/// Returns the hash result metadata for `array_merge_recursive(a, b)`. -/// -/// Scalar collisions combine into lists, so the value type is always `Mixed`; the key widens to -/// `Mixed` when the two operands disagree, matching the type checker. -fn array_merge_recursive_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let first = operands.first()?; - let second = operands.get(1)?; - let t1 = ctx.builder.value_php_type(*first).codegen_repr(); - let t2 = ctx.builder.value_php_type(*second).codegen_repr(); - Some(PhpType::AssocArray { - key: Box::new(PhpType::widen(t1.hash_key_type(), t2.hash_key_type())), - value: Box::new(PhpType::Mixed), - }) -} +/// Emits one duplicate guard for a numeric key in a dynamic associative prefix. +fn emit_dynamic_named_prefix_duplicate_guard( + ctx: &mut LoweringContext<'_, '_>, + prefix_temp: &Expr, + param_idx: usize, + param_name: &str, + span: crate::span::Span, +) { + let exists_expr = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("array_key_exists"), + args: vec![ + Expr::new(ExprKind::IntLiteral(param_idx as i64), span), + prefix_temp.clone(), + ], + }, + span, + ); + let exists = lower_expr(ctx, &exists_expr); + let ok = ctx.builder.create_named_block("call.dynamic_named_prefix.ok", Vec::new()); + let fatal = ctx.builder.create_named_block("call.dynamic_named_prefix.fatal", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: exists.value, + then_target: fatal, + then_args: Vec::new(), + else_target: ok, + else_args: Vec::new(), + }); -/// Returns precise return metadata for `array_fill(start, count, value)`. -fn array_fill_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let value = operands.get(2)?; - let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); - Some(PhpType::Array(Box::new(array_fill_indexed_element_type(value_ty)))) -} + ctx.builder.position_at_end(fatal); + let message = format!( + "Fatal error: Named parameter ${} overwrites previous argument\n", + param_name + ); + let message = ctx.intern_string(&message); + ctx.builder.terminate(Terminator::Fatal { message }); -/// Returns the indexed element storage type for EIR `array_fill()` results. -fn array_fill_indexed_element_type(value_ty: PhpType) -> PhpType { - match value_ty.codegen_repr() { - PhpType::Void | PhpType::Never => PhpType::Mixed, - other => other, - } + ctx.builder.position_at_end(ok); } -/// Returns the extracted column element type for `array_column()`. -fn array_column_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], +/// Lowers named/spread argument plans without re-evaluating dynamic spread expressions. +fn lower_named_args_with_spread_plan( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + plan: &crate::types::call_args::CallArgPlan, + assoc_spread_sources: &[bool], +) -> Option> { + if assoc_spread_sources.iter().any(|is_assoc| *is_assoc) { + return None; + } + let call_span = plan + .source_args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let first_named_pos = plan.first_named_pos?; + let prefix_expr = plan.positional_prefix_expr(call_span)?; + let static_variadic_prefix_len = static_indexed_variadic_prefix_len(&prefix_expr); + if sig.variadic.is_some() && static_variadic_prefix_len.is_none() { + return None; + } + let prefix = lower_expr(ctx, &prefix_expr); + let prefix_type = ctx.builder.value_php_type(prefix.value); + let prefix_temp_name = ctx.declare_hidden_temp(prefix_type.clone()); + store_value_into_temp(ctx, &prefix_temp_name, prefix_type, prefix, prefix_expr.span); + let single_prefix_spread = !matches!(prefix_expr.kind, ExprKind::ArrayLiteral(_)); + let prefix_temp = Expr::new(ExprKind::Variable(prefix_temp_name), prefix_expr.span); + + let mut source_values = vec![None; plan.source_args.len()]; + for (source_index, source_arg) in plan.source_args.iter().enumerate().skip(first_named_pos) { + if matches!(source_arg.kind, ExprKind::Spread(_)) { + return None; + } + source_values[source_index] = Some(lower_call_source_arg(ctx, source_arg)); + } + if single_prefix_spread { + if let [check] = plan.spread_bounds_checks.as_slice() { + let prefix_value = lower_expr(ctx, &prefix_temp); + emit_named_spread_bounds_guard(ctx, prefix_value.value, check, call_span); + } + } + + let mut operands = Vec::with_capacity(plan.regular_args.len()); + for (param_idx, arg) in plan.regular_args.iter().enumerate() { + match arg { + crate::types::call_args::PlannedRegularArg::Source { source_index, .. } => { + if *source_index < first_named_pos { + let expr = spread_element_expr_for_ir( + &prefix_temp, + param_idx, + None, + false, + plan.source_args.get(*source_index).map(|arg| arg.span).unwrap_or(call_span), + ); + operands.push(lower_expr(ctx, &expr).value); + } else { + operands.push(source_values.get(*source_index).copied().flatten()?); + } + } + crate::types::call_args::PlannedRegularArg::Default(default) => { + operands.push(lower_expr(ctx, default).value); + } + crate::types::call_args::PlannedRegularArg::SpreadElement { + element_idx: _, + prefix_element_idx, + param_name, + prefer_named_key, + default, + guaranteed_present, + spread_span, + .. + } => { + let element_idx = *prefix_element_idx; + let expr = if let Some(default) = default { + if *guaranteed_present { + spread_element_expr_for_ir( + &prefix_temp, + element_idx, + param_name.as_deref(), + *prefer_named_key, + *spread_span, + ) + } else { + spread_element_or_default_expr_for_ir( + &prefix_temp, + element_idx, + param_name.as_deref(), + *prefer_named_key, + default.clone(), + *spread_span, + ) + } + } else { + spread_element_expr_for_ir( + &prefix_temp, + element_idx, + param_name.as_deref(), + *prefer_named_key, + *spread_span, + ) + }; + operands.push(lower_expr(ctx, &expr).value); + } + } + } + if sig.variadic.is_some() { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let tail = lower_named_spread_static_variadic_tail_hash( + ctx, + sig, + &prefix_temp, + static_variadic_prefix_len.unwrap_or(regular_param_count), + regular_param_count, + plan, + &source_values, + first_named_pos, + call_span, + ); + operands.push(tail.value); + } + Some(operands) +} + +/// Returns a static prefix length only for indexed array literals without nested spreads. +fn static_indexed_variadic_prefix_len(prefix_expr: &Expr) -> Option { + let ExprKind::ArrayLiteral(items) = &prefix_expr.kind else { + return None; + }; + if items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { + return None; + } + Some(items.len()) +} + +/// Builds a variadic tail hash from static spread overflow plus later named variadics. +#[allow(clippy::too_many_arguments)] +fn lower_named_spread_static_variadic_tail_hash( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + prefix_temp: &Expr, + prefix_len: usize, + regular_param_count: usize, + plan: &crate::types::call_args::CallArgPlan, + source_values: &[Option], + first_named_pos: usize, + span: crate::span::Span, +) -> LoweredValue { + let value_ty = variadic_tail_value_type(sig); + let prefix_tail_len = prefix_len.saturating_sub(regular_param_count); + let named_tail_len = plan + .source_values + .iter() + .filter(|source| source.source_index() >= first_named_pos && source.param_idx().is_none()) + .count(); + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(value_ty.clone()), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity((prefix_tail_len + named_tail_len) as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(span), + ); + let array_ty = PhpType::Array(Box::new(value_ty.clone())); + let mut next_positional_key = 0usize; + for prefix_idx in regular_param_count..prefix_len { + let key = emit_i64_at_span(ctx, next_positional_key as i64, span); + next_positional_key += 1; + let expr = spread_element_expr_for_ir(prefix_temp, prefix_idx, None, false, span); + let value = lower_expr(ctx, &expr); + let value = coerce_variadic_tail_value(ctx, value, &array_ty, span); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(span), + ); + } + for source in &plan.source_values { + if source.source_index() < first_named_pos || source.param_idx().is_some() { + continue; + } + let key = if let Some(key) = source.key() { + lower_string_literal(ctx, key, source.expr()) + } else { + let key = emit_i64_at_span(ctx, next_positional_key as i64, source.expr().span); + next_positional_key += 1; + key + }; + let value = source_values[source.source_index()] + .expect("named spread variadic source was not evaluated"); + let value = lowered_value_from_id(ctx, value); + let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(source.expr().span), + ); + } + hash +} + +/// Emits named-after-spread min/max checks against the already materialized prefix temp. +fn emit_named_spread_bounds_guard( + ctx: &mut LoweringContext<'_, '_>, + spread: crate::ir::ValueId, + check: &crate::types::call_args::SpreadBoundsCheck, + span: crate::span::Span, +) { + if check.min_len == 0 && check.max_len.is_none() { + return; + } + let len = ctx.emit_value( + Op::ArrayLen, + vec![spread], + None, + PhpType::Int, + Op::ArrayLen.default_effects(), + Some(span), + ); + emit_named_spread_min_len_guard(ctx, len.value, check.min_len, span); + emit_named_spread_max_len_guard( + ctx, + len.value, + check.max_len, + check.max_len_param_name.as_deref(), + span, + ); +} + +/// Emits the underflow branch for a named-after-spread bounds check. +fn emit_named_spread_min_len_guard( + ctx: &mut LoweringContext<'_, '_>, + len: crate::ir::ValueId, + min_len: usize, + span: crate::span::Span, +) { + if min_len == 0 { + return; + } + let min = emit_i64_at_span(ctx, min_len as i64, span); + let has_required_args = ctx.emit_value( + Op::ICmp, + vec![len, min.value], + Some(Immediate::CmpPredicate(CmpPredicate::Sge)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(span), + ); + let ok = ctx.builder.create_named_block("call.named_spread.min.ok", Vec::new()); + let fatal = ctx.builder.create_named_block("call.named_spread.min.fatal", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: has_required_args.value, + then_target: ok, + then_args: Vec::new(), + else_target: fatal, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(fatal); + let message = ctx.intern_string("Fatal error: named argument spread length mismatch\n"); + ctx.builder.terminate(Terminator::Fatal { message }); + + ctx.builder.position_at_end(ok); +} + +/// Emits the overflow branch for a named-after-spread bounds check. +fn emit_named_spread_max_len_guard( + ctx: &mut LoweringContext<'_, '_>, + len: crate::ir::ValueId, + max_len: Option, + param_name: Option<&str>, + span: crate::span::Span, +) { + let Some(max_len) = max_len else { + return; + }; + let max = emit_i64_at_span(ctx, max_len as i64, span); + let within_bound = ctx.emit_value( + Op::ICmp, + vec![len, max.value], + Some(Immediate::CmpPredicate(CmpPredicate::Sle)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(span), + ); + let ok = ctx.builder.create_named_block("call.named_spread.max.ok", Vec::new()); + let fatal = ctx.builder.create_named_block("call.named_spread.max.fatal", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: within_bound.value, + then_target: ok, + then_args: Vec::new(), + else_target: fatal, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(fatal); + let message = if let Some(param_name) = param_name { + format!( + "Fatal error: Named parameter ${} overwrites previous argument\n", + param_name + ) + } else { + "Fatal error: named argument spread length mismatch\n".to_string() + }; + let message = ctx.intern_string(&message); + ctx.builder.terminate(Terminator::Fatal { message }); + + ctx.builder.position_at_end(ok); +} + +/// Lowers a single associative spread as named parameter reads by key. +fn lower_assoc_spread_only_args( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + args: &[Expr], +) -> Option> { + let [arg] = args else { + return None; + }; + let ExprKind::Spread(inner) = &arg.kind else { + return None; + }; + if !is_assoc_spread_source(ctx, inner) || sig.variadic.is_some() { + return None; + } + let spread = lower_expr(ctx, inner); + let spread_type = ctx.builder.value_php_type(spread.value); + let temp_name = ctx.declare_hidden_temp(spread_type.clone()); + store_value_into_temp(ctx, &temp_name, spread_type, spread, arg.span); + let spread_expr = Expr::new(ExprKind::Variable(temp_name), inner.span); + let mut operands = Vec::with_capacity(sig.params.len()); + for (idx, (param_name, _)) in sig.params.iter().enumerate() { + let default = sig.defaults.get(idx).and_then(|default| default.as_ref()); + let param_expr = assoc_spread_param_expr(&spread_expr, param_name, default, arg.span); + operands.push(lower_expr(ctx, ¶m_expr).value); + } + Some(operands) +} + +/// Builds an expression that reads one named parameter from an associative spread. +fn assoc_spread_param_expr( + spread_expr: &Expr, + param_name: &str, + default: Option<&Expr>, + span: crate::span::Span, +) -> Expr { + let key = Expr::new(ExprKind::StringLiteral(param_name.to_string()), span); + let access = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(spread_expr.clone()), + index: Box::new(key.clone()), + }, + span, + ); + let Some(default) = default else { + return access; + }; + Expr::new( + ExprKind::Ternary { + condition: Box::new(Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("array_key_exists"), + args: vec![key, spread_expr.clone()], + }, + span, + )), + then_expr: Box::new(access), + else_expr: Box::new(default.clone()), + }, + span, + ) +} + +/// Builds an expression that reads one materialized spread element from a hidden temp. +fn spread_element_expr_for_ir( + spread_expr: &Expr, + element_idx: usize, + param_name: Option<&str>, + prefer_named_key: bool, + span: crate::span::Span, +) -> Expr { + let index = if prefer_named_key { + param_name + .map(|name| Expr::new(ExprKind::StringLiteral(name.to_string()), span)) + .unwrap_or_else(|| Expr::new(ExprKind::IntLiteral(element_idx as i64), span)) + } else { + Expr::new(ExprKind::IntLiteral(element_idx as i64), span) + }; + Expr::new( + ExprKind::ArrayAccess { + array: Box::new(spread_expr.clone()), + index: Box::new(index), + }, + span, + ) +} + +/// Builds an expression that falls back to a default when a spread element is absent. +fn spread_element_or_default_expr_for_ir( + spread_expr: &Expr, + element_idx: usize, + param_name: Option<&str>, + prefer_named_key: bool, + default_expr: Expr, + span: crate::span::Span, +) -> Expr { + let condition = if prefer_named_key { + if let Some(param_name) = param_name { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("array_key_exists"), + args: vec![ + Expr::new(ExprKind::StringLiteral(param_name.to_string()), span), + spread_expr.clone(), + ], + }, + span, + ) + } else { + spread_len_gt_expr_for_ir(spread_expr, element_idx, span) + } + } else { + spread_len_gt_expr_for_ir(spread_expr, element_idx, span) + }; + Expr::new( + ExprKind::Ternary { + condition: Box::new(condition), + then_expr: Box::new(spread_element_expr_for_ir( + spread_expr, + element_idx, + param_name, + prefer_named_key, + span, + )), + else_expr: Box::new(default_expr), + }, + span, + ) +} + +/// Builds `count($spread) > element_idx` for optional spread-slot defaults. +fn spread_len_gt_expr_for_ir( + spread_expr: &Expr, + element_idx: usize, + span: crate::span::Span, +) -> Expr { + Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("count"), + args: vec![spread_expr.clone()], + }, + span, + )), + op: BinOp::Gt, + right: Box::new(Expr::new(ExprKind::IntLiteral(element_idx as i64), span)), + }, + span, + ) +} + +/// Marks spread arguments whose source is known to be an associative array. +fn assoc_spread_sources(ctx: &LoweringContext<'_, '_>, args: &[Expr]) -> Vec { + crate::types::call_args::expand_static_assoc_spread_args(args) + .iter() + .map(|arg| match &arg.kind { + ExprKind::Spread(inner) => is_assoc_spread_source(ctx, inner), + _ => false, + }) + .collect() +} + +/// Returns true when a spread expression should feed named parameters by key. +fn is_assoc_spread_source(ctx: &LoweringContext<'_, '_>, expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Variable(name) => matches!(ctx.local_types.get(name), Some(PhpType::AssocArray { .. })), + ExprKind::ArrayLiteralAssoc(_) => true, + _ => matches!(infer_expr_type_syntactic(expr), PhpType::AssocArray { .. }), + } +} + +/// Lowers one source call argument, unwrapping named syntax while preserving source position. +fn lower_call_source_arg(ctx: &mut LoweringContext<'_, '_>, arg: &Expr) -> crate::ir::ValueId { + match &arg.kind { + ExprKind::NamedArg { value, .. } => lower_expr(ctx, value).value, + _ => lower_expr(ctx, arg).value, + } +} + +/// Builds the variadic tail array for a named-argument call plan. +fn lower_named_variadic_tail_array( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + tail: &[crate::types::call_args::PlannedSourceValue], + source_values: &[crate::ir::ValueId], +) -> LoweredValue { + if tail.iter().any(|source| source.key().is_some()) { + return lower_named_variadic_tail_hash(ctx, sig, tail, source_values); + } + let span = tail + .first() + .map(|arg| arg.expr().span) + .unwrap_or_else(crate::span::Span::dummy); + let variadic_count = tail.iter().filter(|source| source.param_idx().is_none()).count(); + let array_ty = variadic_array_type(sig); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(variadic_count as u32)), + array_ty.clone(), + Op::ArrayNew.default_effects(), + Some(span), + ); + let elem_ty = indexed_array_literal_element_type(&array_ty); + let by_ref_variadic = variadic_param_is_by_ref(sig); + for source in tail { + if source.param_idx().is_some() { + continue; + } + let value = lower_variadic_tail_source_value( + ctx, + source.expr(), + by_ref_variadic, + Some(source_values[source.source_index()]), + &array_ty, + ); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(source.expr().span), + ); + super::stmt::release_indexed_array_write_operand( + ctx, + elem_ty.as_ref(), + value, + source.expr().span, + ); + } + array +} + +/// Builds an associative variadic tail when unknown named args must keep string keys. +fn lower_named_variadic_tail_hash( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + tail: &[crate::types::call_args::PlannedSourceValue], + source_values: &[crate::ir::ValueId], +) -> LoweredValue { + let span = tail + .first() + .map(|arg| arg.expr().span) + .unwrap_or_else(crate::span::Span::dummy); + let value_ty = variadic_tail_value_type(sig); + let variadic_count = tail.iter().filter(|source| source.param_idx().is_none()).count(); + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(value_ty.clone()), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(variadic_count as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(span), + ); + let mut next_positional_key = 0usize; + let by_ref_variadic = variadic_param_is_by_ref(sig); + for source in tail { + if source.param_idx().is_some() { + continue; + } + let key = if let Some(key) = source.key() { + lower_string_literal(ctx, key, source.expr()) + } else { + let key = emit_i64_at_span(ctx, next_positional_key as i64, source.expr().span); + next_positional_key += 1; + key + }; + let value = lower_variadic_tail_source_value( + ctx, + source.expr(), + by_ref_variadic, + Some(source_values[source.source_index()]), + &PhpType::Array(Box::new(value_ty.clone())), + ); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(source.expr().span), + ); + } + hash +} + +/// Rebuilds lowering metadata for an already emitted value. +fn lowered_value_from_id( + ctx: &LoweringContext<'_, '_>, + value: crate::ir::ValueId, +) -> LoweredValue { + LoweredValue { + value, + ir_type: ctx.builder.value_type(value), + } +} + +/// Lowers the synthetic variadic tail array using the variadic parameter's storage type. +fn lower_variadic_tail_array( + ctx: &mut LoweringContext<'_, '_>, + sig: &FunctionSig, + tail: &[Expr], +) -> LoweredValue { + let span = tail + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let array_ty = variadic_array_type(sig); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(tail.len() as u32)), + array_ty.clone(), + Op::ArrayNew.default_effects(), + Some(span), + ); + let elem_ty = indexed_array_literal_element_type(&array_ty); + let by_ref_variadic = variadic_param_is_by_ref(sig); + for item in tail { + let value = lower_variadic_tail_source_value(ctx, item, by_ref_variadic, None, &array_ty); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); + } + array +} + +/// Lowers one value stored into a variadic tail container. +fn lower_variadic_tail_source_value( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, + by_ref_variadic: bool, + prelowered: Option, + array_ty: &PhpType, +) -> LoweredValue { + if by_ref_variadic { + if let ExprKind::Variable(name) = &expr.kind { + return lower_invoker_ref_arg_marker(ctx, name, expr.span); + } + } + let value = prelowered + .map(|value| lowered_value_from_id(ctx, value)) + .unwrap_or_else(|| lower_expr(ctx, expr)); + coerce_variadic_tail_value(ctx, value, array_ty, expr.span) +} + +/// Returns whether the synthetic variadic parameter slot is by-reference. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + +/// Returns the element type expected inside a variadic tail container. +fn variadic_tail_value_type(sig: &FunctionSig) -> PhpType { + if variadic_param_is_by_ref(sig) { + return PhpType::Mixed; + } + let Some(variadic_name) = sig.variadic.as_ref() else { + return PhpType::Mixed; + }; + sig.params + .iter() + .find(|(name, _)| name == variadic_name) + .map(|(_, ty)| match ty.codegen_repr() { + PhpType::Array(elem_ty) => variadic_container_element_type(*elem_ty), + other => variadic_container_element_type(other), + }) + .unwrap_or(PhpType::Mixed) +} + +/// Returns the runtime array type used for a variadic parameter slot. +fn variadic_array_type(sig: &FunctionSig) -> PhpType { + if variadic_param_is_by_ref(sig) { + return PhpType::Array(Box::new(PhpType::Mixed)); + } + let Some(variadic_name) = sig.variadic.as_ref() else { + return PhpType::Array(Box::new(PhpType::Mixed)); + }; + sig.params + .iter() + .find(|(name, _)| name == variadic_name) + .map(|(_, ty)| match ty.codegen_repr() { + PhpType::Array(elem_ty) => { + PhpType::Array(Box::new(variadic_container_element_type(*elem_ty))) + } + other => PhpType::Array(Box::new(variadic_container_element_type(other))), + }) + .unwrap_or_else(|| PhpType::Array(Box::new(PhpType::Mixed))) +} + +/// Maps checker-only variadic container markers to their stored element type. +fn variadic_container_element_type(ty: PhpType) -> PhpType { + if matches!(ty, PhpType::Iterable) { + PhpType::Mixed + } else { + ty + } +} + +/// Boxes variadic tail values when the callee expects an `array` slot. +fn coerce_variadic_tail_value( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + array_ty: &PhpType, + span: crate::span::Span, +) -> LoweredValue { + let PhpType::Array(elem_ty) = array_ty.codegen_repr() else { + return value; + }; + if elem_ty.codegen_repr() != PhpType::Mixed { + return value; + } + if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { + return value; + } + ctx.emit_value( + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) +} + +/// Returns true when a call argument uses unpacking syntax. +fn is_spread_arg(arg: &Expr) -> bool { + matches!(arg.kind, ExprKind::Spread(_)) +} + +/// Returns true when a call contains any static spread that EIR can flatten before lowering. +fn has_static_call_spread_args(args: &[Expr]) -> bool { + has_static_indexed_spread_args(args) || has_static_assoc_spread_args(args) +} + +/// Returns true when a call contains an indexed-array spread that EIR can flatten statically. +fn has_static_indexed_spread_args(args: &[Expr]) -> bool { + args.iter().any(|arg| match &arg.kind { + ExprKind::Spread(inner) => matches!(inner.kind, ExprKind::ArrayLiteral(_)), + _ => false, + }) +} + +/// Returns true when a call contains an associative-array spread literal that can be flattened. +fn has_static_assoc_spread_args(args: &[Expr]) -> bool { + args.iter().any(|arg| match &arg.kind { + ExprKind::Spread(inner) => matches!(inner.kind, ExprKind::ArrayLiteralAssoc(_)), + _ => false, + }) +} + +/// Flattens every statically-known call spread before EIR operand materialization. +fn expand_static_call_spread_args(args: &[Expr]) -> Vec { + let assoc_expanded = crate::types::call_args::expand_static_assoc_spread_args(args); + expand_static_indexed_spread_args(&assoc_expanded) +} + +/// Flattens static indexed array spreads into positional call arguments. +fn expand_static_indexed_spread_args(args: &[Expr]) -> Vec { + let mut expanded = Vec::new(); + for arg in args { + match &arg.kind { + ExprKind::Spread(inner) => { + if let ExprKind::ArrayLiteral(items) = &inner.kind { + expanded.extend(items.iter().map(|value| { + Expr::new(value.kind.clone(), arg.span) + })); + } else { + expanded.push(arg.clone()); + } + } + _ => expanded.push(arg.clone()), + } + } + expanded +} + +/// Returns the best available return type for a function-like call. +fn call_return_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + operands: &[crate::ir::ValueId], +) -> PhpType { + let php_type = if let Some(sig) = ctx.functions.get(name) { + eir_user_function_return_type(sig) + } else if let Some(sig) = ctx.extern_functions.get(name) { + sig.return_type.clone() + } else if let Some(php_type) = builtin_return_type_override(name) { + php_type + } else if let Some(php_type) = pointer_builtin_return_type(ctx, name, operands) { + php_type + } else if let Some(php_type) = numeric_builtin_return_type(ctx, name, operands) { + php_type + } else if let Some(php_type) = pathinfo_builtin_return_type(name, operands) { + php_type + } else if let Some(php_type) = regex_builtin_return_type(name) { + php_type + } else if let Some(php_type) = array_builtin_return_type(ctx, name, operands) { + php_type + } else if let Some(sig) = first_class_builtin_signature(name) { + sig.return_type + } else if let Some(sig) = builtin_call_signature(name) { + sig.return_type + } else { + PhpType::Mixed + }; + normalize_value_php_type(php_type) +} + +/// Returns the caller-visible EIR return type for a user function signature. +fn eir_user_function_return_type(signature: &FunctionSig) -> PhpType { + if signature.declared_return || !signature_has_dynamic_untyped_param(signature) { + return signature.return_type.clone(); + } + dynamic_param_container_return_type(&signature.return_type) +} + +/// Returns true when a PHP signature has params that EIR must receive as Mixed. +fn signature_has_dynamic_untyped_param(signature: &FunctionSig) -> bool { + signature.params.iter().enumerate().any(|(index, (name, _))| { + let declared = signature.declared_params.get(index).copied().unwrap_or(false); + let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); + let variadic = signature.variadic.as_deref() == Some(name.as_str()); + !declared && !by_ref && !variadic + }) +} + +/// Widens inferred container return elements that may be built from dynamic params. +fn dynamic_param_container_return_type(return_type: &PhpType) -> PhpType { + match return_type.codegen_repr() { + PhpType::Array(_) => PhpType::Array(Box::new(PhpType::Mixed)), + PhpType::AssocArray { key, .. } => PhpType::AssocArray { + key, + value: Box::new(PhpType::Mixed), + }, + PhpType::Union(members) => PhpType::Union( + members + .iter() + .map(dynamic_param_container_return_type) + .collect(), + ), + other => other, + } +} + +/// Returns argument-sensitive builtin result metadata when AST operands are still available. +fn call_return_type_for_args( + ctx: &LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + operands: &[crate::ir::ValueId], +) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "array_fill" => array_fill_builtin_return_type_for_args(ctx, args, operands), + "array_map" => array_map_builtin_return_type(ctx, args, operands), + "iterator_to_array" => iterator_to_array_builtin_return_type(ctx, args, operands), + "microtime" => microtime_builtin_return_type_for_args(args), + "print_r" => print_r_builtin_return_type_for_args(args), + _ => None, + } +} + +/// Returns `microtime()` metadata when the literal `as_float` flag is still available. +/// +/// `microtime(true)` is a float; `microtime()` / `microtime(false)` is the "0.NNNNNNNN sec" +/// string; a non-literal flag returns `None` so the result type falls back to the `string|float` +/// union (boxed `Mixed`) declared in `call_return_type`. This must match the checker +/// (`src/types/checker/builtins/system.rs`) and the EIR backend dispatch in `lower_microtime`. +fn microtime_builtin_return_type_for_args(args: &[Expr]) -> Option { + match args.first() { + Some(arg) => match &arg.kind { + ExprKind::BoolLiteral(true) => Some(PhpType::Float), + ExprKind::BoolLiteral(false) => Some(PhpType::Str), + _ => None, + }, + None => Some(PhpType::Str), + } +} + +/// Returns `print_r()` metadata when the literal `$return` flag is still available. +/// +/// `print_r($v, true)` returns a `Str` (the rendered output); `print_r($v)` / +/// `print_r($v, false)` echoes and returns `Bool` (true). A non-literal flag returns +/// `None` so the result type falls back to the `Mixed` declared in `call_return_type` +/// (`string|bool`, boxed — the mode is selected at run time). This must match the +/// checker hook (`src/builtins/io/print_r.rs`) and the EIR backend dispatch in +/// `lower_print_r`, which switches on this result type. +fn print_r_builtin_return_type_for_args(args: &[Expr]) -> Option { + match args.get(1) { + Some(arg) => match &arg.kind { + ExprKind::BoolLiteral(true) => Some(PhpType::Str), + ExprKind::BoolLiteral(false) => Some(PhpType::Bool), + _ => None, + }, + None => Some(PhpType::Bool), + } +} + +/// Returns `array_fill()` metadata when the literal start expression is still available. +fn array_fill_builtin_return_type_for_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + operands: &[crate::ir::ValueId], +) -> Option { + if args.len() != 3 { + return None; + } + let value = operands.get(2)?; + let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); + let start_is_literal_zero = matches!(args[0].kind, ExprKind::IntLiteral(0)); + // A non-literal-zero start builds a keyed Mixed-valued hash (`__rt_array_fill_assoc`, + // keys start..start+count-1). A literal-zero start builds the 0-indexed path: string + // values use the dedicated 16-byte-slot `__rt_array_fill_str` helper, scalars use the + // single-word `__rt_array_fill` / `__rt_array_fill_refcounted` helpers. This must match + // the checker (`src/types/checker/builtins/arrays.rs`) and `infer_local_type`. + if !start_is_literal_zero { + return Some(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Mixed), + }); + } + Some(PhpType::Array(Box::new(array_fill_indexed_element_type(value_ty)))) +} + +/// Returns the EIR result metadata for `array_map()` when a callable param signature is known. +fn array_map_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + operands: &[crate::ir::ValueId], +) -> Option { + if args.len() != 2 { + return None; + } + let callback_sig = callable_expr_signature(ctx, &args[0])?; + let return_ty = normalize_value_php_type(callback_sig.return_type.codegen_repr()); + if return_ty == PhpType::Mixed { + return None; + } + let array = operands.get(1)?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(_) => Some(PhpType::Array(Box::new(return_ty))), + _ => None, + } +} + +/// Returns the EIR result metadata for `iterator_to_array()` when preserve_keys is static. +fn iterator_to_array_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + operands: &[crate::ir::ValueId], +) -> Option { + let source = operands.first()?; + let preserve_keys = match args.get(1) { + Some(arg) => static_preserve_keys_expr(arg), + None => Some(true), + }; + preserve_keys + .map(|value| { + iterator_to_array_static_return_type( + &ctx.builder.value_php_type(*source).codegen_repr(), + value, + ) + }) + .or(Some(PhpType::Mixed)) +} + +/// Computes the concrete `iterator_to_array()` container type for one preserve_keys value. +fn iterator_to_array_static_return_type(source_ty: &PhpType, preserve_keys: bool) -> PhpType { + match source_ty.codegen_repr() { + PhpType::Array(elem_ty) => PhpType::Array(elem_ty), + PhpType::AssocArray { key, value } if preserve_keys => PhpType::AssocArray { key, value }, + PhpType::AssocArray { value, .. } => PhpType::Array(value), + _ if preserve_keys => PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, + _ => PhpType::Array(Box::new(PhpType::Mixed)), + } +} + +/// Evaluates literal PHP truthiness used by static `iterator_to_array()` preserve_keys. +fn static_preserve_keys_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::BoolLiteral(value) => Some(*value), + ExprKind::IntLiteral(value) => Some(*value != 0), + ExprKind::FloatLiteral(value) => Some(*value != 0.0), + ExprKind::StringLiteral(value) => Some(!value.is_empty() && value != "0"), + ExprKind::Null => Some(false), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => Some(*value != 0), + ExprKind::FloatLiteral(value) => Some(*value != 0.0), + _ => None, + }, + _ => None, + } +} + +/// Resolves callable expression metadata tracked during type checking and lowering. +fn callable_expr_signature<'a>( + ctx: &'a LoweringContext<'_, '_>, + callback: &Expr, +) -> Option<&'a FunctionSig> { + match &callback.kind { + ExprKind::Variable(name) => ctx.callable_param_signature(name), + _ => None, + } +} + +/// Returns precise return metadata for pointer-extension builtins. +fn pointer_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + operands: &[crate::ir::ValueId], +) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "ptr" => Some(PhpType::Pointer(None)), + "ptr_null" => Some(PhpType::Pointer(None)), + "ptr_is_null" => Some(PhpType::Bool), + "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" | "ptr_sizeof" => { + Some(PhpType::Int) + } + "ptr_read_string" => Some(PhpType::Str), + "ptr_set" | "ptr_write8" | "ptr_write16" | "ptr_write32" => Some(PhpType::Void), + "ptr_write_string" => Some(PhpType::Int), + "ptr_offset" => { + let pointer = operands.first()?; + match ctx.builder.value_php_type(*pointer).codegen_repr() { + PhpType::Pointer(tag) => Some(PhpType::Pointer(tag)), + _ => Some(PhpType::Pointer(None)), + } + } + "zval_pack" => Some(PhpType::Pointer(None)), + "zval_unpack" => Some(PhpType::Mixed), + "zval_type" => Some(PhpType::Int), + "zval_free" => Some(PhpType::Void), + _ => None, + } +} + +/// Returns precise return metadata for numeric builtins whose result depends on operands. +fn numeric_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + operands: &[crate::ir::ValueId], +) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "abs" => { + let value = operands.first()?; + let ty = ctx.builder.value_php_type(*value).codegen_repr(); + Some(abs_builtin_return_type(&ty)) + } + "min" | "max" => { + let mut saw_float = false; + for value in operands { + match ctx.builder.value_php_type(*value).codegen_repr() { + PhpType::Float => saw_float = true, + PhpType::Int | PhpType::Bool => {} + _ => return Some(PhpType::Mixed), + } + } + Some(if saw_float { + PhpType::Float + } else { + PhpType::Int + }) + } + "clamp" => { + let mut saw_float = false; + let mut all_int = operands.len() == 3; + let mut all_string = operands.len() == 3; + let mut all_numeric = operands.len() == 3; + for value in operands.iter().take(3) { + match ctx.builder.value_php_type(*value).codegen_repr() { + PhpType::Int => { + all_string = false; + } + PhpType::Float => { + saw_float = true; + all_int = false; + all_string = false; + } + PhpType::Str => { + all_int = false; + all_numeric = false; + } + _ => { + all_int = false; + all_string = false; + all_numeric = false; + } + } + } + if all_string { + Some(PhpType::Str) + } else if all_int { + Some(PhpType::Int) + } else if all_numeric { + Some(if saw_float { + PhpType::Float + } else { + PhpType::Int + }) + } else { + Some(PhpType::Mixed) + } + } + _ => None, + } +} + +/// Returns the EIR storage type for `abs()` after operand-sensitive narrowing. +fn abs_builtin_return_type(ty: &PhpType) -> PhpType { + match ty { + PhpType::Float => PhpType::Float, + PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, + _ => PhpType::Int, + } +} + +/// Returns EIR result metadata for `pathinfo()` based on argument shape. +fn pathinfo_builtin_return_type(name: &str, operands: &[crate::ir::ValueId]) -> Option { + if php_symbol_key(name.trim_start_matches('\\')).as_str() != "pathinfo" { + return None; + } + if operands.len() == 1 { + return Some(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }); + } + Some(PhpType::Mixed) +} + +/// Returns precise EIR result metadata for regex builtins lowered by `codegen`. +fn regex_builtin_return_type(name: &str) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "preg_match" | "preg_match_all" => Some(PhpType::Int), + "preg_replace" => Some(PhpType::Str), + "preg_split" => Some(PhpType::Array(Box::new(PhpType::Mixed))), + _ => None, + } +} + +/// Returns precise return metadata for array builtins that preserve operand element type. +fn array_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + operands: &[crate::ir::ValueId], +) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "array_combine" => array_combine_builtin_return_type(ctx, operands), + "array_column" => array_column_builtin_return_type(ctx, operands), + "array_flip" => array_flip_builtin_return_type(ctx, operands), + "array_fill" => array_fill_builtin_return_type(ctx, operands), + "array_fill_keys" => array_fill_keys_builtin_return_type(ctx, operands), + "array_merge" => array_merge_builtin_return_type(ctx, operands), + "array_splice" | "array_filter" | "array_diff" | "array_intersect" | "array_diff_key" + | "array_intersect_key" => array_preserve_first_builtin_return_type(ctx, operands), + "in_array" => Some(PhpType::Bool), + "array_is_list" => Some(PhpType::Bool), + "array_key_first" | "array_key_last" => Some(PhpType::Mixed), + // `array_keys`/`array_slice` produce a fresh indexed array of boxed `Mixed` + // payloads (keys, or the sliced elements). These mirror the result type the + // first-class-callable fallback supplied before they were registered as + // builtins, so the EIR backend keeps receiving a concrete `Array` result + // type rather than the registry's `Mixed` return-type placeholder. + "array_keys" | "array_slice" => Some(PhpType::Array(Box::new(PhpType::Mixed))), + "range" => Some(PhpType::Array(Box::new(PhpType::Int))), + "array_values" => { + let array = operands.first()?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(elem) => Some(PhpType::Array(elem)), + PhpType::AssocArray { value, .. } => Some(PhpType::Array(value)), + other => Some(other), + } + } + "array_reverse" | "array_unique" | "array_pad" => { + let array = operands.first()?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(elem) => Some(PhpType::Array(elem)), + other => Some(other), + } + } + "array_chunk" => { + let array = operands.first()?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(elem) => Some(PhpType::Array(Box::new(PhpType::Array(elem)))), + other => Some(other), + } + } + "array_replace" | "array_replace_recursive" | "array_diff_assoc" + | "array_intersect_assoc" => two_input_hash_builtin_return_type(ctx, operands), + "array_merge_recursive" => array_merge_recursive_builtin_return_type(ctx, operands), + "array_find" => Some(PhpType::Mixed), + "array_any" | "array_all" => Some(PhpType::Bool), + "array_multisort" => Some(PhpType::Bool), + "array_walk_recursive" => Some(PhpType::Void), + "array_udiff" | "array_uintersect" => { + array_preserve_first_builtin_return_type(ctx, operands) + } + _ => None, + } +} + +/// Returns the hash result metadata for the two-input hash builtins (`array_replace`, +/// `array_replace_recursive`, `array_diff_assoc`, `array_intersect_assoc`). +/// +/// Mirrors the type checker's `two_input_hash_result`: the key and value each widen to `Mixed` +/// when the two operands disagree, so the result hash dispatches keys/values correctly at runtime. +fn two_input_hash_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let first = operands.first()?; + let second = operands.get(1)?; + let t1 = ctx.builder.value_php_type(*first).codegen_repr(); + let t2 = ctx.builder.value_php_type(*second).codegen_repr(); + Some(PhpType::two_input_hash_result(&t1, &t2)) +} + +/// Returns the hash result metadata for `array_merge_recursive(a, b)`. +/// +/// Scalar collisions combine into lists, so the value type is always `Mixed`; the key widens to +/// `Mixed` when the two operands disagree, matching the type checker. +fn array_merge_recursive_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let first = operands.first()?; + let second = operands.get(1)?; + let t1 = ctx.builder.value_php_type(*first).codegen_repr(); + let t2 = ctx.builder.value_php_type(*second).codegen_repr(); + Some(PhpType::AssocArray { + key: Box::new(PhpType::widen(t1.hash_key_type(), t2.hash_key_type())), + value: Box::new(PhpType::Mixed), + }) +} + +/// Returns precise return metadata for `array_fill(start, count, value)`. +fn array_fill_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let value = operands.get(2)?; + let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); + Some(PhpType::Array(Box::new(array_fill_indexed_element_type(value_ty)))) +} + +/// Returns the indexed element storage type for EIR `array_fill()` results. +fn array_fill_indexed_element_type(value_ty: PhpType) -> PhpType { + match value_ty.codegen_repr() { + PhpType::Void | PhpType::Never => PhpType::Mixed, + other => other, + } +} + +/// Returns the extracted column element type for `array_column()`. +fn array_column_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let array = operands.first()?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(inner) => match inner.codegen_repr() { + PhpType::AssocArray { value, .. } => Some(PhpType::Array(value)), + other => Some(other), + }, + other => Some(other), + } +} + +/// Returns precise return metadata for array builtins that preserve the first operand type. +fn array_preserve_first_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let first = operands.first()?; + Some(ctx.builder.value_php_type(*first).codegen_repr()) +} + +/// Returns precise return metadata for `array_fill_keys(keys, value)`. +fn array_fill_keys_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let keys = operands.first()?; + let value = operands.get(1)?; + let key_ty = match ctx.builder.value_php_type(*keys).codegen_repr() { + PhpType::Array(elem) => array_key_type_from_value_type(elem.codegen_repr()), + _ => return None, + }; + let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); + Some(PhpType::AssocArray { + key: Box::new(key_ty), + value: Box::new(value_ty), + }) +} + +/// Returns precise return metadata for `array_flip(array)`. +fn array_flip_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let array = operands.first()?; + match ctx.builder.value_php_type(*array).codegen_repr() { + PhpType::Array(value) => Some(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(value.codegen_repr())), + value: Box::new(PhpType::Int), + }), + PhpType::AssocArray { key, value } => Some(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(value.codegen_repr())), + value: key, + }), + _ => None, + } +} + +/// Returns precise return metadata for `array_combine(keys, values)`. +fn array_combine_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let keys = operands.first()?; + let values = operands.get(1)?; + let key_ty = match ctx.builder.value_php_type(*keys).codegen_repr() { + PhpType::Array(elem) => array_key_type_from_value_type(elem.codegen_repr()), + _ => return None, + }; + let value_ty = match ctx.builder.value_php_type(*values).codegen_repr() { + PhpType::Array(elem) => elem.codegen_repr(), + _ => return None, + }; + Some(PhpType::AssocArray { + key: Box::new(key_ty), + value: Box::new(value_ty), + }) +} + +/// Returns precise return metadata for `array_merge()`. +/// +/// Empty indexed arrays lower as `Array`; when that is the first operand, the merged +/// array inherits the second operand's element metadata so later indexed reads materialize +/// real payload values instead of void sentinels. +fn array_merge_builtin_return_type( + ctx: &LoweringContext<'_, '_>, + operands: &[crate::ir::ValueId], +) -> Option { + let first = operands.first()?; + let first_ty = ctx.builder.value_php_type(*first).codegen_repr(); + let second_ty = operands + .get(1) + .map(|value| ctx.builder.value_php_type(*value).codegen_repr()); + match first_ty { + PhpType::Array(elem) if is_empty_array_element_type(elem.as_ref()) => match second_ty { + Some(PhpType::Array(right)) if is_scalar_merge_element_type(right.as_ref()) => { + Some(PhpType::Array(right)) + } + _ => Some(PhpType::Array(elem)), + }, + PhpType::Array(elem) => Some(PhpType::Array(elem)), + other => Some(other), + } +} + +/// Returns true for the element sentinel used by statically empty indexed arrays. +fn is_empty_array_element_type(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Void) +} + +/// Returns true for element types copied safely by the scalar merge runtime helper. +fn is_scalar_merge_element_type(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Callable | PhpType::Void + ) +} + +/// Returns precise builtin return types needed by EIR value materialization. +fn builtin_return_type_override(name: &str) -> Option { + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "chdir" | "checkdate" | "chgrp" | "chmod" | "chown" | "lchgrp" | "lchown" + | "class_alias" | "class_exists" | "copy" | "define" | "defined" + | "empty" | "file_exists" | "fnmatch" | "function_exists" | "is_a" | "is_callable" + | "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_object" | "is_real" | "is_scalar" | "is_string" + | "fdatasync" | "fflush" | "flock" | "fsync" | "ftruncate" | "interface_exists" + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_numeric" | "link" + | "method_exists" | "mkdir" | "property_exists" | "rename" + | "enum_exists" | "trait_exists" | "putenv" | "rmdir" | "is_readable" + | "is_subclass_of" | "is_writeable" | "is_writable" | "settype" + | "is_resource" | "hash_equals" | "hash_update" | "spl_autoload_register" + | "spl_autoload_unregister" | "stream_context_set_option" | "stream_context_set_params" + | "stream_filter_register" | "stream_filter_remove" | "__elephc_phar_set_compression" + | "__elephc_phar_set_metadata" | "__elephc_phar_set_stub" + | "__elephc_phar_set_file_metadata" + | "__elephc_phar_sign_openssl" | "__elephc_phar_sign_hash" + | "__elephc_phar_set_zip_password" + | "stream_wrapper_register" | "stream_wrapper_restore" | "stream_wrapper_unregister" + | "stream_isatty" | "stream_is_local" | "stream_set_blocking" | "stream_set_timeout" + | "stream_socket_enable_crypto" | "stream_socket_shutdown" | "stream_supports_lock" | "symlink" | "touch" + | "unlink" => { + Some(PhpType::Bool) + } + "basename" | "date" | "gmdate" | "dirname" | "exec" | "get_class" | "get_parent_class" + | "getcwd" | "getenv" | "gethostname" | "gethostbyname" | "php_uname" + | "readline" | "shell_exec" | "sys_get_temp_dir" + | "fread" | "get_resource_type" | "gzcompress" | "gzdeflate" | "hash" | "hash_final" | "hash_hmac" | "long2ip" + | "stream_get_line" | "system" | "spl_autoload_extensions" | "strval" | "tempnam" | "vsprintf" + | "__elephc_phar_get_metadata" | "__elephc_phar_get_stub" + | "__elephc_phar_get_file_metadata" + | "__elephc_phar_gzip_archive" | "__elephc_phar_bzip2_archive" + | "__elephc_phar_decompress_archive" + | "__elephc_phar_get_signature_hash" | "__elephc_phar_get_signature_type" => { + Some(PhpType::Str) + } + "disk_free_space" | "disk_total_space" => Some(PhpType::Float), + "clearstatcache" | "closedir" | "exit" | "die" | "passthru" | "rewinddir" + | "stream_bucket_append" | "stream_bucket_prepend" | "unset" => Some(PhpType::Void), + "fclose" | "feof" | "rewind" => Some(PhpType::Bool), + "printf" | "array_rand" | "array_unshift" | "file_put_contents" | "filemtime" + | "filesize" | "fprintf" | "fpassthru" | "fputcsv" | "fseek" | "ftell" | "fwrite" + | "crc32" | "get_resource_id" | "isset" | "linkinfo" | "mktime" | "gmmktime" | "sleep" + | "__elephc_mktime_raw" | "__elephc_gmmktime_raw" + | "pclose" | "spl_object_id" | "stream_select" | "stream_set_chunk_size" + | "stream_set_read_buffer" | "stream_set_write_buffer" + | "__elephc_strtotime_raw" | "time" + | "umask" | "vfprintf" | "vprintf" | "realpath_cache_size" => { + Some(PhpType::Int) + } + // strtotime() is `int|false`: a real timestamp (including a valid -1 pre-epoch) on success, + // or boolean false when the string cannot be parsed. The backend boxes the result so + // `=== false` and `echo` observe the distinct false; `__elephc_strtotime_raw` (the DateTime + // internal alias above) stays a plain Int that maps the failure sentinel to -1. + "strtotime" => Some(PhpType::Union(vec![PhpType::Int, PhpType::False])), + // microtime() with a non-literal `as_float` flag yields `string|float` (boxed `Mixed`): + // the runtime branches on the flag and boxes either the "0.NNNNNNNN sec" string or the + // float. Literal-true / literal-false / omitted cases are resolved earlier by + // `call_return_type_for_args` (Float / Str), so this entry is only reached for a + // non-literal flag. + "microtime" => Some(PhpType::Union(vec![PhpType::Str, PhpType::Float])), + "spl_object_hash" => Some(PhpType::Str), + "spl_autoload" | "spl_autoload_call" | "usleep" => Some(PhpType::Void), + "stream_context_create" | "stream_context_get_default" | "stream_context_set_default" => { + Some(PhpType::stream_resource()) + } + "realpath_cache_get" | "stream_context_get_options" | "stream_context_get_params" + | "stream_get_meta_data" => Some(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }), + "getdate" | "localtime" | "hrtime" | "file_get_contents" | "fileatime" | "filectime" | "filegroup" | "fileinode" + | "fileowner" | "fileperms" | "filetype" | "readfile" | "readlink" | "realpath" + | "fgetc" | "fgets" | "fopen" | "fstat" | "hash_copy" | "hash_file" | "hash_init" + | "gethostbyaddr" | "getprotobyname" | "getprotobynumber" | "getservbyname" + | "getservbyport" | "fsockopen" | "inet_ntop" | "inet_pton" | "ip2long" | "opendir" + | "pfsockopen" | "readdir" | "popen" | "stat" | "lstat" | "stream_get_contents" + | "stream_bucket_make_writeable" | "stream_bucket_new" | "stream_filter_append" + | "stream_filter_prepend" | "stream_resolve_include_path" | "stream_socket_accept" + | "stream_socket_client" | "stream_socket_pair" | "stream_copy_to_stream" + | "stream_socket_get_name" | "stream_socket_recvfrom" | "stream_socket_sendto" + | "stream_socket_server" | "tmpfile" | "gzinflate" | "gzuncompress" | "strpos" | "strrpos" => { + Some(PhpType::Mixed) + } + "spl_autoload_functions" => Some(PhpType::Array(Box::new(PhpType::Int))), + "__elephc_phar_list_entries" + | "class_attribute_names" + | "explode" + | "fgetcsv" + | "file" + | "get_declared_classes" + | "fscanf" + | "get_declared_interfaces" + | "get_declared_traits" + | "glob" + | "hash_algos" + | "scandir" + | "spl_classes" + | "str_split" + | "stream_get_filters" + | "stream_get_transports" + | "stream_get_wrappers" + | "sscanf" => Some(PhpType::Array(Box::new(PhpType::Str))), + "class_attribute_args" => Some(PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }), + "class_get_attributes" => Some(PhpType::Array(Box::new(PhpType::Object( + "ReflectionAttribute".to_string(), + )))), + _ => None, + } +} + +/// Distinguishes pre-lowered array-literal items between plain elements and spread operands. +enum SpreadItem { + Element(LoweredValue), + Spread(LoweredValue), +} + +/// Lowers an indexed array literal. +fn lower_array_literal(ctx: &mut LoweringContext<'_, '_>, items: &[Expr], expr: &Expr) -> LoweredValue { + // Fast path: literals without any spread keep the original dest-first lowering so the + // common `[1, 2, 3]` form does not reorder allocation relative to element evaluation. + if !items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { + let array_ty = array_literal_type_for_ir(ctx, items, expr); + return lower_array_literal_without_spread(ctx, items, expr, array_ty); + } + // Spread-containing literals: lower every item value in source order first so PHP-visible side + // effects happen in order, then inspect each spread source's actual IR type to decide whether + // the destination must be associative (hash) storage. Dest allocation is pure, so emitting it + // after source evaluation preserves observable behavior. + let mut lowered: Vec = Vec::with_capacity(items.len()); + let mut any_assoc_spread = false; + for item in items { + match &item.kind { + ExprKind::Spread(inner) => { + let source = lower_expr(ctx, inner); + if matches!( + ctx.builder.value_php_type(source.value).codegen_repr(), + PhpType::AssocArray { .. } + ) { + any_assoc_spread = true; + } + lowered.push(SpreadItem::Spread(source)); + } + _ => { + let value = lower_expr(ctx, item); + lowered.push(SpreadItem::Element(value)); + } + } + } + if any_assoc_spread { + lower_array_literal_as_hash_from_lowered(ctx, items, &lowered, expr) + } else { + lower_array_literal_as_indexed_from_lowered(ctx, items, &lowered, expr) + } +} + +/// Lowers an indexed array literal using a contextual element storage type. +pub(crate) fn lower_array_literal_with_expected_type( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, + elem_ty: PhpType, +) -> LoweredValue { + let ExprKind::ArrayLiteral(items) = &expr.kind else { + return lower_expr(ctx, expr); + }; + if items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { + return lower_array_literal(ctx, items, expr); + } + let array_ty = expected_indexed_array_literal_type(elem_ty); + lower_array_literal_without_spread(ctx, items, expr, array_ty) +} + +/// Returns an indexed-array type for contextual literal lowering. +fn expected_indexed_array_literal_type(elem_ty: PhpType) -> PhpType { + PhpType::Array(Box::new(elem_ty.codegen_repr())) +} + +/// Lowers a no-spread indexed array literal into the requested array storage type. +fn lower_array_literal_without_spread( + ctx: &mut LoweringContext<'_, '_>, + items: &[Expr], + expr: &Expr, + array_ty: PhpType, +) -> LoweredValue { + let elem_ty = indexed_array_literal_element_type(&array_ty); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty, + Op::ArrayNew.default_effects(), + Some(expr.span), + ); + for item in items { + let value = lower_expr(ctx, item); + let value = coerce_array_literal_element_to_storage_type(ctx, value, elem_ty.as_ref(), item); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); + } + array +} + +/// Coerces an array literal element to the contextual storage type when needed. +fn coerce_array_literal_element_to_storage_type( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + elem_ty: Option<&PhpType>, + expr: &Expr, +) -> LoweredValue { + let Some(elem_ty) = elem_ty else { + return value; + }; + let coerced = match elem_ty.codegen_repr() { + PhpType::Int | PhpType::Bool if value.ir_type != IrType::I64 => { + coerce_to_int(ctx, value, expr) + } + PhpType::Float if value.ir_type != IrType::F64 => coerce_to_float(ctx, value, expr), + PhpType::Str if value.ir_type != IrType::Str => coerce_to_string(ctx, value, expr), + _ => value, + }; + if coerced.value != value.value && ctx.value_is_owning_temporary(value) { + crate::ir_lower::ownership::release_if_owned(ctx, value, Some(expr.span)); + } + coerced +} + +/// Lowers a spread-containing indexed-array literal whose spread sources are all indexed arrays. +fn lower_array_literal_as_indexed_from_lowered( + ctx: &mut LoweringContext<'_, '_>, + items: &[Expr], + lowered: &[SpreadItem], + expr: &Expr, +) -> LoweredValue { + let array_ty = array_literal_type_for_ir(ctx, items, expr); + let elem_ty = indexed_array_literal_element_type(&array_ty); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty, + Op::ArrayNew.default_effects(), + Some(expr.span), + ); + for (item, value) in items.iter().zip(lowered.iter()) { + match value { + SpreadItem::Spread(source) => { + lower_indexed_array_spread_into_array(ctx, array, *source, elem_ty.as_ref(), item.span); + } + SpreadItem::Element(value) => { + ctx.emit_void(Op::ArrayPush, vec![array.value, value.value], None, Op::ArrayPush.default_effects(), Some(item.span)); + super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), *value, item.span); + } + } + } + array +} + +/// Lowers a spread-containing array literal with at least one associative spread as a hash. +fn lower_array_literal_as_hash_from_lowered( + ctx: &mut LoweringContext<'_, '_>, + items: &[Expr], + lowered: &[SpreadItem], + expr: &Expr, +) -> LoweredValue { + let hash_ty = assoc_array_literal_type_from_spreads(ctx, items, expr); + let value_ty = match hash_ty.codegen_repr() { + PhpType::AssocArray { value, .. } => value.codegen_repr(), + _ => PhpType::Mixed, + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(expr.span), + ); + for (item, value) in items.iter().zip(lowered.iter()) { + match value { + SpreadItem::Spread(source) => { + lower_hash_spread_into_hash_from_value(ctx, hash, *source, item.span); + } + SpreadItem::Element(value) => { + ctx.emit_void( + Op::RuntimeCall, + vec![hash.value, value.value], + None, + effects_lookup::runtime_effects(), + Some(item.span), + ); + release_value_after_retaining_insert(ctx, Some(&value_ty), *value, item.span); + } + } + } + hash +} + +/// Lowers a single already-lowered spread operand into a hash destination, handling both +/// associative and indexed source storage. Associative sources flatten directly through +/// `__rt_hash_spread`; indexed sources are first promoted to hash storage so the same +/// reindexing path applies. +fn lower_hash_spread_into_hash_from_value( + ctx: &mut LoweringContext<'_, '_>, + hash: LoweredValue, + source: LoweredValue, + span: crate::span::Span, +) { + let source_is_hash = matches!( + ctx.builder.value_php_type(source.value).codegen_repr(), + PhpType::AssocArray { .. } + ); + let spread_source = if source_is_hash { + source + } else { + let promoted = ctx.emit_value( + Op::ArrayToHash, + vec![source.value], + None, + PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Mixed), + }, + Op::ArrayToHash.default_effects(), + Some(span), + ); + LoweredValue { + value: promoted.value, + ir_type: IrType::Heap(IrHeapKind::Hash), + } + }; + ctx.emit_void( + Op::HashSpread, + vec![hash.value, spread_source.value], + None, + Op::HashSpread.default_effects(), + Some(span), + ); + if ctx.value_is_owning_temporary(spread_source) { + crate::ir_lower::ownership::release_if_owned(ctx, spread_source, Some(span)); + } +} + +/// Lowers an indexed-array spread by appending each source element to the destination. +fn lower_indexed_array_spread_into_array( + ctx: &mut LoweringContext<'_, '_>, + array: LoweredValue, + source: LoweredValue, + container_elem_ty: Option<&PhpType>, + span: crate::span::Span, +) { + let source_elem_ty = match ctx.builder.value_php_type(source.value).codegen_repr() { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Mixed, + }; + let len = ctx.emit_value( + Op::ArrayLen, + vec![source.value], + None, + PhpType::Int, + Op::ArrayLen.default_effects(), + Some(span), + ); + let zero = emit_i64_at_span(ctx, 0, span); + let header = ctx.builder.create_named_block("array.spread.next", vec![(IrType::I64, PhpType::Int)]); + let body = ctx.builder.create_named_block("array.spread.body", Vec::new()); + let exit = ctx.builder.create_named_block("array.spread.exit", Vec::new()); + ctx.builder.terminate(Terminator::Br { target: header, args: vec![zero.value] }); + + ctx.builder.position_at_end(header); + let index = ctx.builder.block_param(header, 0); + let has_next = ctx.emit_value( + Op::ICmp, + vec![index, len.value], + Some(Immediate::CmpPredicate(CmpPredicate::Slt)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: has_next.value, + then_target: body, + then_args: Vec::new(), + else_target: exit, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(body); + let value = ctx.emit_value( + Op::ArrayGet, + vec![source.value, index], + None, + source_elem_ty, + Op::ArrayGet.default_effects(), + Some(span), + ); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(span), + ); + super::stmt::release_indexed_array_write_operand(ctx, container_elem_ty, value, span); + let one = emit_i64_at_span(ctx, 1, span); + let next = ctx.emit_value( + Op::IAdd, + vec![index, one.value], + None, + PhpType::Int, + Op::IAdd.default_effects(), + Some(span), + ); + ctx.builder.terminate(Terminator::Br { target: header, args: vec![next.value] }); + + ctx.builder.position_at_end(exit); + if ctx.value_is_owning_temporary(source) { + crate::ir_lower::ownership::release_if_owned(ctx, source, Some(span)); + } +} + +/// Emits an integer constant at a specific source span. +fn emit_i64_at_span( + ctx: &mut LoweringContext<'_, '_>, + value: i64, + span: crate::span::Span, +) -> LoweredValue { + ctx.emit_value( + Op::ConstI64, + Vec::new(), + Some(Immediate::I64(value)), + PhpType::Int, + Op::ConstI64.default_effects(), + Some(span), + ) +} + +/// Returns the element type from an indexed-array literal type. +fn indexed_array_literal_element_type(array_ty: &PhpType) -> Option { + match array_ty.codegen_repr() { + PhpType::Array(elem) => Some(elem.codegen_repr()), + _ => None, + } +} + +/// Releases an inserted temporary when the container retained or copied its payload. +/// Callable arrays keep raw descriptor pointers today, so the inserted owner stays alive. +fn release_value_after_retaining_insert( + ctx: &mut LoweringContext<'_, '_>, + container_elem_ty: Option<&PhpType>, + value: LoweredValue, + span: crate::span::Span, +) { + if matches!( + container_elem_ty.map(PhpType::codegen_repr), + Some(PhpType::Mixed | PhpType::Callable) + ) { + return; + } + if ctx.value_is_owning_temporary(value) { + crate::ir_lower::ownership::release_if_owned(ctx, value, Some(span)); + } +} + +/// Returns the indexed-array type that the EIR backend can faithfully materialize. +fn array_literal_type_for_ir( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], + expr: &Expr, +) -> PhpType { + if items.is_empty() { + return fallback_expr_type(expr); + } + let mut elem_ty = array_literal_element_type_for_ir(ctx, &items[0]); + for item in items.iter().skip(1) { + elem_ty = merge_ir_indexed_element_type( + elem_ty, + array_literal_element_type_for_ir(ctx, item), + ); + } + PhpType::Array(Box::new(elem_ty)) +} + +/// Returns the best EIR storage element type for one indexed-array literal item. +fn array_literal_element_type_for_ir( + ctx: &LoweringContext<'_, '_>, + item: &Expr, +) -> PhpType { + match &item.kind { + ExprKind::Null => PhpType::Mixed, + ExprKind::Spread(inner) => match array_literal_element_type_for_ir(ctx, inner).codegen_repr() { + PhpType::Array(elem) => elem.codegen_repr(), + _ => PhpType::Mixed, + }, + ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, item).codegen_repr(), + ExprKind::ArrayLiteralAssoc(pairs) => assoc_array_literal_type_for_ir(ctx, pairs, item), + ExprKind::ConstRef(name) => ctx + .constant_value(name.as_str()) + .map(|(_, ty)| ir_array_storage_type(ty)) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), + ExprKind::Variable(name) => ir_array_storage_type( + ctx.local_types + .get(name) + .cloned() + .unwrap_or_else(|| infer_expr_type_syntactic(item)), + ), + ExprKind::FunctionCall { name, .. } => { + let canonical = name.as_str(); + if let Some(sig) = ctx.functions.get(canonical) { + return ir_array_storage_type(sig.return_type.clone()); + } + if let Some(sig) = ctx.extern_functions.get(canonical) { + return ir_array_storage_type(sig.return_type.clone()); + } + ir_array_storage_type(infer_expr_type_syntactic(item)) + } + ExprKind::ArrayAccess { array, .. } => array_access_expr_value_type_for_ir(ctx, array) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), + ExprKind::PropertyAccess { object, property } => property_access_expr_type_for_ir( + ctx, + object, + property, + ) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), + _ => ir_array_storage_type(infer_expr_type_syntactic(item)), + } +} + +/// Returns the EIR array storage metadata type, preserving PHP resources. +fn ir_array_storage_type(php_type: PhpType) -> PhpType { + let php_type = normalize_value_php_type(php_type); + if matches!(php_type, PhpType::Resource(_)) { + php_type + } else { + php_type.codegen_repr() + } +} + +/// Merges indexed-array element types for EIR storage metadata. +fn merge_ir_indexed_element_type(left: PhpType, right: PhpType) -> PhpType { + if left == right { + return left; + } + if matches!(left.codegen_repr(), PhpType::Void | PhpType::Never) { + return right; + } + if matches!(right.codegen_repr(), PhpType::Void | PhpType::Never) { + return left; + } + PhpType::Mixed +} + +/// Lowers an associative array literal. +fn lower_assoc_array_literal(ctx: &mut LoweringContext<'_, '_>, pairs: &[(Expr, Expr)], expr: &Expr) -> LoweredValue { + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(pairs.len() as u32)), + assoc_array_literal_type_for_ir(ctx, pairs, expr), + Op::HashNew.default_effects(), + Some(expr.span), + ); + for (key, value) in pairs { + let key = lower_expr(ctx, key); + let value = lower_expr(ctx, value); + ctx.emit_void(Op::HashSet, vec![hash.value, key.value, value.value], None, Op::HashSet.default_effects(), Some(expr.span)); + } + hash +} + +/// Returns the associative-array type for a literal that contains at least one associative +/// spread. Mirrors the type checker's `assoc_spread_literal_value_type` so EIR storage matches +/// the value types actually lowered into the hash. +fn assoc_array_literal_type_from_spreads( + ctx: &LoweringContext<'_, '_>, + items: &[Expr], + expr: &Expr, +) -> PhpType { + let mut value_ty = PhpType::Never; + for item in items { + let next = match &item.kind { + ExprKind::Spread(inner) => match infer_expr_type_syntactic(inner).codegen_repr() { + PhpType::Array(elem) => elem.codegen_repr(), + PhpType::AssocArray { value, .. } => value.codegen_repr(), + _ => PhpType::Mixed, + }, + _ => array_literal_element_type_for_ir(ctx, item).codegen_repr(), + }; + value_ty = merge_ir_assoc_value_type(value_ty, next); + } + if matches!(value_ty, PhpType::Never) { + return fallback_expr_type(expr); + } + PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(value_ty), + } +} + +/// Returns the associative-array type that the EIR backend can faithfully materialize. +fn assoc_array_literal_type_for_ir( + ctx: &LoweringContext<'_, '_>, + pairs: &[(Expr, Expr)], + expr: &Expr, +) -> PhpType { + if pairs.is_empty() { + return fallback_expr_type(expr); + } + let mut key_ty = normalized_array_key_type( + &pairs[0].0, + infer_expr_type_syntactic(&pairs[0].0), + ); + let mut value_ty = assoc_array_literal_value_type_for_ir(ctx, &pairs[0].1); + for (key, value) in pairs.iter().skip(1) { + key_ty = merge_array_key_types( + key_ty, + normalized_array_key_type(key, infer_expr_type_syntactic(key)), + ); + value_ty = merge_ir_assoc_value_type( + value_ty, + assoc_array_literal_value_type_for_ir(ctx, value), + ); + } + PhpType::AssocArray { + key: Box::new(key_ty), + value: Box::new(value_ty), + } +} + +/// Returns the best EIR storage value type for one associative-array literal value. +fn assoc_array_literal_value_type_for_ir( + ctx: &LoweringContext<'_, '_>, + value: &Expr, +) -> PhpType { + match &value.kind { + ExprKind::Null => PhpType::Mixed, + ExprKind::ConstRef(name) => ctx + .constant_value(name.as_str()) + .map(|(_, ty)| ir_array_storage_type(ty)) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), + // A class constant or enum case must be typed the way `lower_scoped_constant` + // resolves it, not by the syntactic `::class`-is-string default, or the hash + // value-type stamp would diverge from the lowered value and corrupt reads. + ExprKind::ScopedConstantAccess { receiver, name } => { + scoped_constant_value_type_for_ir(ctx, receiver, name, value) + } + ExprKind::Variable(name) => ir_array_storage_type( + ctx.local_types + .get(name) + .cloned() + .unwrap_or_else(|| infer_expr_type_syntactic(value)), + ), + ExprKind::FunctionCall { name, .. } => { + let canonical = name.as_str(); + if let Some(sig) = ctx.functions.get(canonical) { + return ir_array_storage_type(sig.return_type.clone()); + } + if let Some(sig) = ctx.extern_functions.get(canonical) { + return ir_array_storage_type(sig.return_type.clone()); + } + ir_array_storage_type(infer_expr_type_syntactic(value)) + } + ExprKind::ArrayAccess { array, .. } => array_access_expr_value_type_for_ir(ctx, array) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), + ExprKind::PropertyAccess { object, property } => property_access_expr_type_for_ir( + ctx, + object, + property, + ) + .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), + _ => ir_array_storage_type(infer_expr_type_syntactic(value)), + } +} + +/// Returns the EIR storage value type for a scoped-constant array value, +/// resolving a class/interface constant the same way `lower_scoped_constant` +/// lowers it so the hash value-type stamp matches the value actually stored +/// (rather than the syntactic `::class`-is-string default). Falls back to the +/// syntactic guess when the constant cannot be resolved. +fn scoped_constant_value_type_for_ir( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, + member: &str, + value: &Expr, +) -> PhpType { + let class_name = scoped_constant_receiver_name(ctx, receiver); + let normalized = class_name.trim_start_matches('\\'); + // An enum case lowers to the case *object* singleton (see `lower_scoped_constant`), + // so the hash must box it as a Mixed cell — stamp the value type Mixed to match. + if ctx + .enums + .get(normalized) + .is_some_and(|enum_info| enum_info.cases.iter().any(|case| case.name == member)) + { + return PhpType::Mixed; + } + if let Some(const_expr) = ctx.scoped_constant_value(&class_name, member) { + return ir_array_storage_type(infer_expr_type_syntactic(&const_expr)); + } + ir_array_storage_type(infer_expr_type_syntactic(value)) +} + +/// Returns the element/value type for an array-access expression used inside a literal. +fn array_access_expr_value_type_for_ir( + ctx: &LoweringContext<'_, '_>, + array: &Expr, +) -> Option { + let array_ty = match &array.kind { + ExprKind::Variable(name) => ctx.local_types.get(name).cloned(), + ExprKind::PropertyAccess { object, property } => { + property_access_expr_type_for_ir(ctx, object, property) + } + ExprKind::ArrayLiteral(items) => Some(array_literal_type_for_ir(ctx, items, array)), + ExprKind::ArrayLiteralAssoc(pairs) => Some(assoc_array_literal_type_for_ir(ctx, pairs, array)), + _ => None, + }? + .codegen_repr(); + match array_ty { + PhpType::Array(elem_ty) => { + Some(array_access_element_result_type(normalize_value_php_type(*elem_ty).codegen_repr())) + } + PhpType::AssocArray { value, .. } => { + Some(array_access_element_result_type(normalize_value_php_type(*value).codegen_repr())) + } + PhpType::Str => Some(PhpType::Str), + PhpType::Mixed | PhpType::Union(_) => Some(PhpType::Mixed), + _ => None, + } +} + +/// Returns the declared type for an object property expression used inside a literal. +fn property_access_expr_type_for_ir( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> Option { + let class_name = instance_callable_object_class(ctx, object)?; + let normalized = class_name.trim_start_matches('\\'); + if is_builtin_stdclass_name(normalized) { + return Some(PhpType::Mixed); + } + if let Some(property_ty) = runtime_property_type_override(ctx, normalized, property) { + return Some(normalize_value_php_type(property_ty)); + } + let class_info = ctx.classes.get(normalized)?; + class_info + .properties + .iter() + .find(|(name, _)| name == property) + .map(|(_, ty)| normalize_value_php_type(ty.codegen_repr())) +} + +/// Merges associative-array value types for EIR storage metadata. +fn merge_ir_assoc_value_type(left: PhpType, right: PhpType) -> PhpType { + if left == right { + return left; + } + if matches!(left, PhpType::Never) { + return right; + } + if matches!(right, PhpType::Never) { + return left; + } + PhpType::Mixed +} + +/// Lowers a match expression with lazy arm-result evaluation. +fn lower_match( + ctx: &mut LoweringContext<'_, '_>, + subject: &Expr, + arms: &[(Vec, Expr)], + default: Option<&Expr>, + expr: &Expr, +) -> LoweredValue { + let subject = lower_expr(ctx, subject); + let result_type = fallback_expr_type(expr); + let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); + let merge = ctx.builder.create_named_block("match.merge", Vec::new()); + + for (conditions, result) in arms { + let result_block = ctx.builder.create_named_block("match.result", Vec::new()); + let mut fallthrough = ctx.builder.insertion_block(); + for condition in conditions { + let next_test = ctx.builder.create_named_block("match.next", Vec::new()); + let condition = lower_expr(ctx, condition); + let matched = ctx.emit_value( + Op::StrictEq, + vec![subject.value, condition.value], + None, + PhpType::Bool, + Op::StrictEq.default_effects(), + Some(expr.span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: matched.value, + then_target: result_block, + then_args: Vec::new(), + else_target: next_test, + else_args: Vec::new(), + }); + ctx.builder.position_at_end(next_test); + fallthrough = Some(next_test); + } + ctx.builder.position_at_end(result_block); + store_expr_into_temp(ctx, &temp_name, result_type.clone(), result, expr.span); + branch_to(ctx, merge); + if let Some(fallthrough) = fallthrough { + ctx.builder.position_at_end(fallthrough); + } + } + if let Some(default) = default { + store_expr_into_temp(ctx, &temp_name, result_type.clone(), default, expr.span); + branch_to(ctx, merge); + } else if !ctx.builder.insertion_block_is_terminated() { + let message = ctx.intern_string("Fatal error: unhandled match case\n"); + ctx.builder.terminate(Terminator::Fatal { message }); + } + ctx.builder.position_at_end(merge); + take_owned_temp(ctx, &temp_name, expr.span) +} + +/// Lowers array, hash, string, or ArrayAccess indexing. +fn lower_array_access( + ctx: &mut LoweringContext<'_, '_>, + array: &Expr, + index: &Expr, + expr: &Expr, +) -> LoweredValue { + lower_array_access_with_missing_warning(ctx, array, index, expr, true) +} + +/// Lowers array, hash, string, or ArrayAccess indexing with configurable +/// undefined-offset warning behavior for native indexed-array reads. +fn lower_array_access_with_missing_warning( + ctx: &mut LoweringContext<'_, '_>, + array: &Expr, + index: &Expr, + expr: &Expr, + warn_on_missing: bool, +) -> LoweredValue { + let array_value = lower_expr(ctx, array); + if value_is_nullable(ctx, array_value.value) { + return lower_nullable_array_access(ctx, array_value, index, expr, warn_on_missing); + } + lower_array_access_from_value(ctx, array_value, index, expr, warn_on_missing) +} + +/// Lowers array access once the receiver is already evaluated. +fn lower_array_access_from_value( + ctx: &mut LoweringContext<'_, '_>, + array_value: LoweredValue, + index: &Expr, + expr: &Expr, + warn_on_missing: bool, +) -> LoweredValue { + let mut index_value = lower_expr(ctx, index); + let op = match array_value.ir_type { + IrType::Heap(IrHeapKind::Array) => { + let index_ty = index_expr_key_type(ctx, index); + if index_ty == PhpType::Int { + index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); + if warn_on_missing { + Op::ArrayGet + } else { + Op::ArrayGetSilent + } + } else { + // String or Mixed key on indexed storage: use the mixed-key + // runtime read path (mirrors Op::ArraySetMixedKey for writes). + if warn_on_missing { + Op::ArrayGetMixedKey + } else { + Op::ArrayGetMixedKeySilent + } + } + } + IrType::Heap(IrHeapKind::Hash) => Op::HashGet, + IrType::Heap(IrHeapKind::Buffer) => Op::BufferGet, + IrType::Str => { + index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); + Op::StrCharAt + } + _ => Op::RuntimeCall, + }; + let result_type = array_access_result_type(ctx, array_value.value, op, expr); + ctx.emit_value( + op, + vec![array_value.value, index_value.value], + None, + result_type, + op.default_effects(), + Some(expr.span), + ) +} + +/// Lowers nullable receiver indexing without evaluating the index on a null receiver. +fn lower_nullable_array_access( + ctx: &mut LoweringContext<'_, '_>, + array_value: LoweredValue, + index: &Expr, + expr: &Expr, + warn_on_missing: bool, +) -> LoweredValue { + let is_null = ctx.emit_value( + Op::IsNull, + vec![array_value.value], + None, + PhpType::Bool, + Op::IsNull.default_effects(), + Some(expr.span), + ); + let result_type = PhpType::Mixed; + let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); + let null_block = ctx + .builder + .create_named_block("nullable.index.null", Vec::new()); + let read_block = ctx + .builder + .create_named_block("nullable.index.read", Vec::new()); + let merge = ctx + .builder + .create_named_block("nullable.index.merge", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: is_null.value, + then_target: null_block, + then_args: Vec::new(), + else_target: read_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(null_block); + let null_value = lower_boxed_null(ctx, expr); + store_value_into_temp(ctx, &temp_name, result_type.clone(), null_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(read_block); + let read_value = lower_array_access_from_value(ctx, array_value, index, expr, warn_on_missing); + store_value_into_temp(ctx, &temp_name, result_type, read_value, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + take_owned_temp(ctx, &temp_name, expr.span) +} + +/// Returns the statically-known key type for an array index expression. +/// Used to decide between Op::ArrayGet (int key) and Op::ArrayGetMixedKey. +fn index_expr_key_type(_ctx: &LoweringContext<'_, '_>, index: &Expr) -> PhpType { + let ty = infer_expr_type_syntactic(index); + normalized_array_key_type(index, ty) +} + +/// Returns the best PHP result type for a lowered array/string/hash access. +fn array_access_result_type( + ctx: &LoweringContext<'_, '_>, + array: crate::ir::ValueId, + op: Op, + expr: &Expr, +) -> PhpType { + match op { + Op::StrCharAt => PhpType::Str, + Op::ArrayGet | Op::ArrayGetSilent => match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::Array(elem_ty) => { + array_access_element_result_type(normalize_value_php_type(*elem_ty)) + } + _ => fallback_expr_type(expr), + }, + Op::HashGet => match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::AssocArray { value, .. } => { + array_access_element_result_type(normalize_value_php_type(*value)) + } + _ => fallback_expr_type(expr), + }, + Op::BufferGet => match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::Buffer(elem_ty) => normalize_value_php_type(*elem_ty), + _ => fallback_expr_type(expr), + }, + Op::RuntimeCall => array_access_runtime_call_result_type(ctx, array, expr), + Op::ArrayGetMixedKey | Op::ArrayGetMixedKeySilent => PhpType::Mixed, + _ => match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, + _ => fallback_expr_type(expr), + }, + } +} + +/// Returns the materialized result type for a PHP array read, including miss-capable int reads. +pub(crate) fn array_access_element_result_type(element_ty: PhpType) -> PhpType { + if crate::codegen::sentinels::null_repr_is_tagged() && matches!(element_ty, PhpType::Int) { + PhpType::TaggedScalar + } else { + element_ty + } +} + +/// Returns the EIR result type for object indexing routed through `ArrayAccess::offsetGet`. +fn array_access_runtime_call_result_type( + ctx: &LoweringContext<'_, '_>, + array: crate::ir::ValueId, + expr: &Expr, +) -> PhpType { + match ctx.builder.value_php_type(array).codegen_repr() { + PhpType::Object(class_name) => array_access_offset_get_return_type(ctx, &class_name) + .unwrap_or_else(|| fallback_expr_type(expr)), + PhpType::Mixed => PhpType::Mixed, + _ => fallback_expr_type(expr), + } +} + +/// Looks up the effective `offsetGet` return type for an ArrayAccess class. +fn array_access_offset_get_return_type( + ctx: &LoweringContext<'_, '_>, + class_name: &str, ) -> Option { - let array = operands.first()?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(inner) => match inner.codegen_repr() { - PhpType::AssocArray { value, .. } => Some(PhpType::Array(value)), - other => Some(other), - }, - other => Some(other), + if !object_name_satisfies_interface_for_ir(ctx, class_name, "ArrayAccess") { + return None; + } + let method_key = php_symbol_key("offsetGet"); + class_method_return_type_for_ir(ctx, class_name, &method_key) + .or_else(|| interface_method_return_type_for_ir(ctx, "ArrayAccess", &method_key)) + .map(normalize_value_php_type) +} + +/// Returns true when a syntactic array receiver is statically known as `ArrayAccess`. +fn array_access_expr_satisfies_array_access( + ctx: &LoweringContext<'_, '_>, + array: &Expr, +) -> bool { + let ty = match &array.kind { + ExprKind::Variable(name) => ctx + .local_types + .get(name) + .cloned() + .unwrap_or_else(|| infer_expr_type_syntactic(array)), + _ => infer_expr_type_syntactic(array), + }; + type_satisfies_array_access_for_ir(ctx, &ty) +} + +/// Returns true when every possible object arm satisfies PHP's `ArrayAccess` interface. +pub(crate) fn type_satisfies_array_access_for_ir( + ctx: &LoweringContext<'_, '_>, + ty: &PhpType, +) -> bool { + match ty { + PhpType::Object(class_name) => { + object_name_satisfies_interface_for_ir(ctx, class_name, "ArrayAccess") + } + PhpType::Union(members) => { + let mut saw_object = false; + for member in members { + match member { + PhpType::Void | PhpType::Never => {} + other if type_satisfies_array_access_for_ir(ctx, other) => { + saw_object = true; + } + _ => return false, + } + } + saw_object + } + _ => false, + } +} + +/// Returns true when a class or interface name satisfies the requested interface. +fn object_name_satisfies_interface_for_ir( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + interface_name: &str, +) -> bool { + let normalized = class_name.trim_start_matches('\\'); + if php_symbol_key(normalized) == php_symbol_key(interface_name.trim_start_matches('\\')) { + return true; + } + if ctx.interfaces.contains_key(normalized) { + return interface_extends_interface_for_ir(ctx, normalized, interface_name); + } + class_implements_interface_for_ir(ctx, normalized, interface_name) +} + +/// Returns whether a lowered class implements an interface, following parents. +fn class_implements_interface_for_ir( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + interface_name: &str, +) -> bool { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + let mut current = Some(class_name.trim_start_matches('\\')); + while let Some(candidate) = current { + let Some(info) = ctx.classes.get(candidate) else { + return false; + }; + if info + .interfaces + .iter() + .any(|interface| { + let interface = interface.trim_start_matches('\\'); + php_symbol_key(interface) == interface_key + || interface_extends_interface_for_ir(ctx, interface, interface_name) + }) + { + return true; + } + current = info.parent.as_deref(); + } + false +} + +/// Returns true when an interface extends the requested ancestor interface. +fn interface_extends_interface_for_ir( + ctx: &LoweringContext<'_, '_>, + interface_name: &str, + ancestor_name: &str, +) -> bool { + if php_symbol_key(interface_name.trim_start_matches('\\')) + == php_symbol_key(ancestor_name.trim_start_matches('\\')) + { + return true; + } + let Some(info) = ctx.interfaces.get(interface_name.trim_start_matches('\\')) else { + return false; + }; + info.parents.iter().any(|parent| { + let parent = parent.trim_start_matches('\\'); + php_symbol_key(parent) == php_symbol_key(ancestor_name.trim_start_matches('\\')) + || interface_extends_interface_for_ir(ctx, parent, ancestor_name) + }) +} + +/// Returns a method return type from class metadata, following parent classes. +fn class_method_return_type_for_ir( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method_key: &str, +) -> Option { + let mut current = Some(class_name.trim_start_matches('\\')); + while let Some(candidate) = current { + let info = ctx.classes.get(candidate)?; + if let Some(sig) = info.methods.get(method_key) { + return Some(sig.return_type.clone()); + } + current = info.parent.as_deref(); + } + None +} + +/// Returns a method return type from interface metadata, following interface parents. +fn interface_method_return_type_for_ir( + ctx: &LoweringContext<'_, '_>, + interface_name: &str, + method_key: &str, +) -> Option { + let mut visited = std::collections::HashSet::new(); + let mut queue = vec![interface_name.trim_start_matches('\\').to_string()]; + while let Some(name) = queue.pop() { + if !visited.insert(name.clone()) { + continue; + } + let Some(info) = ctx.interfaces.get(&name) else { + continue; + }; + if let Some(sig) = info.methods.get(method_key) { + return Some(sig.return_type.clone()); + } + queue.extend(info.parents.iter().cloned()); + } + None +} + +/// Lowers a ternary expression with lazy branch evaluation. +fn lower_ternary( + ctx: &mut LoweringContext<'_, '_>, + condition: &Expr, + then_expr: &Expr, + else_expr: &Expr, + expr: &Expr, +) -> LoweredValue { + let cond = lower_expr(ctx, condition); + let cond = ctx.truthy(cond, Some(condition.span)); + let result_type = branch_merge_result_type(ctx, then_expr, else_expr, expr); + let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); + let split_initialized = ctx.initialized_slots_snapshot(); + let then_block = ctx.builder.create_named_block("ternary.then", Vec::new()); + let else_block = ctx.builder.create_named_block("ternary.else", Vec::new()); + let merge = ctx.builder.create_named_block("ternary.merge", Vec::new()); + ctx.builder.terminate(Terminator::CondBr { + cond: cond.value, + then_target: then_block, + then_args: Vec::new(), + else_target: else_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(then_block); + ctx.restore_initialized_slots(split_initialized.clone()); + store_expr_into_temp(ctx, &temp_name, result_type.clone(), then_expr, expr.span); + let then_reachable = !ctx.builder.insertion_block_is_terminated(); + let then_initialized = ctx.initialized_slots_snapshot(); + branch_to(ctx, merge); + + ctx.builder.position_at_end(else_block); + ctx.restore_initialized_slots(split_initialized.clone()); + store_expr_into_temp(ctx, &temp_name, result_type, else_expr, expr.span); + let else_reachable = !ctx.builder.insertion_block_is_terminated(); + let else_initialized = ctx.initialized_slots_snapshot(); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + ctx.restore_initialized_slots(merge_initialized_slots_for_expr( + &split_initialized, + then_initialized, + then_reachable, + else_initialized, + else_reachable, + )); + take_owned_temp(ctx, &temp_name, expr.span) +} + +/// Lowers a cast expression. +fn lower_cast(ctx: &mut LoweringContext<'_, '_>, target: &CastType, inner: &Expr, expr: &Expr) -> LoweredValue { + let value = lower_expr(ctx, inner); + let php_type = cast_php_type(target); + let result = ctx.emit_value( + Op::Cast, + vec![value.value], + Some(Immediate::CastTarget(value_ir_type(&php_type))), + php_type, + Op::Cast.default_effects(), + Some(expr.span), + ); + if matches!(target, CastType::String) { + release_stringified_source_if_owned(ctx, value, Some(expr.span)); + } + result +} + +/// Releases an owned source whose string result cannot alias the original storage. +fn release_stringified_source_if_owned( + ctx: &mut LoweringContext<'_, '_>, + source: LoweredValue, + span: Option, +) { + if !ctx.value_is_owning_temporary(source) { + return; + } + match ctx.builder.value_php_type(source.value).codegen_repr() { + PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + crate::ir_lower::ownership::release_if_owned(ctx, source, span); + } + _ => {} + } +} + +/// Returns the PHP type produced by a cast. +fn cast_php_type(target: &CastType) -> PhpType { + match target { + CastType::Int => PhpType::Int, + CastType::Float => PhpType::Float, + CastType::String => PhpType::Str, + CastType::Bool => PhpType::Bool, + CastType::Array => PhpType::Array(Box::new(PhpType::Mixed)), + } +} + +/// Lowers a closure expression into a callable descriptor backed by an EIR closure function. +fn lower_closure( + ctx: &mut LoweringContext<'_, '_>, + params: &[(String, Option, Option, bool)], + variadic: Option<&str>, + variadic_by_ref: bool, + return_type: Option<&TypeExpr>, + body: &[crate::parser::ast::Stmt], + captures: &[String], + capture_refs: &[String], + expr: &Expr, + is_static: bool, +) -> LoweredValue { + lower_closure_with_context( + ctx, + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + capture_refs, + expr, + &[], + None, + is_static, + ) +} + +/// Lowers a closure assigned to a local and specializes self by-reference captures as callable. +pub(crate) fn lower_closure_for_assignment( + ctx: &mut LoweringContext<'_, '_>, + assigned_name: &str, + value: &Expr, +) -> Option { + let ExprKind::Closure { + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + capture_refs, + is_static, + .. + } = &value.kind + else { + return None; + }; + if !capture_refs.iter().any(|capture| capture == assigned_name) { + return None; + } + Some(lower_closure_with_context( + ctx, + params, + variadic.as_deref(), + *variadic_by_ref, + return_type.as_ref(), + body, + captures, + capture_refs, + value, + &[], + Some(assigned_name), + *is_static, + )) +} + +/// Lowers a closure expression, applying contextual types to unannotated parameters. +fn lower_closure_with_context( + ctx: &mut LoweringContext<'_, '_>, + params: &[(String, Option, Option, bool)], + variadic: Option<&str>, + variadic_by_ref: bool, + return_type: Option<&TypeExpr>, + body: &[crate::parser::ast::Stmt], + captures: &[String], + capture_refs: &[String], + expr: &Expr, + contextual_arg_types: &[PhpType], + self_ref_callable_capture: Option<&str>, + is_static: bool, +) -> LoweredValue { + // PHP auto-binds `$this` to non-static closures (including arrow functions) + // defined inside an instance method, with no `use($this)` needed. The parser + // never lists `$this` as a capture, so thread it through the existing capture + // machinery here: load the enclosing `this` and append it to the captures so + // the closure body gets a `this` local. Only capture when the body actually + // references `$this` (directly or in a nested closure) — adding an unused + // capture would push otherwise capture-free closures through capture-only + // runtime paths. Nested closures compose: each level captures `this` from the + // level above. + // A method-defined closure loads the enclosing `this`; a top-level closure + // that uses `$this` (bound later via `Closure::bind`) gets a null `this` + // slot the bind fills, typed `Mixed` for runtime-dispatched member access. + let with_this; + let captures: &[String] = if !is_static + && !captures.iter().any(|name| name == "this") + && crate::types::checker::closure_body_uses_this(body) + { + with_this = captures + .iter() + .cloned() + .chain(std::iter::once("this".to_string())) + .collect::>(); + &with_this + } else { + captures + }; + let body_contains_eval = body_contains_eval_call(body); + let mut captured_values = Vec::with_capacity(captures.len()); + let mut capture_params = Vec::with_capacity(captures.len()); + for capture in captures { + let by_ref = capture_refs.iter().any(|name| name == capture); + let (captured, php_type) = if capture == "this" && !ctx.local_slots.contains_key("this") { + // Top-level closure: no enclosing `$this`. Start with a null receiver + // that `Closure::bind` overwrites; `Mixed` so members dispatch at + // runtime against the bound object's class. + (lower_null(ctx, expr), PhpType::Mixed) + } else { + let php_type_override = if by_ref && self_ref_callable_capture == Some(capture.as_str()) { + Some(PhpType::Callable) + } else if by_ref && body_contains_eval { + ctx.set_local_type(capture, PhpType::Mixed); + Some(PhpType::Mixed) + } else { + None + }; + let captured = ctx.load_local(capture, Some(expr.span)); + let php_type = php_type_override + .unwrap_or_else(|| ctx.builder.value_php_type(captured.value)); + (captured, php_type) + }; + let immediate = by_ref.then_some(Immediate::I64(1)); + ctx.emit_void(Op::ClosureCapture, vec![captured.value], immediate, Op::ClosureCapture.default_effects(), Some(expr.span)); + if by_ref { + ctx.mark_ref_bound_local(capture); + } + captured_values.push(ClosureCapture { value: captured.value }); + capture_params.push((capture.clone(), php_type, by_ref)); + } + let name = ctx.next_closure_name(); + let by_ref_return = matches!(&expr.kind, ExprKind::Closure { by_ref_return: true, .. }); + let signature = if contextual_arg_types.is_empty() { + function::lower_closure_function( + ctx, + &name, + params, + variadic, + variadic_by_ref, + return_type, + body, + &capture_params, + self_ref_callable_capture, + by_ref_return, + ) + } else { + function::lower_closure_function_with_context( + ctx, + &name, + params, + variadic, + variadic_by_ref, + return_type, + body, + &capture_params, + contextual_arg_types, + self_ref_callable_capture, + by_ref_return, + ) + }; + let data = ctx.intern_string(&name); + let closure_operands = captured_values + .iter() + .map(|capture| capture.value) + .collect::>(); + ctx.set_pending_static_callable_result(StaticCallableBinding::Closure { + name, + signature, + captures: captured_values, + }); + let closure = ctx.emit_value( + Op::ClosureNew, + closure_operands, + Some(Immediate::Data(data)), + PhpType::Callable, + Op::ClosureNew.default_effects(), + Some(expr.span), + ); + if let Some(capture) = self_ref_callable_capture { + ctx.set_local_logical_type(capture, PhpType::Callable); + } + closure +} + +/// Returns true when a statement body contains an `eval(...)` call. +fn body_contains_eval_call(body: &[Stmt]) -> bool { + body.iter().any(stmt_contains_eval_call) +} + +/// Returns true when a statement or nested statement body contains an `eval(...)` call. +fn stmt_contains_eval_call(stmt: &Stmt) -> bool { + match &stmt.kind { + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::ConstDecl { value: expr, .. } + | StmtKind::ListUnpack { value: expr, .. } + | StmtKind::StaticVar { init: expr, .. } + | StmtKind::Assign { value: expr, .. } + | StmtKind::TypedAssign { value: expr, .. } + | StmtKind::ArrayPush { value: expr, .. } + | StmtKind::StaticPropertyAssign { value: expr, .. } + | StmtKind::StaticPropertyArrayPush { value: expr, .. } => expr_contains_eval_call(expr), + StmtKind::Return(expr) => expr.as_ref().is_some_and(expr_contains_eval_call), + StmtKind::ArrayAssign { index, value, .. } + | StmtKind::StaticPropertyArrayAssign { index, value, .. } + | StmtKind::PropertyArrayAssign { index, value, .. } => { + expr_contains_eval_call(index) || expr_contains_eval_call(value) + } + StmtKind::NestedArrayAssign { target, value } => { + expr_contains_eval_call(target) || expr_contains_eval_call(value) + } + StmtKind::PropertyAssign { object, value, .. } + | StmtKind::PropertyArrayPush { object, value, .. } => { + expr_contains_eval_call(object) || expr_contains_eval_call(value) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + expr_contains_eval_call(condition) + || body_contains_eval_call(then_body) + || elseif_clauses.iter().any(|(condition, body)| { + expr_contains_eval_call(condition) || body_contains_eval_call(body) + }) + || else_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::IfDef { then_body, else_body, .. } => { + body_contains_eval_call(then_body) + || else_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + expr_contains_eval_call(condition) || body_contains_eval_call(body) + } + StmtKind::For { init, condition, update, body } => { + init.as_deref().is_some_and(stmt_contains_eval_call) + || condition.as_ref().is_some_and(expr_contains_eval_call) + || update.as_deref().is_some_and(stmt_contains_eval_call) + || body_contains_eval_call(body) + } + StmtKind::Foreach { array, body, .. } => { + expr_contains_eval_call(array) || body_contains_eval_call(body) + } + StmtKind::Switch { subject, cases, default } => { + expr_contains_eval_call(subject) + || cases.iter().any(|(patterns, body)| { + patterns.iter().any(expr_contains_eval_call) || body_contains_eval_call(body) + }) + || default.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::Include { path, .. } => expr_contains_eval_call(path), + StmtKind::Synthetic(body) + | StmtKind::NamespaceBlock { body, .. } + | StmtKind::IncludeOnceGuard { body, .. } => body_contains_eval_call(body), + StmtKind::FunctionDecl { params, body, .. } => { + params + .iter() + .any(|(_, _, default, _)| default.as_ref().is_some_and(expr_contains_eval_call)) + || body_contains_eval_call(body) + } + StmtKind::ClassDecl { properties, methods, constants, .. } + | StmtKind::TraitDecl { properties, methods, constants, .. } + | StmtKind::InterfaceDecl { properties, methods, constants, .. } => { + properties.iter().any(|property| { + property.default.as_ref().is_some_and(expr_contains_eval_call) + }) || constants + .iter() + .any(|constant| expr_contains_eval_call(&constant.value)) + || methods.iter().any(|method| { + method.params.iter().any(|(_, _, default, _)| { + default.as_ref().is_some_and(expr_contains_eval_call) + }) || body_contains_eval_call(&method.body) + }) + } + StmtKind::Try { try_body, catches, finally_body } => { + body_contains_eval_call(try_body) + || catches.iter().any(|catch_clause| body_contains_eval_call(&catch_clause.body)) + || finally_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::EnumDecl { cases, .. } => cases + .iter() + .any(|case| case.value.as_ref().is_some_and(expr_contains_eval_call)), + StmtKind::RefAssign { .. } + | StmtKind::Break(_) + | StmtKind::Continue(_) + | StmtKind::NamespaceDecl { .. } + | StmtKind::UseDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::FunctionVariantMark { .. } + | StmtKind::IncludeOnceMark { .. } + | StmtKind::Global { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => false, + } +} + +/// Returns true when an expression contains an `eval(...)` call. +fn expr_contains_eval_call(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::FunctionCall { name, args } => { + is_eval_call_name(name) || args.iter().any(expr_contains_eval_call) + } + ExprKind::BinaryOp { left, right, .. } => { + expr_contains_eval_call(left) || expr_contains_eval_call(right) + } + ExprKind::InstanceOf { value, target } => { + expr_contains_eval_call(value) || instance_of_target_contains_eval_call(target) + } + ExprKind::Negate(expr) + | ExprKind::Not(expr) + | ExprKind::BitNot(expr) + | ExprKind::Throw(expr) + | ExprKind::Clone(expr) + | ExprKind::ErrorSuppress(expr) + | ExprKind::Print(expr) + | ExprKind::Spread(expr) + | ExprKind::Cast { expr, .. } + | ExprKind::PtrCast { expr, .. } + | ExprKind::BufferNew { len: expr, .. } + | ExprKind::YieldFrom(expr) => expr_contains_eval_call(expr), + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } + | ExprKind::Pipe { value, callable: default } + | ExprKind::ArrayAccess { array: value, index: default } => { + expr_contains_eval_call(value) || expr_contains_eval_call(default) + } + ExprKind::Assignment { target, value, result_target, prelude, .. } => { + expr_contains_eval_call(target) + || expr_contains_eval_call(value) + || result_target.as_ref().is_some_and(|target| expr_contains_eval_call(target)) + || body_contains_eval_call(prelude) + } + ExprKind::ArrayLiteral(items) => items.iter().any(expr_contains_eval_call), + ExprKind::ArrayLiteralAssoc(entries) => entries + .iter() + .any(|(key, value)| expr_contains_eval_call(key) || expr_contains_eval_call(value)), + ExprKind::Match { subject, arms, default } => { + expr_contains_eval_call(subject) + || arms.iter().any(|(patterns, value)| { + patterns.iter().any(expr_contains_eval_call) || expr_contains_eval_call(value) + }) + || default.as_ref().is_some_and(|default| expr_contains_eval_call(default)) + } + ExprKind::Ternary { condition, then_expr, else_expr } => { + expr_contains_eval_call(condition) + || expr_contains_eval_call(then_expr) + || expr_contains_eval_call(else_expr) + } + ExprKind::Closure { params, body, .. } => { + params + .iter() + .any(|(_, _, default, _)| default.as_ref().is_some_and(expr_contains_eval_call)) + || body_contains_eval_call(body) + } + ExprKind::NamedArg { value, .. } => expr_contains_eval_call(value), + ExprKind::ClosureCall { args, .. } + | ExprKind::StaticMethodCall { args, .. } + | ExprKind::NewObject { args, .. } + | ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_contains_eval_call), + ExprKind::ExprCall { callee, args } => { + expr_contains_eval_call(callee) || args.iter().any(expr_contains_eval_call) + } + ExprKind::NewDynamic { name_expr, args } => { + expr_contains_eval_call(name_expr) || args.iter().any(expr_contains_eval_call) + } + ExprKind::NewDynamicObject { class_name, args, .. } => { + expr_contains_eval_call(class_name) || args.iter().any(expr_contains_eval_call) + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => expr_contains_eval_call(object), + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + expr_contains_eval_call(object) || expr_contains_eval_call(property) + } + ExprKind::MethodCall { object, args, .. } + | ExprKind::NullsafeMethodCall { object, args, .. } => { + expr_contains_eval_call(object) || args.iter().any(expr_contains_eval_call) + } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_eval_call(object) + || expr_contains_eval_call(method) + || args.iter().any(expr_contains_eval_call) + } + ExprKind::FirstClassCallable(target) => callable_target_contains_eval_call(target), + ExprKind::Yield { key, value } => { + key.as_ref().is_some_and(|key| expr_contains_eval_call(key)) + || value.as_ref().is_some_and(|value| expr_contains_eval_call(value)) + } + ExprKind::IncludeValue { path, .. } => expr_contains_eval_call(path), + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::Variable(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::ConstRef(_) + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::This + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => false, + } +} + +/// Returns true when an `instanceof` target expression contains an `eval(...)` call. +fn instance_of_target_contains_eval_call(target: &InstanceOfTarget) -> bool { + match target { + InstanceOfTarget::Name(_) => false, + InstanceOfTarget::Expr(expr) => expr_contains_eval_call(expr), + } +} + +/// Returns true when a first-class callable target contains an `eval(...)` call. +fn callable_target_contains_eval_call(target: &CallableTarget) -> bool { + match target { + CallableTarget::Function(_) | CallableTarget::StaticMethod { .. } => false, + CallableTarget::Method { object, .. } => expr_contains_eval_call(object), + } +} + +/// Returns true when a function call name resolves to PHP's `eval` construct. +fn is_eval_call_name(name: &Name) -> bool { + php_symbol_key(name.as_str().trim_start_matches('\\')) == "eval" +} + +/// Lowers a closure variable call. +fn lower_closure_call(ctx: &mut LoweringContext<'_, '_>, var: &str, args: &[Expr], expr: &Expr) -> LoweredValue { + if let Some(value) = lower_invokable_object_variable_call(ctx, var, args, expr) { + return value; + } + let mut result_type = None; + let mut instance_signature = None; + if let Some(target) = ctx.static_callable_local(var) { + result_type = Some(static_callable_return_type(ctx, &target)); + instance_signature = instance_callable_signature(&target).cloned(); + if let Some(value) = lower_static_callable_call(ctx, target, args, expr) { + return value; + } + } + let callable = ctx.load_local(var, Some(expr.span)); + let result_type = result_type.unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); + if instance_signature.is_none() { + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { + return emit_callable_descriptor_invoke( + ctx, + callable, + arg_container, + result_type, + expr.span, + ); + } + } + let mut operands = vec![callable.value]; + operands.extend(lower_args_with_signature(ctx, instance_signature.as_ref(), args)); + ctx.emit_value(Op::ClosureCall, operands, None, result_type, Op::ClosureCall.default_effects(), Some(expr.span)) +} + +/// Lowers `$object(...)` when the local object has an `__invoke` method. +fn lower_invokable_object_variable_call( + ctx: &mut LoweringContext<'_, '_>, + var: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object = Expr::new(ExprKind::Variable(var.to_string()), expr.span); + lower_invokable_object_expr_call(ctx, &object, args, expr) +} + +/// Lowers invokable object calls through the normal method-call path. +fn lower_invokable_object_expr_call( + ctx: &mut LoweringContext<'_, '_>, + callee: &Expr, + args: &[Expr], + expr: &Expr, +) -> Option { + if !is_invokable_object_expr(ctx, callee) { + return None; + } + Some(lower_method_call(ctx, callee, "__invoke", args, Op::MethodCall, expr)) +} + +/// Returns true when an expression is known to evaluate to an object with `__invoke`. +fn is_invokable_object_expr( + ctx: &LoweringContext<'_, '_>, + callee: &Expr, +) -> bool { + instance_callable_object_class(ctx, callee) + .and_then(|class_name| class_method_signature(ctx, &class_name, "__invoke")) + .is_some() +} + +/// Lowers an expression call. +fn lower_expr_call(ctx: &mut LoweringContext<'_, '_>, callee: &Expr, args: &[Expr], expr: &Expr) -> LoweredValue { + if let Some(value) = lower_invokable_object_expr_call(ctx, callee, args, expr) { + return value; + } + if let Some(value) = lower_first_class_callable_expr_call(ctx, callee, args, expr) { + return value; + } + if let Some(value) = lower_literal_callable_array_expr_call(ctx, callee, args, expr) { + return value; + } + if let Some(callback) = static_call_user_func_callback(ctx, callee) { + if let Some(value) = lower_static_callable_call(ctx, callback, args, expr) { + return value; + } + } + if let Some(callback) = static_assignment_callable_target(ctx, callee) { + lower_expr(ctx, callee); + if let Some(value) = lower_static_callable_call(ctx, callback, args, expr) { + return value; + } + } + // `Closure::bind(fn &() => $this->prop, $obj, $obj)()` invokes the bound closure. Lower it + // as a direct call to the closure with `$obj` boxed as its `$this` capture, so a + // by-reference return passes the property's ref-cell pointer through (the generic runtime + // descriptor invoker boxes results and cannot). + if let Some(value) = lower_bound_closure_immediate_call(ctx, callee, args, expr) { + return value; + } + let lowered_callee = lower_expr(ctx, callee); + // An immediately-invoked closure literal (`(fn &() => …)()`) registers its static + // callable binding while lowering. Call it directly through the static-callable path + // (as `$f()` does) so the closure body's signature — including a by-reference return — + // drives the call instead of the generic descriptor-invoke path, which cannot return + // every result type. + if let Some(target) = ctx.take_pending_static_callable_result() { + if let Some(value) = lower_static_callable_call(ctx, target, args, expr) { + return value; + } + } + let result_type = dynamic_callable_result_type(ctx, lowered_callee.value, expr); + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { + return emit_callable_descriptor_invoke( + ctx, + lowered_callee, + arg_container, + result_type, + expr.span, + ); + } + let mut operands = vec![lowered_callee.value]; + operands.extend(lower_args(ctx, args)); + ctx.emit_value(Op::ExprCall, operands, None, result_type, Op::ExprCall.default_effects(), Some(expr.span)) +} + +/// Lowers direct calls to literal callable arrays through descriptor metadata. +fn lower_literal_callable_array_expr_call( + ctx: &mut LoweringContext<'_, '_>, + callee: &Expr, + args: &[Expr], + expr: &Expr, +) -> Option { + let ExprKind::ArrayLiteral(items) = &callee.kind else { + return None; + }; + if let Some(StaticCallableBinding::StaticMethodDescriptor { receiver, method }) = + static_array_callable_descriptor_target(ctx, items) + { + return Some(lower_static_method_descriptor_call(ctx, &receiver, &method, args, expr)); + } + instance_array_callable_target(ctx, items)?; + let lowered_callee = lower_expr(ctx, callee); + let result_type = dynamic_callable_result_type(ctx, lowered_callee.value, expr); + let arg_container = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; + Some(emit_callable_descriptor_invoke( + ctx, + lowered_callee, + arg_container, + result_type, + expr.span, + )) +} + +/// Lowers an expression call once the callable expression is already evaluated. +fn lower_expr_call_from_value( + ctx: &mut LoweringContext<'_, '_>, + callee: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let result_type = dynamic_callable_result_type(ctx, callee.value, expr); + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { + return emit_callable_descriptor_invoke(ctx, callee, arg_container, result_type, expr.span); + } + let mut operands = vec![callee.value]; + operands.extend(lower_args(ctx, args)); + ctx.emit_value( + Op::ExprCall, + operands, + None, + result_type, + Op::ExprCall.default_effects(), + Some(expr.span), + ) +} + +/// Lowers explicit named arguments for signature-unknown descriptor invocations. +fn lower_untyped_descriptor_invoker_arg_container( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], + span: Span, +) -> Option { + if crate::types::call_args::has_named_args(args) { + return Some(lower_untyped_descriptor_invoker_hash_container(ctx, args, span)); + } + Some(lower_untyped_descriptor_invoker_indexed_container(ctx, args, span)) +} + +/// Builds an indexed descriptor-invoker container for signature-unknown calls. +fn lower_untyped_descriptor_invoker_indexed_container( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], + span: Span, +) -> LoweredValue { + let elem_ty = PhpType::Mixed; + let array_ty = PhpType::Array(Box::new(elem_ty.clone())); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(args.len() as u32)), + array_ty.clone(), + Op::ArrayNew.default_effects(), + Some(span), + ); + for arg in args { + if let ExprKind::Spread(inner) = &arg.kind { + let source = lower_expr(ctx, inner); + lower_indexed_array_spread_into_array(ctx, array, source, Some(&elem_ty), arg.span); + continue; + } + let value = lower_untyped_descriptor_invoker_arg_value(ctx, arg); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(arg.span), + ); + super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, arg.span); + } + array +} + +/// Builds an associative descriptor-invoker container for named or named/spread calls. +fn lower_untyped_descriptor_invoker_hash_container( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], + span: Span, +) -> LoweredValue { + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(args.len() as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(span), + ); + let mut next_positional_key = emit_i64_at_span(ctx, 0, span); + for arg in args { + match &arg.kind { + ExprKind::NamedArg { name, value } => { + let key = lower_string_literal(ctx, name, arg); + let value = lower_untyped_descriptor_invoker_arg_value(ctx, value); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(arg.span), + ); + } + ExprKind::Spread(inner) => { + let source = lower_expr(ctx, inner); + next_positional_key = lower_untyped_descriptor_invoker_spread_into_hash( + ctx, + hash, + source, + next_positional_key, + arg.span, + ); + } + _ => { + let key = next_positional_key; + let value = lower_untyped_descriptor_invoker_arg_value(ctx, arg); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(arg.span), + ); + let one = emit_i64_at_span(ctx, 1, arg.span); + next_positional_key = ctx.emit_value( + Op::IAdd, + vec![key.value, one.value], + None, + PhpType::Int, + Op::IAdd.default_effects(), + Some(arg.span), + ); + } + } } + ctx.emit_value( + Op::MixedBox, + vec![hash.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) } -/// Returns precise return metadata for array builtins that preserve the first operand type. -fn array_preserve_first_builtin_return_type( +/// Copies an indexed spread source into a descriptor-invoker hash with numeric keys. +fn lower_untyped_descriptor_invoker_spread_into_hash( + ctx: &mut LoweringContext<'_, '_>, + hash: LoweredValue, + source: LoweredValue, + start_key: LoweredValue, + span: Span, +) -> LoweredValue { + let source_elem_ty = match ctx.builder.value_php_type(source.value).codegen_repr() { + PhpType::Array(elem_ty) => elem_ty.codegen_repr(), + _ => PhpType::Mixed, + }; + let len = ctx.emit_value( + Op::ArrayLen, + vec![source.value], + None, + PhpType::Int, + Op::ArrayLen.default_effects(), + Some(span), + ); + let zero = emit_i64_at_span(ctx, 0, span); + let header = ctx.builder.create_named_block("descriptor.spread.next", vec![(IrType::I64, PhpType::Int)]); + let body = ctx.builder.create_named_block("descriptor.spread.body", Vec::new()); + let exit = ctx.builder.create_named_block("descriptor.spread.exit", Vec::new()); + ctx.builder.terminate(Terminator::Br { target: header, args: vec![zero.value] }); + + ctx.builder.position_at_end(header); + let index = ctx.builder.block_param(header, 0); + let has_next = ctx.emit_value( + Op::ICmp, + vec![index, len.value], + Some(Immediate::CmpPredicate(CmpPredicate::Slt)), + PhpType::Bool, + Op::ICmp.default_effects(), + Some(span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: has_next.value, + then_target: body, + then_args: Vec::new(), + else_target: exit, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(body); + let key = ctx.emit_value( + Op::IAdd, + vec![start_key.value, index], + None, + PhpType::Int, + Op::IAdd.default_effects(), + Some(span), + ); + let value = ctx.emit_value( + Op::ArrayGet, + vec![source.value, index], + None, + source_elem_ty, + Op::ArrayGet.default_effects(), + Some(span), + ); + let value = coerce_descriptor_invoker_mixed_value(ctx, value, span); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(span), + ); + release_value_after_retaining_insert(ctx, Some(&PhpType::Mixed), value, span); + let one = emit_i64_at_span(ctx, 1, span); + let next = ctx.emit_value( + Op::IAdd, + vec![index, one.value], + None, + PhpType::Int, + Op::IAdd.default_effects(), + Some(span), + ); + ctx.builder.terminate(Terminator::Br { target: header, args: vec![next.value] }); + + ctx.builder.position_at_end(exit); + crate::ir_lower::ownership::release_if_owned(ctx, source, Some(span)); + ctx.emit_value( + Op::IAdd, + vec![start_key.value, len.value], + None, + PhpType::Int, + Op::IAdd.default_effects(), + Some(span), + ) +} + +/// Lowers one untyped descriptor argument, preserving variables as ref markers. +fn lower_untyped_descriptor_invoker_arg_value( + ctx: &mut LoweringContext<'_, '_>, + arg: &Expr, +) -> LoweredValue { + let value = match &arg.kind { + ExprKind::Variable(name) => lower_invoker_ref_arg_marker(ctx, name, arg.span), + _ => lower_expr(ctx, arg), + }; + coerce_descriptor_invoker_mixed_value(ctx, value, arg.span) +} + +/// Boxes a descriptor-invoker argument value into the Mixed slot shape. +fn coerce_descriptor_invoker_mixed_value( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + span: Span, +) -> LoweredValue { + if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { + return value; + } + ctx.emit_value( + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) +} + +/// Returns the result storage type for an indirect callable with no static signature. +fn dynamic_callable_result_type( ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let first = operands.first()?; - Some(ctx.builder.value_php_type(*first).codegen_repr()) + callable: ValueId, + expr: &Expr, +) -> PhpType { + match ctx.builder.value_php_type(callable).codegen_repr() { + PhpType::Callable | PhpType::Str | PhpType::Array(_) | PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, + _ => fallback_expr_type(expr), + } } -/// Returns precise return metadata for `array_fill_keys(keys, value)`. -fn array_fill_keys_builtin_return_type( +/// Resolves an assignment-expression callee whose assigned value is a static callable. +fn static_assignment_callable_target( ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let keys = operands.first()?; - let value = operands.get(1)?; - let key_ty = match ctx.builder.value_php_type(*keys).codegen_repr() { - PhpType::Array(elem) => array_key_type_from_value_type(elem.codegen_repr()), - _ => return None, + callee: &Expr, +) -> Option { + let ExprKind::Assignment { target, value, .. } = &callee.kind else { + return None; }; - let value_ty = ctx.builder.value_php_type(*value).codegen_repr(); - Some(PhpType::AssocArray { - key: Box::new(key_ty), - value: Box::new(value_ty), - }) + if !matches!(target.kind, ExprKind::Variable(_)) { + return None; + } + static_callable_binding_for_expr(ctx, value).and_then(direct_static_callable_binding) } -/// Returns precise return metadata for `array_flip(array)`. -fn array_flip_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let array = operands.first()?; - match ctx.builder.value_php_type(*array).codegen_repr() { - PhpType::Array(value) => Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(value.codegen_repr())), - value: Box::new(PhpType::Int), - }), - PhpType::AssocArray { key, value } => Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(value.codegen_repr())), - value: key, - }), +/// Lowers direct invocation of a literal first-class callable target. +fn lower_first_class_callable_expr_call( + ctx: &mut LoweringContext<'_, '_>, + callee: &Expr, + args: &[Expr], + expr: &Expr, +) -> Option { + match &callee.kind { + ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { + Some(lower_function_call(ctx, name, args, expr)) + } + ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { + Some(lower_static_method_call(ctx, receiver, method, args, expr)) + } + ExprKind::FirstClassCallable(target @ CallableTarget::Method { .. }) => { + let signature = static_callable_binding_for_expr(ctx, callee) + .and_then(|target| signature_for_static_callable_binding(ctx, target)); + let callable = lower_first_class_callable(ctx, target, callee); + let result_type = signature + .as_ref() + .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) + .unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); + let arg_container = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; + Some(emit_callable_descriptor_invoke( + ctx, + callable, + arg_container, + result_type, + expr.span, + )) + } _ => None, } } -/// Returns precise return metadata for `array_combine(keys, values)`. -fn array_combine_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let keys = operands.first()?; - let values = operands.get(1)?; - let key_ty = match ctx.builder.value_php_type(*keys).codegen_repr() { - PhpType::Array(elem) => array_key_type_from_value_type(elem.codegen_repr()), - _ => return None, - }; - let value_ty = match ctx.builder.value_php_type(*values).codegen_repr() { - PhpType::Array(elem) => elem.codegen_repr(), - _ => return None, - }; - Some(PhpType::AssocArray { - key: Box::new(key_ty), - value: Box::new(value_ty), - }) +/// Lowers fixed-class object construction. +fn lower_new_object( + ctx: &mut LoweringContext<'_, '_>, + class_name: &Name, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionclass" { + if let Some(operands) = lower_reflection_class_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionparameter" { + if let Some(operands) = lower_reflection_parameter_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionmethod" { + if let Some(operands) = lower_reflection_method_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } + if ctx.has_eval_barrier() + && !ctx.classes.contains_key(class_name.as_str()) + && plain_positional_call_args(args) + { + let operands = lower_args_with_signature(ctx, None, args); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::EvalObjectNew, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalObjectNew.default_effects(), + Some(expr.span), + ); + } + let sig = constructor_signature(ctx, class_name).cloned(); + let operands = lower_args_with_signature(ctx, sig.as_ref(), args); + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ) } -/// Returns precise return metadata for `array_merge()`. -/// -/// Empty indexed arrays lower as `Array`; when that is the first operand, the merged -/// array inherits the second operand's element metadata so later indexed reads materialize -/// real payload values instead of void sentinels. -fn array_merge_builtin_return_type( - ctx: &LoweringContext<'_, '_>, - operands: &[crate::ir::ValueId], -) -> Option { - let first = operands.first()?; - let first_ty = ctx.builder.value_php_type(*first).codegen_repr(); - let second_ty = operands - .get(1) - .map(|value| ctx.builder.value_php_type(*value).codegen_repr()); - match first_ty { - PhpType::Array(elem) if is_empty_array_element_type(elem.as_ref()) => match second_ty { - Some(PhpType::Array(right)) if is_scalar_merge_element_type(right.as_ref()) => { - Some(PhpType::Array(right)) - } - _ => Some(PhpType::Array(elem)), - }, - PhpType::Array(elem) => Some(PhpType::Array(elem)), - other => Some(other), +/// Lowers `ReflectionClass(object)` while preserving object operands for runtime class metadata. +fn lower_reflection_class_constructor_operands( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; + let class_name = instance_callable_object_class(ctx, &reflected_arg)?; + let lowered = lower_expr(ctx, &reflected_arg); + if matches!( + ctx.builder.value_php_type(lowered.value).codegen_repr(), + PhpType::Object(_) + ) { + return Some(vec![lowered.value]); + } + if ctx.value_is_owning_temporary(lowered) { + crate::ir_lower::ownership::release_if_owned(ctx, lowered, Some(reflected_arg.span)); } + let data = ctx.intern_class_name(&class_name); + let value = ctx.emit_value( + Op::ConstClassName, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Str, + Op::ConstClassName.default_effects(), + Some(reflected_arg.span), + ); + Some(vec![value.value]) } -/// Returns true for the element sentinel used by statically empty indexed arrays. -fn is_empty_array_element_type(ty: &PhpType) -> bool { - matches!(ty.codegen_repr(), PhpType::Void) +/// Lowers direct `ReflectionMethod` constructor operands to literal class and method names. +fn lower_reflection_method_constructor_operands( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let (class_arg, method_arg) = reflection_method_constructor_regular_args(ctx, args)?; + Some(vec![ + lower_expr(ctx, &class_arg).value, + lower_expr(ctx, &method_arg).value, + ]) } -/// Returns true for element types copied safely by the scalar merge runtime helper. -fn is_scalar_merge_element_type(ty: &PhpType) -> bool { - matches!( - ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Callable | PhpType::Void - ) +/// Lowers PHP `clone $object` to a shallow object-copy opcode and optional `__clone()` hook. +fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> LoweredValue { + let object = lower_expr(ctx, inner); + let object_ty = ctx.builder.value_php_type(object.value); + let Some((class_name, false)) = singular_object_class(&object_ty) else { + unreachable!("clone expressions must be type-checked as non-null objects before lowering"); + }; + let class_name = class_name.to_string(); + let data = ctx.intern_class_name(&class_name); + let result_ty = PhpType::Object(class_name.clone()); + let cloned = ctx.emit_value( + Op::ObjectCloneShallow, + vec![object.value], + Some(Immediate::Data(data)), + result_ty, + Op::ObjectCloneShallow.default_effects(), + Some(expr.span), + ); + if class_method_signature(ctx, &class_name, &php_symbol_key("__clone")).is_some() { + lower_method_call_with_receiver(ctx, cloned, "__clone", &[], Op::MethodCall, expr); + } + cloned } -/// Returns precise builtin return types needed by EIR value materialization. -fn builtin_return_type_override(name: &str) -> Option { - match php_symbol_key(name.trim_start_matches('\\')).as_str() { - "chdir" | "checkdate" | "chgrp" | "chmod" | "chown" | "lchgrp" | "lchown" - | "class_alias" | "class_exists" | "copy" | "define" | "defined" - | "empty" | "file_exists" | "fnmatch" | "function_exists" | "is_a" | "is_callable" - | "is_array" | "is_object" | "is_scalar" - | "fdatasync" | "fflush" | "flock" | "fsync" | "ftruncate" | "interface_exists" | "is_dir" - | "is_executable" | "is_file" | "is_link" | "is_numeric" | "link" | "mkdir" | "rename" - | "enum_exists" | "trait_exists" | "putenv" | "rmdir" | "is_readable" - | "is_subclass_of" | "is_writeable" | "is_writable" | "settype" - | "is_resource" | "hash_equals" | "hash_update" | "spl_autoload_register" - | "spl_autoload_unregister" | "stream_context_set_option" | "stream_context_set_params" - | "stream_filter_register" | "stream_filter_remove" | "__elephc_phar_set_compression" - | "__elephc_phar_set_metadata" | "__elephc_phar_set_stub" - | "__elephc_phar_set_file_metadata" - | "__elephc_phar_sign_openssl" | "__elephc_phar_sign_hash" - | "__elephc_phar_set_zip_password" - | "stream_wrapper_register" | "stream_wrapper_restore" | "stream_wrapper_unregister" - | "stream_isatty" | "stream_is_local" | "stream_set_blocking" | "stream_set_timeout" - | "stream_socket_enable_crypto" | "stream_socket_shutdown" | "stream_supports_lock" | "symlink" | "touch" - | "unlink" => { - Some(PhpType::Bool) - } - "basename" | "date" | "gmdate" | "dirname" | "exec" | "get_class" | "get_parent_class" - | "getcwd" | "getenv" | "gethostname" | "gethostbyname" | "php_uname" - | "readline" | "shell_exec" | "sys_get_temp_dir" - | "fread" | "get_resource_type" | "gzcompress" | "gzdeflate" | "hash" | "hash_final" | "hash_hmac" | "long2ip" - | "stream_get_line" | "system" | "spl_autoload_extensions" | "tempnam" | "vsprintf" - | "__elephc_phar_get_metadata" | "__elephc_phar_get_stub" - | "__elephc_phar_get_file_metadata" - | "__elephc_phar_gzip_archive" | "__elephc_phar_bzip2_archive" - | "__elephc_phar_decompress_archive" - | "__elephc_phar_get_signature_hash" | "__elephc_phar_get_signature_type" => { - Some(PhpType::Str) - } - "disk_free_space" | "disk_total_space" => Some(PhpType::Float), - "clearstatcache" | "closedir" | "exit" | "die" | "passthru" | "rewinddir" - | "stream_bucket_append" | "stream_bucket_prepend" | "unset" => Some(PhpType::Void), - "fclose" | "feof" | "rewind" => Some(PhpType::Bool), - "printf" | "array_rand" | "array_unshift" | "file_put_contents" | "filemtime" - | "filesize" | "fprintf" | "fpassthru" | "fputcsv" | "fseek" | "ftell" | "fwrite" - | "crc32" | "get_resource_id" | "isset" | "linkinfo" | "mktime" | "gmmktime" | "sleep" - | "__elephc_mktime_raw" | "__elephc_gmmktime_raw" - | "pclose" | "spl_object_id" | "stream_select" | "stream_set_chunk_size" - | "stream_set_read_buffer" | "stream_set_write_buffer" - | "__elephc_strtotime_raw" | "time" - | "umask" | "vfprintf" | "vprintf" | "realpath_cache_size" => { - Some(PhpType::Int) - } - // strtotime() is `int|false`: a real timestamp (including a valid -1 pre-epoch) on success, - // or boolean false when the string cannot be parsed. The backend boxes the result so - // `=== false` and `echo` observe the distinct false; `__elephc_strtotime_raw` (the DateTime - // internal alias above) stays a plain Int that maps the failure sentinel to -1. - "strtotime" => Some(PhpType::Union(vec![PhpType::Int, PhpType::False])), - // microtime() with a non-literal `as_float` flag yields `string|float` (boxed `Mixed`): - // the runtime branches on the flag and boxes either the "0.NNNNNNNN sec" string or the - // float. Literal-true / literal-false / omitted cases are resolved earlier by - // `call_return_type_for_args` (Float / Str), so this entry is only reached for a - // non-literal flag. - "microtime" => Some(PhpType::Union(vec![PhpType::Str, PhpType::Float])), - "spl_object_hash" => Some(PhpType::Str), - "spl_autoload" | "spl_autoload_call" | "usleep" => Some(PhpType::Void), - "stream_context_create" | "stream_context_get_default" | "stream_context_set_default" => { - Some(PhpType::stream_resource()) - } - "realpath_cache_get" | "stream_context_get_options" | "stream_context_get_params" - | "stream_get_meta_data" => Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Mixed), - }), - "getdate" | "localtime" | "hrtime" | "file_get_contents" | "fileatime" | "filectime" | "filegroup" | "fileinode" - | "fileowner" | "fileperms" | "filetype" | "readfile" | "readlink" | "realpath" - | "fgetc" | "fgets" | "fopen" | "fstat" | "hash_copy" | "hash_file" | "hash_init" - | "gethostbyaddr" | "getprotobyname" | "getprotobynumber" | "getservbyname" - | "getservbyport" | "fsockopen" | "inet_ntop" | "inet_pton" | "ip2long" | "opendir" - | "pfsockopen" | "readdir" | "popen" | "stat" | "lstat" | "stream_get_contents" - | "stream_bucket_make_writeable" | "stream_bucket_new" | "stream_filter_append" - | "stream_filter_prepend" | "stream_resolve_include_path" | "stream_socket_accept" - | "stream_socket_client" | "stream_socket_pair" | "stream_copy_to_stream" - | "stream_socket_get_name" | "stream_socket_recvfrom" | "stream_socket_sendto" - | "stream_socket_server" | "tmpfile" | "gzinflate" | "gzuncompress" | "strpos" | "strrpos" => { - Some(PhpType::Mixed) - } - "spl_autoload_functions" => Some(PhpType::Array(Box::new(PhpType::Int))), - "__elephc_phar_list_entries" | "class_attribute_names" | "explode" | "fgetcsv" - | "file" | "get_declared_classes" | "fscanf" | "get_declared_interfaces" - | "get_declared_traits" | "glob" | "hash_algos" | "scandir" | "spl_classes" - | "str_split" | "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" - | "sscanf" => { - Some(PhpType::Array(Box::new(PhpType::Str))) - } - "class_attribute_args" => Some(PhpType::Array(Box::new(PhpType::Mixed))), - "class_get_attributes" => Some(PhpType::Array(Box::new(PhpType::Object( - "ReflectionAttribute".to_string(), - )))), - _ => None, - } +/// Metadata operand source for direct `ReflectionParameter` constructor lowering. +enum ReflectionParameterConstructorOperand { + Expr(Expr), + ClassName { name: String, span: Span }, + ObjectExpr { expr: Expr, span: Span }, } -/// Distinguishes pre-lowered array-literal items between plain elements and spread operands. -enum SpreadItem { - Element(LoweredValue), - Spread(LoweredValue), +/// Lowers validated `ReflectionParameter` constructor arguments into metadata operands. +/// +/// Method targets lower as `[class, method, parameter]`; function targets lower +/// as `[function, parameter]`. +fn lower_reflection_parameter_constructor_operands( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let arg_exprs = reflection_parameter_constructor_arg_exprs(ctx, args)?; + Some( + arg_exprs + .iter() + .map(|arg| lower_reflection_parameter_constructor_operand(ctx, arg)) + .collect(), + ) } -/// Lowers an indexed array literal. -fn lower_array_literal(ctx: &mut LoweringContext<'_, '_>, items: &[Expr], expr: &Expr) -> LoweredValue { - // Fast path: literals without any spread keep the original dest-first lowering so the - // common `[1, 2, 3]` form does not reorder allocation relative to element evaluation. - if !items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { - let array_ty = array_literal_type_for_ir(ctx, items, expr); - return lower_array_literal_without_spread(ctx, items, expr, array_ty); - } - // Spread-containing literals: lower every item value in source order first so PHP-visible side - // effects happen in order, then inspect each spread source's actual IR type to decide whether - // the destination must be associative (hash) storage. Dest allocation is pure, so emitting it - // after source evaluation preserves observable behavior. - let mut lowered: Vec = Vec::with_capacity(items.len()); - let mut any_assoc_spread = false; - for item in items { - match &item.kind { - ExprKind::Spread(inner) => { - let source = lower_expr(ctx, inner); - if matches!( - ctx.builder.value_php_type(source.value).codegen_repr(), - PhpType::AssocArray { .. } - ) { - any_assoc_spread = true; - } - lowered.push(SpreadItem::Spread(source)); - } - _ => { - let value = lower_expr(ctx, item); - lowered.push(SpreadItem::Element(value)); +/// Lowers one direct `ReflectionParameter` metadata operand. +fn lower_reflection_parameter_constructor_operand( + ctx: &mut LoweringContext<'_, '_>, + operand: &ReflectionParameterConstructorOperand, +) -> ValueId { + match operand { + ReflectionParameterConstructorOperand::Expr(expr) => lower_expr(ctx, expr).value, + ReflectionParameterConstructorOperand::ObjectExpr { expr, span } => { + let object = lower_expr(ctx, expr); + let class_name = reflection_parameter_lowered_object_class_name(ctx, object.value) + .expect("ReflectionParameter object target must be type-checked as a known object"); + if ctx.value_is_owning_temporary(object) { + crate::ir_lower::ownership::release_if_owned(ctx, object, Some(*span)); } + emit_reflection_parameter_class_name_operand(ctx, &class_name, *span) + } + ReflectionParameterConstructorOperand::ClassName { name, span } => { + emit_reflection_parameter_class_name_operand(ctx, name, *span) } - } - if any_assoc_spread { - lower_array_literal_as_hash_from_lowered(ctx, items, &lowered, expr) - } else { - lower_array_literal_as_indexed_from_lowered(ctx, items, &lowered, expr) } } -/// Lowers an indexed array literal using a contextual element storage type. -pub(crate) fn lower_array_literal_with_expected_type( +/// Emits one class-name operand for direct `ReflectionParameter` metadata. +fn emit_reflection_parameter_class_name_operand( ctx: &mut LoweringContext<'_, '_>, - expr: &Expr, - elem_ty: PhpType, -) -> LoweredValue { - let ExprKind::ArrayLiteral(items) = &expr.kind else { - return lower_expr(ctx, expr); - }; - if items.iter().any(|item| matches!(item.kind, ExprKind::Spread(_))) { - return lower_array_literal(ctx, items, expr); - } - let array_ty = expected_indexed_array_literal_type(elem_ty); - lower_array_literal_without_spread(ctx, items, expr, array_ty) + name: &str, + span: Span, +) -> ValueId { + let data = ctx.intern_class_name(name); + ctx.emit_value( + Op::ConstClassName, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Str, + Op::ConstClassName.default_effects(), + Some(span), + ) + .value } -/// Returns an indexed-array type for contextual literal lowering. -fn expected_indexed_array_literal_type(elem_ty: PhpType) -> PhpType { - PhpType::Array(Box::new(elem_ty.codegen_repr())) +/// Returns metadata operand expressions from a normalized static `ReflectionParameter` call. +fn reflection_parameter_constructor_arg_exprs( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let args = expand_static_call_spread_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (target, parameter) = if crate::types::call_args::has_named_args(&args) { + let sig = ctx + .classes + .get("ReflectionParameter") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = + crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + ( + planned_regular_arg_expr(plan.regular_args.first()?)?.clone(), + planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(), + ) + } else { + (args.first()?.clone(), args.get(1)?.clone()) + }; + match &target.kind { + ExprKind::ArrayLiteral(items) if items.len() == 2 => { + let owner = reflection_parameter_method_owner_operand(ctx, &items[0])?; + Some(vec![ + owner, + ReflectionParameterConstructorOperand::Expr(items[1].clone()), + ReflectionParameterConstructorOperand::Expr(parameter), + ]) + } + ExprKind::StringLiteral(_) => Some(vec![ + ReflectionParameterConstructorOperand::Expr(target), + ReflectionParameterConstructorOperand::Expr(parameter), + ]), + _ => None, + } } -/// Lowers a no-spread indexed array literal into the requested array storage type. -fn lower_array_literal_without_spread( - ctx: &mut LoweringContext<'_, '_>, - items: &[Expr], - expr: &Expr, - array_ty: PhpType, -) -> LoweredValue { - let elem_ty = indexed_array_literal_element_type(&array_ty); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - array_ty, - Op::ArrayNew.default_effects(), - Some(expr.span), - ); - for item in items { - let value = lower_expr(ctx, item); - let value = coerce_array_literal_element_to_storage_type(ctx, value, elem_ty.as_ref(), item); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(item.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, item.span); +/// Returns the static class-name operand for a ReflectionParameter method target. +fn reflection_parameter_method_owner_operand( + ctx: &LoweringContext<'_, '_>, + owner: &Expr, +) -> Option { + match &owner.kind { + ExprKind::StringLiteral(name) => Some(ReflectionParameterConstructorOperand::ClassName { + name: name.clone(), + span: owner.span, + }), + ExprKind::ClassConstant { receiver } => { + static_receiver_class_name(ctx, receiver).map(|name| { + ReflectionParameterConstructorOperand::ClassName { + name, + span: owner.span, + } + }) + } + ExprKind::Variable(name) => { + let PhpType::Object(class_name) = ctx.local_type(name).codegen_repr() else { + return None; + }; + if class_name.is_empty() { + return None; + } + Some(ReflectionParameterConstructorOperand::ClassName { + name: class_name, + span: owner.span, + }) + } + ExprKind::This => { + ctx.current_class + .clone() + .map(|name| ReflectionParameterConstructorOperand::ClassName { + name, + span: owner.span, + }) + } + _ => Some(ReflectionParameterConstructorOperand::ObjectExpr { + expr: owner.clone(), + span: owner.span, + }), } - array } -/// Coerces an array literal element to the contextual storage type when needed. -fn coerce_array_literal_element_to_storage_type( - ctx: &mut LoweringContext<'_, '_>, - value: LoweredValue, - elem_ty: Option<&PhpType>, - expr: &Expr, -) -> LoweredValue { - let Some(elem_ty) = elem_ty else { - return value; - }; - let coerced = match elem_ty.codegen_repr() { - PhpType::Int | PhpType::Bool if value.ir_type != IrType::I64 => { - coerce_to_int(ctx, value, expr) - } - PhpType::Float if value.ir_type != IrType::F64 => coerce_to_float(ctx, value, expr), - PhpType::Str if value.ir_type != IrType::Str => coerce_to_string(ctx, value, expr), - _ => value, +/// Returns the concrete class name from a lowered object target. +fn reflection_parameter_lowered_object_class_name( + ctx: &LoweringContext<'_, '_>, + value: ValueId, +) -> Option { + let PhpType::Object(class_name) = ctx.builder.value_php_type(value).codegen_repr() else { + return None; }; - if coerced.value != value.value && ctx.value_is_owning_temporary(value) { - crate::ir_lower::ownership::release_if_owned(ctx, value, Some(expr.span)); + if class_name.is_empty() || !ctx.classes.contains_key(class_name.as_str()) { + return None; } - coerced + Some(class_name) } -/// Lowers a spread-containing indexed-array literal whose spread sources are all indexed arrays. -fn lower_array_literal_as_indexed_from_lowered( +/// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. +fn lower_new_dynamic( ctx: &mut LoweringContext<'_, '_>, - items: &[Expr], - lowered: &[SpreadItem], + name_expr: &Expr, + args: &[Expr], expr: &Expr, ) -> LoweredValue { - let array_ty = array_literal_type_for_ir(ctx, items, expr); - let elem_ty = indexed_array_literal_element_type(&array_ty); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - array_ty, - Op::ArrayNew.default_effects(), + let mut operands = vec![lower_expr(ctx, name_expr).value]; + operands.extend(lower_args(ctx, args)); + ctx.emit_value( + Op::DynamicObjectNewMixed, + operands, + None, + PhpType::Mixed, + Op::DynamicObjectNewMixed.default_effects(), Some(expr.span), - ); - for (item, value) in items.iter().zip(lowered.iter()) { - match value { - SpreadItem::Spread(source) => { - lower_indexed_array_spread_into_array(ctx, array, *source, elem_ty.as_ref(), item.span); - } - SpreadItem::Element(value) => { - ctx.emit_void(Op::ArrayPush, vec![array.value, value.value], None, Op::ArrayPush.default_effects(), Some(item.span)); - super::stmt::release_indexed_array_write_operand(ctx, elem_ty.as_ref(), *value, item.span); - } - } - } - array + ) } -/// Lowers a spread-containing array literal with at least one associative spread as a hash. -fn lower_array_literal_as_hash_from_lowered( +/// Lowers dynamic object construction. +fn lower_new_dynamic_object( ctx: &mut LoweringContext<'_, '_>, - items: &[Expr], - lowered: &[SpreadItem], + class_name: &Expr, + fallback_class: &Name, + required_parent: &Name, + args: &[Expr], expr: &Expr, ) -> LoweredValue { - let hash_ty = assoc_array_literal_type_from_spreads(ctx, items, expr); - let value_ty = match hash_ty.codegen_repr() { - PhpType::AssocArray { value, .. } => value.codegen_repr(), - _ => PhpType::Mixed, - }; - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity(items.len() as u32)), - hash_ty, - Op::HashNew.default_effects(), + let mut operands = vec![lower_expr(ctx, class_name).value]; + operands.extend(lower_args(ctx, args)); + let name = format!("{}|{}", fallback_class.as_str(), required_parent.as_str()); + let data = ctx.intern_class_name(&name); + ctx.emit_value( + Op::DynamicObjectNew, + operands, + Some(Immediate::Data(data)), + PhpType::Object(fallback_class.as_str().to_string()), + Op::DynamicObjectNew.default_effects(), Some(expr.span), - ); - for (item, value) in items.iter().zip(lowered.iter()) { - match value { - SpreadItem::Spread(source) => { - lower_hash_spread_into_hash_from_value(ctx, hash, *source, item.span); - } - SpreadItem::Element(value) => { - ctx.emit_void( - Op::RuntimeCall, - vec![hash.value, value.value], - None, - effects_lookup::runtime_effects(), - Some(item.span), - ); - release_value_after_retaining_insert(ctx, Some(&value_ty), *value, item.span); - } - } - } - hash + ) } -/// Lowers a single already-lowered spread operand into a hash destination, handling both -/// associative and indexed source storage. Associative sources flatten directly through -/// `__rt_hash_spread`; indexed sources are first promoted to hash storage so the same -/// reindexing path applies. -fn lower_hash_spread_into_hash_from_value( +/// Returns constructor signature metadata when available for a fixed class. +fn constructor_signature<'a>( + ctx: &'a LoweringContext<'_, '_>, + class_name: &Name, +) -> Option<&'a FunctionSig> { + let key = php_symbol_key("__construct"); + ctx.classes + .get(class_name.as_str().trim_start_matches('\\')) + .and_then(|class_info| class_info.methods.get(&key)) +} + +/// Lowers an object property read. +fn lower_property_get( ctx: &mut LoweringContext<'_, '_>, - hash: LoweredValue, - source: LoweredValue, - span: crate::span::Span, -) { - let source_is_hash = matches!( - ctx.builder.value_php_type(source.value).codegen_repr(), - PhpType::AssocArray { .. } - ); - let spread_source = if source_is_hash { - source - } else { - let promoted = ctx.emit_value( - Op::ArrayToHash, - vec![source.value], - None, - PhpType::AssocArray { - key: Box::new(PhpType::Int), - value: Box::new(PhpType::Mixed), - }, - Op::ArrayToHash.default_effects(), - Some(span), - ); - LoweredValue { - value: promoted.value, - ir_type: IrType::Heap(IrHeapKind::Hash), - } + object: &Expr, + property: &str, + op: Op, + expr: &Expr, +) -> LoweredValue { + let object = lower_expr(ctx, object); + lower_property_get_from_value(ctx, object, property, op, expr) +} + +/// Lowers `$target = &$obj->prop`: binds the local `$target` to the reference cell +/// stored in the object's reference-property slot, so reads/writes of either side go +/// through the same cell (write-through). The property was promoted to a reference +/// property by the checker, so its slot holds a live cell pointer. +pub(crate) fn lower_ref_assign_property( + ctx: &mut LoweringContext<'_, '_>, + target: &str, + source: &Expr, + span: Span, +) { + let ExprKind::PropertyAccess { object, property } = &source.kind else { + return; }; - ctx.emit_void( - Op::HashSpread, - vec![hash.value, spread_source.value], - None, - Op::HashSpread.default_effects(), + let object = lower_expr(ctx, object); + let value_type = property_get_result_type(ctx, object.value, property, Op::PropGet, source); + let data = ctx.intern_string(property); + let cell_ptr = ctx.emit_value( + Op::LoadPropRefCell, + vec![object.value], + Some(Immediate::Data(data)), + value_type.clone(), + Op::LoadPropRefCell.default_effects(), Some(span), ); - if ctx.value_is_owning_temporary(spread_source) { - crate::ir_lower::ownership::release_if_owned(ctx, spread_source, Some(span)); - } + ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); } -/// Lowers an indexed-array spread by appending each source element to the destination. -fn lower_indexed_array_spread_into_array( +/// Lowers `$target = &call()`: binds `$target` to the reference cell returned by a +/// by-reference-returning callee. The call yields the cell pointer; the target shares it +/// non-owning (the owner is the object property the callee returned a reference to). +pub(crate) fn lower_ref_assign_call( ctx: &mut LoweringContext<'_, '_>, - array: LoweredValue, - source: LoweredValue, - container_elem_ty: Option<&PhpType>, - span: crate::span::Span, + target: &str, + source: &Expr, + span: Span, ) { - let source_elem_ty = match ctx.builder.value_php_type(source.value).codegen_repr() { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Mixed, + let cell_ptr = lower_expr(ctx, source); + let value_type = ctx.builder.value_php_type(cell_ptr.value); + ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); +} + +/// Lowers `$target =& $arr[idx]`: promotes the indexed-array element's inline storage to a +/// reference cell and binds `$target` to it non-owning. The returned cell pointer addresses +/// the element within the array payload, so writes through `$target` propagate to `$arr[idx]` +/// and vice versa. The array must remain live while the alias is in use (the local does not +/// own the storage). Operands: the lowered array value and the lowered index value. +pub(crate) fn lower_ref_assign_array_elem( + ctx: &mut LoweringContext<'_, '_>, + target: &str, + source: &Expr, + span: Span, +) { + let ExprKind::ArrayAccess { array, index } = &source.kind else { + return; }; - let len = ctx.emit_value( - Op::ArrayLen, - vec![source.value], + let array_value = lower_expr(ctx, array); + let mut index_value = lower_expr(ctx, index); + index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); + // Use the array's declared element type (the inline storage shape), not the + // null-capable `TaggedScalar` result type that `array_access_result_type` widens + // Int elements to. The ref-cell aliases the raw element slot, so loads and stores + // through the alias must match the element's storage width, not the read result. + let value_type = match ctx.builder.value_php_type(array_value.value).codegen_repr() { + PhpType::Array(elem_ty) => normalize_value_php_type(*elem_ty), + _ => array_access_result_type(ctx, array_value.value, Op::ArrayGet, source), + }; + let cell_ptr = ctx.emit_value( + Op::LoadArrayElemRefCell, + vec![array_value.value, index_value.value], None, - PhpType::Int, - Op::ArrayLen.default_effects(), + value_type.clone(), + Op::LoadArrayElemRefCell.default_effects(), Some(span), ); - let zero = emit_i64_at_span(ctx, 0, span); - let header = ctx.builder.create_named_block("array.spread.next", vec![(IrType::I64, PhpType::Int)]); - let body = ctx.builder.create_named_block("array.spread.body", Vec::new()); - let exit = ctx.builder.create_named_block("array.spread.exit", Vec::new()); - ctx.builder.terminate(Terminator::Br { target: header, args: vec![zero.value] }); + ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); +} - ctx.builder.position_at_end(header); - let index = ctx.builder.block_param(header, 0); - let has_next = ctx.emit_value( - Op::ICmp, - vec![index, len.value], - Some(Immediate::CmpPredicate(CmpPredicate::Slt)), - PhpType::Bool, - Op::ICmp.default_effects(), - Some(span), - ); - ctx.builder.terminate(Terminator::CondBr { - cond: has_next.value, - then_target: body, - then_args: Vec::new(), - else_target: exit, - else_args: Vec::new(), - }); +/// Lowers a named property read once the receiver is already evaluated. +fn lower_property_get_from_value( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + op: Op, + expr: &Expr, +) -> LoweredValue { + if op == Op::NullsafePropGet && value_is_definitely_null(ctx, object.value) { + return lower_boxed_null(ctx, expr); + } + // Route a read of a get-hooked property to its synthetic accessor, except inside that property's + // own accessor, where `$this->prop` must read the raw backing slot to avoid infinite recursion. + // A nullsafe read (`$obj?->prop`) routes to a nullsafe call so the null short-circuit is kept. + if matches!(op, Op::PropGet | Op::NullsafePropGet) + && class_declares_hook_accessor(ctx, object.value, &property_hook_get_method(property)) + && !ctx.in_own_property_accessor(property) + { + let accessor = property_hook_get_method(property); + let call_op = if op == Op::NullsafePropGet { + Op::NullsafeMethodCall + } else { + Op::MethodCall + }; + return lower_method_call_with_receiver(ctx, object, &accessor, &[], call_op, expr); + } + let data = ctx.intern_string(property); + let result_type = property_get_result_type(ctx, object.value, property, op, expr); + ctx.emit_value( + op, + vec![object.value], + Some(Immediate::Data(data)), + result_type, + op.default_effects(), + Some(expr.span), + ) +} - ctx.builder.position_at_end(body); - let value = ctx.emit_value( - Op::ArrayGet, - vec![source.value, index], - None, - source_elem_ty, - Op::ArrayGet.default_effects(), - Some(span), - ); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], - None, - Op::ArrayPush.default_effects(), - Some(span), - ); - super::stmt::release_indexed_array_write_operand(ctx, container_elem_ty, value, span); - let one = emit_i64_at_span(ctx, 1, span); - let next = ctx.emit_value( - Op::IAdd, - vec![index, one.value], - None, - PhpType::Int, - Op::IAdd.default_effects(), - Some(span), - ); - ctx.builder.terminate(Terminator::Br { target: header, args: vec![next.value] }); +/// Returns true when value metadata proves the runtime value is PHP null. +fn value_is_definitely_null(ctx: &LoweringContext<'_, '_>, value: crate::ir::ValueId) -> bool { + matches!(ctx.builder.value_php_type(value), PhpType::Void | PhpType::Never) +} - ctx.builder.position_at_end(exit); - if ctx.value_is_owning_temporary(source) { - crate::ir_lower::ownership::release_if_owned(ctx, source, Some(span)); +/// Returns true when value metadata permits PHP null at runtime. +fn value_is_nullable(ctx: &LoweringContext<'_, '_>, value: crate::ir::ValueId) -> bool { + match ctx.builder.value_php_type(value) { + PhpType::Void | PhpType::Never => true, + PhpType::Union(members) => members.iter().any(|member| matches!(member, PhpType::Void)), + _ => false, + } +} + +/// Returns precise PHP metadata for a named property read when class metadata is available. +fn property_get_result_type( + ctx: &LoweringContext<'_, '_>, + object: crate::ir::ValueId, + property: &str, + op: Op, + expr: &Expr, +) -> PhpType { + if op == Op::NullsafePropGet { + return PhpType::Mixed; + } + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, nullable)) = singular_object_class(&object_ty) else { + if matches!(object_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + return PhpType::Mixed; + } + if let PhpType::Packed(class_name) = object_ty.codegen_repr() { + let normalized = class_name.trim_start_matches('\\'); + let Some(class_info) = ctx.packed_classes.get(normalized) else { + return fallback_expr_type(expr); + }; + let Some(field) = class_info.fields.iter().find(|field| field.name == property) else { + return fallback_expr_type(expr); + }; + return normalize_value_php_type(field.php_type.codegen_repr()); + } + return fallback_expr_type(expr); + }; + let normalized = class_name.trim_start_matches('\\'); + if is_builtin_stdclass_name(normalized) { + return if nullable { + nullable_result_type(PhpType::Mixed) + } else { + PhpType::Mixed + }; + } + let Some(class_info) = ctx.classes.get(normalized) else { + return fallback_expr_type(expr); + }; + if let Some(property_ty) = runtime_property_type_override(ctx, normalized, property) { + let property_ty = normalize_value_php_type(property_ty); + return if nullable { + nullable_result_type(property_ty) + } else { + property_ty + }; + } + let Some((_, (_, property_ty))) = class_info.visible_property(property) else { + if let Some(magic_ty) = magic_get_result_type(ctx, normalized) { + return if nullable { + nullable_result_type(magic_ty) + } else { + magic_ty + }; + } + if class_info.allow_dynamic_properties { + return if nullable { + nullable_result_type(PhpType::Mixed) + } else { + PhpType::Mixed + }; + } + return fallback_expr_type(expr); + }; + let property_ty = normalize_value_php_type(property_ty.clone()); + if nullable { + nullable_result_type(property_ty) + } else { + property_ty } } -/// Emits an integer constant at a specific source span. -fn emit_i64_at_span( - ctx: &mut LoweringContext<'_, '_>, - value: i64, - span: crate::span::Span, -) -> LoweredValue { - ctx.emit_value( - Op::ConstI64, - Vec::new(), - Some(Immediate::I64(value)), - PhpType::Int, - Op::ConstI64.default_effects(), - Some(span), - ) -} - -/// Returns the element type from an indexed-array literal type. -fn indexed_array_literal_element_type(array_ty: &PhpType) -> Option { - match array_ty.codegen_repr() { - PhpType::Array(elem) => Some(elem.codegen_repr()), - _ => None, - } +/// Returns the normalized return type for a class `__get` magic property hook. +fn magic_get_result_type(ctx: &LoweringContext<'_, '_>, class_name: &str) -> Option { + class_method_signature(ctx, class_name, &php_symbol_key("__get")) + .map(|signature| normalize_value_php_type(signature.return_type.clone())) } -/// Releases an inserted temporary when the container retained or copied its payload. -/// Callable arrays keep raw descriptor pointers today, so the inserted owner stays alive. -fn release_value_after_retaining_insert( - ctx: &mut LoweringContext<'_, '_>, - container_elem_ty: Option<&PhpType>, - value: LoweredValue, - span: crate::span::Span, -) { - if matches!( - container_elem_ty.map(PhpType::codegen_repr), - Some(PhpType::Mixed | PhpType::Callable) - ) { - return; - } - if ctx.value_is_owning_temporary(value) { - crate::ir_lower::ownership::release_if_owned(ctx, value, Some(span)); +/// Adds nullability to a result type without nesting existing union metadata. +fn nullable_result_type(php_type: PhpType) -> PhpType { + match php_type { + PhpType::Union(mut members) => { + if !members.iter().any(|member| matches!(member, PhpType::Void)) { + members.push(PhpType::Void); + } + PhpType::Union(members) + } + other => PhpType::Union(vec![other, PhpType::Void]), } } -/// Returns the indexed-array type that the EIR backend can faithfully materialize. -fn array_literal_type_for_ir( +/// Returns true when the runtime class of `object` declares the synthetic property-hook accessor +/// `accessor_method` (`__propget_

` / `__propset_

`). Drives the decision to route a property +/// read/write to a hook; inherited (flattened) methods count, so subclasses inherit hooks. +fn class_declares_hook_accessor( ctx: &LoweringContext<'_, '_>, - items: &[Expr], - expr: &Expr, -) -> PhpType { - if items.is_empty() { - return fallback_expr_type(expr); - } - let mut elem_ty = array_literal_element_type_for_ir(ctx, &items[0]); - for item in items.iter().skip(1) { - elem_ty = merge_ir_indexed_element_type( - elem_ty, - array_literal_element_type_for_ir(ctx, item), - ); - } - PhpType::Array(Box::new(elem_ty)) + object: crate::ir::ValueId, + accessor_method: &str, +) -> bool { + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, _nullable)) = singular_object_class(&object_ty) else { + return false; + }; + let key = php_symbol_key(accessor_method); + ctx.classes + .get(class_name) + .is_some_and(|info| info.methods.contains_key(&key)) } -/// Returns the best EIR storage element type for one indexed-array literal item. -fn array_literal_element_type_for_ir( - ctx: &LoweringContext<'_, '_>, - item: &Expr, -) -> PhpType { - match &item.kind { - ExprKind::Null => PhpType::Mixed, - ExprKind::Spread(inner) => match array_literal_element_type_for_ir(ctx, inner).codegen_repr() { - PhpType::Array(elem) => elem.codegen_repr(), - _ => PhpType::Mixed, - }, - ExprKind::ArrayLiteral(items) => array_literal_type_for_ir(ctx, items, item).codegen_repr(), - ExprKind::ArrayLiteralAssoc(pairs) => assoc_array_literal_type_for_ir(ctx, pairs, item), - ExprKind::ConstRef(name) => ctx - .constant_value(name.as_str()) - .map(|(_, ty)| ir_array_storage_type(ty)) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), - ExprKind::Variable(name) => ir_array_storage_type( - ctx.local_types - .get(name) - .cloned() - .unwrap_or_else(|| infer_expr_type_syntactic(item)), - ), - ExprKind::FunctionCall { name, .. } => { - let canonical = name.as_str(); - if let Some(sig) = ctx.functions.get(canonical) { - return ir_array_storage_type(sig.return_type.clone()); - } - if let Some(sig) = ctx.extern_functions.get(canonical) { - return ir_array_storage_type(sig.return_type.clone()); +/// Returns the class name and nullability if `php_type` is a single object type (optionally +/// nullable). Heterogeneous unions and non-object types return `None`. +fn singular_object_class(php_type: &PhpType) -> Option<(&str, bool)> { + match php_type { + PhpType::Object(name) => Some((name.as_str(), false)), + PhpType::Union(members) => { + let mut found = None; + let mut nullable = false; + for member in members { + match member { + PhpType::Void => nullable = true, + PhpType::Object(name) => { + if found.is_some_and(|existing| existing != name.as_str()) { + return None; + } + found = Some(name.as_str()); + } + _ => return None, + } } - ir_array_storage_type(infer_expr_type_syntactic(item)) + found.map(|class_name| (class_name, nullable)) } - ExprKind::ArrayAccess { array, .. } => array_access_expr_value_type_for_ir(ctx, array) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), - ExprKind::PropertyAccess { object, property } => property_access_expr_type_for_ir( - ctx, - object, - property, - ) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(item))), - _ => ir_array_storage_type(infer_expr_type_syntactic(item)), + _ => None, } } -/// Returns the EIR array storage metadata type, preserving PHP resources. -fn ir_array_storage_type(php_type: PhpType) -> PhpType { - let php_type = normalize_value_php_type(php_type); - if matches!(php_type, PhpType::Resource(_)) { - php_type - } else { - php_type.codegen_repr() +/// Returns precise runtime storage types for inherited SPL callback-filter internals. +fn runtime_property_type_override( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + property: &str, +) -> Option { + if !class_extends_class(ctx, class_name, "CallbackFilterIterator") { + return None; + } + match property { + "callback" => Some(PhpType::Callable), + "callbackEnv" => Some(PhpType::Pointer(None)), + _ => None, } } -/// Merges indexed-array element types for EIR storage metadata. -fn merge_ir_indexed_element_type(left: PhpType, right: PhpType) -> PhpType { - if left == right { - return left; - } - if matches!(left.codegen_repr(), PhpType::Void | PhpType::Never) { - return right; - } - if matches!(right.codegen_repr(), PhpType::Void | PhpType::Never) { - return left; +/// Returns true when a class is or extends the target class. +fn class_extends_class( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + target_class: &str, +) -> bool { + let target_key = php_symbol_key(target_class); + let mut current = Some(class_name.trim_start_matches('\\').to_string()); + while let Some(name) = current { + if php_symbol_key(&name) == target_key { + return true; + } + current = ctx + .classes + .get(name.as_str()) + .and_then(|class_info| class_info.parent.clone()); } - PhpType::Mixed + false } -/// Lowers an associative array literal. -fn lower_assoc_array_literal(ctx: &mut LoweringContext<'_, '_>, pairs: &[(Expr, Expr)], expr: &Expr) -> LoweredValue { - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity(pairs.len() as u32)), - assoc_array_literal_type_for_ir(ctx, pairs, expr), - Op::HashNew.default_effects(), - Some(expr.span), - ); - for (key, value) in pairs { - let key = lower_expr(ctx, key); - let value = lower_expr(ctx, value); - ctx.emit_void(Op::HashSet, vec![hash.value, key.value, value.value], None, Op::HashSet.default_effects(), Some(expr.span)); - } - hash +/// Lowers a dynamic property read. +fn lower_dynamic_property_get(ctx: &mut LoweringContext<'_, '_>, object: &Expr, property: &Expr, expr: &Expr) -> LoweredValue { + let object = lower_expr(ctx, object); + lower_dynamic_property_get_from_value(ctx, object, property, expr) } -/// Returns the associative-array type for a literal that contains at least one associative -/// spread. Mirrors the type checker's `assoc_spread_literal_value_type` so EIR storage matches -/// the value types actually lowered into the hash. -fn assoc_array_literal_type_from_spreads( - ctx: &LoweringContext<'_, '_>, - items: &[Expr], +/// Lowers a dynamic property read once the receiver is already evaluated. +fn lower_dynamic_property_get_from_value( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &Expr, expr: &Expr, -) -> PhpType { - let mut value_ty = PhpType::Never; - for item in items { - let next = match &item.kind { - ExprKind::Spread(inner) => match infer_expr_type_syntactic(inner).codegen_repr() { - PhpType::Array(elem) => elem.codegen_repr(), - PhpType::AssocArray { value, .. } => value.codegen_repr(), - _ => PhpType::Mixed, - }, - _ => array_literal_element_type_for_ir(ctx, item).codegen_repr(), - }; - value_ty = merge_ir_assoc_value_type(value_ty, next); - } - if matches!(value_ty, PhpType::Never) { - return fallback_expr_type(expr); - } - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(value_ty), - } +) -> LoweredValue { + let result_type = dynamic_property_get_result_type(ctx, object.value, property, expr); + let property = lower_expr(ctx, property); + ctx.emit_value( + Op::DynamicPropGet, + vec![object.value, property.value], + None, + result_type, + Op::DynamicPropGet.default_effects(), + Some(expr.span), + ) } -/// Returns the associative-array type that the EIR backend can faithfully materialize. -fn assoc_array_literal_type_for_ir( +/// Returns precise metadata for dynamic property reads when class slots are statically known. +fn dynamic_property_get_result_type( ctx: &LoweringContext<'_, '_>, - pairs: &[(Expr, Expr)], + object: crate::ir::ValueId, + property: &Expr, expr: &Expr, ) -> PhpType { - if pairs.is_empty() { - return fallback_expr_type(expr); + if let ExprKind::StringLiteral(name) = &property.kind { + return property_get_result_type(ctx, object, name, Op::DynamicPropGet, expr); } - let mut key_ty = normalized_array_key_type( - &pairs[0].0, - infer_expr_type_syntactic(&pairs[0].0), - ); - let mut value_ty = assoc_array_literal_value_type_for_ir(ctx, &pairs[0].1); - for (key, value) in pairs.iter().skip(1) { - key_ty = merge_array_key_types( - key_ty, - normalized_array_key_type(key, infer_expr_type_syntactic(key)), - ); - value_ty = merge_ir_assoc_value_type( - value_ty, - assoc_array_literal_value_type_for_ir(ctx, value), - ); + let object_ty = ctx.builder.value_php_type(object); + if matches!(object_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + return PhpType::Mixed; } - PhpType::AssocArray { - key: Box::new(key_ty), - value: Box::new(value_ty), + let Some((class_name, nullable)) = singular_object_class(&object_ty) else { + return fallback_expr_type(expr); + }; + let normalized = class_name.trim_start_matches('\\'); + if is_builtin_stdclass_name(normalized) { + return if nullable { + nullable_result_type(PhpType::Mixed) + } else { + PhpType::Mixed + }; } + let Some(class_info) = ctx.classes.get(normalized) else { + return fallback_expr_type(expr); + }; + let members = class_info + .properties + .iter() + .map(|(_, property_ty)| { + let property_ty = normalize_value_php_type(property_ty.clone()); + if nullable { + nullable_result_type(property_ty) + } else { + property_ty + } + }) + .collect::>(); + normalize_union_members(members).unwrap_or_else(|| fallback_expr_type(expr)) } -/// Returns the best EIR storage value type for one associative-array literal value. -fn assoc_array_literal_value_type_for_ir( - ctx: &LoweringContext<'_, '_>, - value: &Expr, -) -> PhpType { - match &value.kind { - ExprKind::Null => PhpType::Mixed, - ExprKind::ConstRef(name) => ctx - .constant_value(name.as_str()) - .map(|(_, ty)| ir_array_storage_type(ty)) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), - // A class constant or enum case must be typed the way `lower_scoped_constant` - // resolves it, not by the syntactic `::class`-is-string default, or the hash - // value-type stamp would diverge from the lowered value and corrupt reads. - ExprKind::ScopedConstantAccess { receiver, name } => { - scoped_constant_value_type_for_ir(ctx, receiver, name, value) - } - ExprKind::Variable(name) => ir_array_storage_type( - ctx.local_types - .get(name) - .cloned() - .unwrap_or_else(|| infer_expr_type_syntactic(value)), - ), - ExprKind::FunctionCall { name, .. } => { - let canonical = name.as_str(); - if let Some(sig) = ctx.functions.get(canonical) { - return ir_array_storage_type(sig.return_type.clone()); +/// Returns true when the normalized class name refers to PHP's builtin stdClass. +fn is_builtin_stdclass_name(class_name: &str) -> bool { + crate::types::checker::builtin_stdclass::is_stdclass(class_name) +} + +/// Flattens and deduplicates union candidates, with `Mixed` absorbing all members. +fn normalize_union_members(members: Vec) -> Option { + let mut deduped = Vec::new(); + for member in members { + match member { + PhpType::Union(inner) => { + for inner_member in inner { + if inner_member == PhpType::Mixed { + return Some(PhpType::Mixed); + } + if !deduped.iter().any(|existing| existing == &inner_member) { + deduped.push(inner_member); + } + } } - if let Some(sig) = ctx.extern_functions.get(canonical) { - return ir_array_storage_type(sig.return_type.clone()); + PhpType::Mixed => return Some(PhpType::Mixed), + other => { + if !deduped.iter().any(|existing| existing == &other) { + deduped.push(other); + } } - ir_array_storage_type(infer_expr_type_syntactic(value)) } - ExprKind::ArrayAccess { array, .. } => array_access_expr_value_type_for_ir(ctx, array) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), - ExprKind::PropertyAccess { object, property } => property_access_expr_type_for_ir( - ctx, - object, - property, - ) - .unwrap_or_else(|| ir_array_storage_type(infer_expr_type_syntactic(value))), - _ => ir_array_storage_type(infer_expr_type_syntactic(value)), + } + match deduped.len() { + 0 => None, + 1 => deduped.pop(), + _ => Some(PhpType::Union(deduped)), } } -/// Returns the EIR storage value type for a scoped-constant array value, -/// resolving a class/interface constant the same way `lower_scoped_constant` -/// lowers it so the hash value-type stamp matches the value actually stored -/// (rather than the syntactic `::class`-is-string default). Falls back to the -/// syntactic guess when the constant cannot be resolved. -fn scoped_constant_value_type_for_ir( +/// Lowers a static property read. +fn lower_static_property_get(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticReceiver, property: &str, expr: &Expr) -> LoweredValue { + let name = format!("{}::{}", receiver_name(receiver), property); + let data = ctx.intern_string(&name); + let result_type = static_property_result_type(ctx, receiver, property, expr); + ctx.emit_value( + Op::LoadStaticProperty, + Vec::new(), + Some(Immediate::Data(data)), + result_type, + Op::LoadStaticProperty.default_effects(), + Some(expr.span), + ) +} + +/// Returns precise PHP metadata for a static property read when class metadata is available. +fn static_property_result_type( ctx: &LoweringContext<'_, '_>, receiver: &StaticReceiver, - member: &str, - value: &Expr, + property: &str, + _expr: &Expr, ) -> PhpType { - let class_name = scoped_constant_receiver_name(ctx, receiver); - let normalized = class_name.trim_start_matches('\\'); - // An enum case lowers to the case *object* singleton (see `lower_scoped_constant`), - // so the hash must box it as a Mixed cell — stamp the value type Mixed to match. - if ctx - .enums - .get(normalized) - .is_some_and(|enum_info| enum_info.cases.iter().any(|case| case.name == member)) - { + let Some(class_name) = static_receiver_class_name(ctx, receiver) else { return PhpType::Mixed; - } - if let Some(const_expr) = ctx.scoped_constant_value(&class_name, member) { - return ir_array_storage_type(infer_expr_type_syntactic(&const_expr)); - } - ir_array_storage_type(infer_expr_type_syntactic(value)) + }; + let Some(class_info) = ctx.classes.get(class_name.as_str()) else { + return PhpType::Mixed; + }; + let Some((_, property_ty)) = class_info + .static_properties + .iter() + .find(|(name, _)| name == property) + else { + return PhpType::Mixed; + }; + normalize_value_php_type(property_ty.codegen_repr()) } -/// Returns the element/value type for an array-access expression used inside a literal. -fn array_access_expr_value_type_for_ir( - ctx: &LoweringContext<'_, '_>, - array: &Expr, -) -> Option { - let array_ty = match &array.kind { - ExprKind::Variable(name) => ctx.local_types.get(name).cloned(), - ExprKind::PropertyAccess { object, property } => { - property_access_expr_type_for_ir(ctx, object, property) - } - ExprKind::ArrayLiteral(items) => Some(array_literal_type_for_ir(ctx, items, array)), - ExprKind::ArrayLiteralAssoc(pairs) => Some(assoc_array_literal_type_for_ir(ctx, pairs, array)), - _ => None, - }? - .codegen_repr(); - match array_ty { - PhpType::Array(elem_ty) => { - Some(array_access_element_result_type(normalize_value_php_type(*elem_ty).codegen_repr())) +/// Lowers an object method call. +fn lower_method_call( + ctx: &mut LoweringContext<'_, '_>, + object: &Expr, + method: &str, + args: &[Expr], + op: Op, + expr: &Expr, +) -> LoweredValue { + // A statically-decided private/protected method access from an inaccessible + // scope raises a catchable `Error` in PHP rather than a compile-time error, + // but the receiver expression must still be evaluated first. + let throw_access_message = if op == Op::MethodCall { + ctx.throw_access_sites.get(&expr.span).and_then(|info| { + if let ThrowAccessKind::PrivateMethod { + visibility, + class_name, + method: m, + } = &info.kind + { + Some(format!( + "Call to {} method {}::{}() from global scope", + visibility, class_name, m + )) + } else { + None + } + }) + } else { + None + }; + let object_expr = object; + let object = lower_expr(ctx, object_expr); + if let Some(message) = throw_access_message { + release_owning_receiver_temporary(ctx, object, expr.span); + return crate::ir_lower::stmt::lower_throw_access_error_expr(ctx, &message, expr.span); + } + if op == Op::MethodCall && value_is_definitely_null(ctx, object.value) { + let null_value = lower_null(ctx, expr); + terminate_method_call_on_null(ctx, method); + return null_value; + } + if op == Op::MethodCall { + if let Some(value) = + lower_reflection_function_invoke_call(ctx, Some(object_expr), method, args, expr) + { + return value; } - PhpType::AssocArray { value, .. } => { - Some(array_access_element_result_type(normalize_value_php_type(*value).codegen_repr())) + if let Some(value) = + lower_reflection_method_invoke_call(ctx, Some(object_expr), method, args, expr) + { + return value; } - PhpType::Str => Some(PhpType::Str), - PhpType::Mixed | PhpType::Union(_) => Some(PhpType::Mixed), - _ => None, } -} - -/// Returns the declared type for an object property expression used inside a literal. -fn property_access_expr_type_for_ir( - ctx: &LoweringContext<'_, '_>, - object: &Expr, - property: &str, -) -> Option { - let class_name = instance_callable_object_class(ctx, object)?; - let normalized = class_name.trim_start_matches('\\'); - if is_builtin_stdclass_name(normalized) { - return Some(PhpType::Mixed); + if op == Op::MethodCall && value_is_nullable(ctx, object.value) { + return lower_nullable_regular_method_call(ctx, object, method, args, expr); + } + if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { + return lower_reflection_class_new_instance(ctx, Some(object_expr), object, args, expr); + } + if op == Op::MethodCall && is_reflection_class_new_instance_args_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_args( + ctx, + Some(object_expr), + object, + args, + expr, + ); + } + if op == Op::MethodCall + && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_without_constructor(ctx, object, args, expr); } - if let Some(property_ty) = runtime_property_type_override(ctx, normalized, property) { - return Some(normalize_value_php_type(property_ty)); + if op == Op::MethodCall { + if let Some(value) = lower_reflection_class_static_property_value_call( + ctx, + Some(object_expr), + method, + args, + expr, + ) { + return value; + } } - let class_info = ctx.classes.get(normalized)?; - class_info - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| normalize_value_php_type(ty.codegen_repr())) -} - -/// Merges associative-array value types for EIR storage metadata. -fn merge_ir_assoc_value_type(left: PhpType, right: PhpType) -> PhpType { - if left == right { - return left; + if op == Op::MethodCall { + if let Some(value) = + lower_reflection_class_member_list_call(ctx, Some(object_expr), method, args, expr) + { + return value; + } } - if matches!(left, PhpType::Never) { - return right; + if op == Op::MethodCall { + if let Some(value) = + lower_reflection_property_value_call(ctx, Some(object_expr), method, args, expr) + { + return value; + } } - if matches!(right, PhpType::Never) { - return left; + if matches!( + ctx.builder.value_php_type(object.value).codegen_repr(), + PhpType::Callable + ) { + if let Some(result) = lower_closure_bind_method(ctx, &object, method, args, expr) { + return result; + } } - PhpType::Mixed + let magic_args; + let (dispatch_method, args) = if let Some(args) = + magic_call_dispatch_args(ctx, object.value, method, args, object_expr.span) + { + magic_args = args; + ("__call", magic_args.as_slice()) + } else { + (method, args) + }; + let result_type = method_call_result_type(ctx, object.value, dispatch_method, op, expr); + let mut operands = vec![object.value]; + let sig = method_call_argument_signature(ctx, object_expr, object.value, dispatch_method); + let arg_values = lower_args_with_signature(ctx, sig.as_ref(), args); + operands.extend(arg_values.iter().copied()); + let data = ctx.intern_string(dispatch_method); + let call = ctx.emit_value( + op, + operands, + Some(Immediate::Data(data)), + result_type, + op.default_effects(), + Some(expr.span), + ); + release_owned_call_arg_temporaries(ctx, &arg_values, Some(call.value), expr.span); + release_owning_receiver_temporary(ctx, object, expr.span); + call } -/// Lowers a match expression with lazy arm-result evaluation. -fn lower_match( +/// Lowers the `Closure` rebinding methods on a closure (`Callable`) receiver: +/// `$closure->bindTo($newThis [, $scope])` and `$closure->call($newThis, ...$args)`. +/// Returns `None` for any other method so normal dispatch (and its diagnostics) +/// still apply. The `$scope` argument is accepted and ignored — visibility is +/// resolved at compile time in elephc's closed-world model. +fn lower_closure_bind_method( ctx: &mut LoweringContext<'_, '_>, - subject: &Expr, - arms: &[(Vec, Expr)], - default: Option<&Expr>, + closure: &LoweredValue, + method: &str, + args: &[Expr], expr: &Expr, -) -> LoweredValue { - let subject = lower_expr(ctx, subject); - let result_type = fallback_expr_type(expr); - let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let merge = ctx.builder.create_named_block("match.merge", Vec::new()); - - for (conditions, result) in arms { - let result_block = ctx.builder.create_named_block("match.result", Vec::new()); - let mut fallthrough = ctx.builder.insertion_block(); - for condition in conditions { - let next_test = ctx.builder.create_named_block("match.next", Vec::new()); - let condition = lower_expr(ctx, condition); - let matched = ctx.emit_value( - Op::StrictEq, - vec![subject.value, condition.value], +) -> Option { + match php_symbol_key(method).as_str() { + "bindto" => { + let new_this = match args.first() { + Some(arg) => lower_expr(ctx, arg), + None => lower_null(ctx, expr), + }; + Some(emit_closure_bind(ctx, closure.value, new_this.value, expr)) + } + "call" => { + // `$closure->call($newThis, ...$args)`: bind `$this` then invoke the + // bound closure with the remaining arguments in one step. + let new_this = match args.first() { + Some(arg) => lower_expr(ctx, arg), + None => lower_null(ctx, expr), + }; + let bound = emit_closure_bind(ctx, closure.value, new_this.value, expr); + let call_args = &args[args.len().min(1)..]; + let arg_container = + lower_untyped_descriptor_invoker_arg_container(ctx, call_args, expr.span)?; + Some(ctx.emit_value( + Op::CallableDescriptorInvoke, + vec![bound.value, arg_container.value], None, - PhpType::Bool, - Op::StrictEq.default_effects(), + PhpType::Mixed, + Op::CallableDescriptorInvoke.default_effects(), Some(expr.span), - ); - ctx.builder.terminate(Terminator::CondBr { - cond: matched.value, - then_target: result_block, - then_args: Vec::new(), - else_target: next_test, - else_args: Vec::new(), - }); - ctx.builder.position_at_end(next_test); - fallthrough = Some(next_test); - } - ctx.builder.position_at_end(result_block); - store_expr_into_temp(ctx, &temp_name, result_type.clone(), result, expr.span); - branch_to(ctx, merge); - if let Some(fallthrough) = fallthrough { - ctx.builder.position_at_end(fallthrough); + )) } + _ => None, } - if let Some(default) = default { - store_expr_into_temp(ctx, &temp_name, result_type.clone(), default, expr.span); - branch_to(ctx, merge); - } else if !ctx.builder.insertion_block_is_terminated() { - let message = ctx.intern_string("Fatal error: unhandled match case\n"); - ctx.builder.terminate(Terminator::Fatal { message }); - } - ctx.builder.position_at_end(merge); - take_owned_temp(ctx, &temp_name, expr.span) } -/// Lowers array, hash, string, or ArrayAccess indexing. -fn lower_array_access( +/// Emits the `closure_bind` runtime call that rebinds a closure's captured +/// `$this`, yielding a new closure (`Callable`) descriptor. +fn emit_closure_bind( ctx: &mut LoweringContext<'_, '_>, - array: &Expr, - index: &Expr, + closure: crate::ir::ValueId, + new_this: crate::ir::ValueId, expr: &Expr, ) -> LoweredValue { - lower_array_access_with_missing_warning(ctx, array, index, expr, true) + let data = ctx.intern_function_name("closure_bind"); + ctx.emit_value( + Op::BuiltinCall, + vec![closure, new_this], + Some(Immediate::Data(data)), + PhpType::Callable, + Op::BuiltinCall.default_effects(), + Some(expr.span), + ) } -/// Lowers array, hash, string, or ArrayAccess indexing with configurable -/// undefined-offset warning behavior for native indexed-array reads. -fn lower_array_access_with_missing_warning( - ctx: &mut LoweringContext<'_, '_>, - array: &Expr, - index: &Expr, - expr: &Expr, - warn_on_missing: bool, -) -> LoweredValue { - let array_value = lower_expr(ctx, array); - if value_is_nullable(ctx, array_value.value) { - return lower_nullable_array_access(ctx, array_value, index, expr, warn_on_missing); +/// Builds synthetic `__call` arguments when a class lacks the requested method. +fn magic_call_dispatch_args( + ctx: &LoweringContext<'_, '_>, + object: crate::ir::ValueId, + method: &str, + args: &[Expr], + span: Span, +) -> Option> { + if method_signature(ctx, object, method).is_some() { + return None; } - lower_array_access_from_value(ctx, array_value, index, expr, warn_on_missing) + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, _)) = singular_object_class(&object_ty) else { + return None; + }; + let normalized = class_name.trim_start_matches('\\'); + class_method_signature(ctx, normalized, &php_symbol_key("__call"))?; + Some(vec![ + Expr::new(ExprKind::StringLiteral(method.to_string()), span), + Expr::new(ExprKind::ArrayLiteral(args.to_vec()), span), + ]) } -/// Lowers array access once the receiver is already evaluated. -fn lower_array_access_from_value( - ctx: &mut LoweringContext<'_, '_>, - array_value: LoweredValue, - index: &Expr, - expr: &Expr, - warn_on_missing: bool, -) -> LoweredValue { - let mut index_value = lower_expr(ctx, index); - let op = match array_value.ir_type { - IrType::Heap(IrHeapKind::Array) => { - let index_ty = index_expr_key_type(ctx, index); - if index_ty == PhpType::Int { - index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); - if warn_on_missing { - Op::ArrayGet - } else { - Op::ArrayGetSilent - } - } else { - // String or Mixed key on indexed storage: use the mixed-key - // runtime read path (mirrors Op::ArraySetMixedKey for writes). - if warn_on_missing { - Op::ArrayGetMixedKey - } else { - Op::ArrayGetMixedKeySilent - } - } - } - IrType::Heap(IrHeapKind::Hash) => Op::HashGet, - IrType::Heap(IrHeapKind::Buffer) => Op::BufferGet, - IrType::Str => { - index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); - Op::StrCharAt - } - _ => Op::RuntimeCall, +/// Returns the signature to use for method-call argument normalization. +fn method_call_argument_signature( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, + object: crate::ir::ValueId, + method: &str, +) -> Option { + if method_is_fiber_start(ctx, object, method) { + return crate::ir_lower::fibers::start_sig_for_expr(ctx, object_expr); + } + method_signature(ctx, object, method) +} + +/// Returns true when a method call targets PHP's built-in `Fiber::start()`. +fn method_is_fiber_start( + ctx: &LoweringContext<'_, '_>, + object: crate::ir::ValueId, + method: &str, +) -> bool { + if php_symbol_key(method) != "start" { + return false; + } + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, _)) = singular_object_class(&object_ty) else { + return false; }; - let result_type = array_access_result_type(ctx, array_value.value, op, expr); - ctx.emit_value( - op, - vec![array_value.value, index_value.value], - None, - result_type, - op.default_effects(), - Some(expr.span), - ) + php_symbol_key(class_name.trim_start_matches('\\')) == "fiber" } -/// Lowers nullable receiver indexing without evaluating the index on a null receiver. -fn lower_nullable_array_access( +/// Lowers `?Object->method()` calls so null receivers fatal before argument evaluation. +fn lower_nullable_regular_method_call( ctx: &mut LoweringContext<'_, '_>, - array_value: LoweredValue, - index: &Expr, + object: LoweredValue, + method: &str, + args: &[Expr], expr: &Expr, - warn_on_missing: bool, ) -> LoweredValue { + let result_type = method_call_result_type(ctx, object.value, method, Op::MethodCall, expr); + let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); + let fatal_block = ctx + .builder + .create_named_block("method.null.fatal", Vec::new()); + let call_block = ctx + .builder + .create_named_block("method.non_null.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("method.nullable.merge", Vec::new()); let is_null = ctx.emit_value( Op::IsNull, - vec![array_value.value], + vec![object.value], None, PhpType::Bool, Op::IsNull.default_effects(), Some(expr.span), ); - let result_type = PhpType::Mixed; - let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let null_block = ctx.builder.create_named_block("nullable.index.null", Vec::new()); - let read_block = ctx.builder.create_named_block("nullable.index.read", Vec::new()); - let merge = ctx.builder.create_named_block("nullable.index.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: is_null.value, - then_target: null_block, + then_target: fatal_block, then_args: Vec::new(), - else_target: read_block, + else_target: call_block, else_args: Vec::new(), }); - ctx.builder.position_at_end(null_block); - let null_value = lower_boxed_null(ctx, expr); - store_value_into_temp(ctx, &temp_name, result_type.clone(), null_value, expr.span); - branch_to(ctx, merge); + ctx.builder.position_at_end(fatal_block); + terminate_method_call_on_null(ctx, method); - ctx.builder.position_at_end(read_block); - let read_value = lower_array_access_from_value(ctx, array_value, index, expr, warn_on_missing); - store_value_into_temp(ctx, &temp_name, result_type, read_value, expr.span); + ctx.builder.position_at_end(call_block); + let call = lower_method_call_with_receiver(ctx, object, method, args, Op::MethodCall, expr); + store_value_into_temp(ctx, &temp_name, result_type.clone(), call, expr.span); branch_to(ctx, merge); ctx.builder.position_at_end(merge); take_owned_temp(ctx, &temp_name, expr.span) } -/// Returns the statically-known key type for an array index expression. -/// Used to decide between Op::ArrayGet (int key) and Op::ArrayGetMixedKey. -fn index_expr_key_type(_ctx: &LoweringContext<'_, '_>, index: &Expr) -> PhpType { - let ty = infer_expr_type_syntactic(index); - normalized_array_key_type(index, ty) -} - -/// Returns the best PHP result type for a lowered array/string/hash access. -fn array_access_result_type( - ctx: &LoweringContext<'_, '_>, - array: crate::ir::ValueId, - op: Op, - expr: &Expr, -) -> PhpType { - match op { - Op::StrCharAt => PhpType::Str, - Op::ArrayGet | Op::ArrayGetSilent => match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::Array(elem_ty) => { - array_access_element_result_type(normalize_value_php_type(*elem_ty)) - } - _ => fallback_expr_type(expr), - }, - Op::HashGet => match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::AssocArray { value, .. } => { - array_access_element_result_type(normalize_value_php_type(*value)) - } - _ => fallback_expr_type(expr), - }, - Op::BufferGet => match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::Buffer(elem_ty) => normalize_value_php_type(*elem_ty), - _ => fallback_expr_type(expr), - }, - Op::RuntimeCall => array_access_runtime_call_result_type(ctx, array, expr), - Op::ArrayGetMixedKey | Op::ArrayGetMixedKeySilent => PhpType::Mixed, - _ => match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, - _ => fallback_expr_type(expr), - }, - } -} - -/// Returns the materialized result type for a PHP array read, including miss-capable int reads. -pub(crate) fn array_access_element_result_type(element_ty: PhpType) -> PhpType { - if crate::codegen::sentinels::null_repr_is_tagged() && matches!(element_ty, PhpType::Int) { - PhpType::TaggedScalar - } else { - element_ty - } -} - -/// Returns the EIR result type for object indexing routed through `ArrayAccess::offsetGet`. -fn array_access_runtime_call_result_type( - ctx: &LoweringContext<'_, '_>, - array: crate::ir::ValueId, +/// Lowers `ReflectionClass::newInstance()` by constructing the reflected class name. +fn lower_reflection_class_new_instance( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + object: LoweredValue, + args: &[Expr], expr: &Expr, -) -> PhpType { - match ctx.builder.value_php_type(array).codegen_repr() { - PhpType::Object(class_name) => array_access_offset_get_return_type(ctx, &class_name) - .unwrap_or_else(|| fallback_expr_type(expr)), - PhpType::Mixed => PhpType::Mixed, - _ => fallback_expr_type(expr), - } -} - -/// Looks up the effective `offsetGet` return type for an ArrayAccess class. -fn array_access_offset_get_return_type( - ctx: &LoweringContext<'_, '_>, - class_name: &str, -) -> Option { - if !object_name_satisfies_interface_for_ir(ctx, class_name, "ArrayAccess") { - return None; - } - let method_key = php_symbol_key("offsetGet"); - class_method_return_type_for_ir(ctx, class_name, &method_key) - .or_else(|| interface_method_return_type_for_ir(ctx, "ArrayAccess", &method_key)) - .map(normalize_value_php_type) -} - -/// Returns true when a syntactic array receiver is statically known as `ArrayAccess`. -fn array_access_expr_satisfies_array_access( - ctx: &LoweringContext<'_, '_>, - array: &Expr, -) -> bool { - let ty = match &array.kind { - ExprKind::Variable(name) => ctx - .local_types - .get(name) - .cloned() - .unwrap_or_else(|| infer_expr_type_syntactic(array)), - _ => infer_expr_type_syntactic(array), - }; - type_satisfies_array_access_for_ir(ctx, &ty) -} - -/// Returns true when every possible object arm satisfies PHP's `ArrayAccess` interface. -pub(crate) fn type_satisfies_array_access_for_ir( - ctx: &LoweringContext<'_, '_>, - ty: &PhpType, -) -> bool { - match ty { - PhpType::Object(class_name) => { - object_name_satisfies_interface_for_ir(ctx, class_name, "ArrayAccess") - } - PhpType::Union(members) => { - let mut saw_object = false; - for member in members { - match member { - PhpType::Void | PhpType::Never => {} - other if type_satisfies_array_access_for_ir(ctx, other) => { - saw_object = true; - } - _ => return false, - } - } - saw_object - } - _ => false, - } -} - -/// Returns true when a class or interface name satisfies the requested interface. -fn object_name_satisfies_interface_for_ir( - ctx: &LoweringContext<'_, '_>, - class_name: &str, - interface_name: &str, -) -> bool { - let normalized = class_name.trim_start_matches('\\'); - if php_symbol_key(normalized) == php_symbol_key(interface_name.trim_start_matches('\\')) { - return true; - } - if ctx.interfaces.contains_key(normalized) { - return interface_extends_interface_for_ir(ctx, normalized, interface_name); - } - class_implements_interface_for_ir(ctx, normalized, interface_name) -} - -/// Returns whether a lowered class implements an interface, following parents. -fn class_implements_interface_for_ir( - ctx: &LoweringContext<'_, '_>, - class_name: &str, - interface_name: &str, -) -> bool { - let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); - let mut current = Some(class_name.trim_start_matches('\\')); - while let Some(candidate) = current { - let Some(info) = ctx.classes.get(candidate) else { - return false; - }; - if info - .interfaces - .iter() - .any(|interface| { - let interface = interface.trim_start_matches('\\'); - php_symbol_key(interface) == interface_key - || interface_extends_interface_for_ir(ctx, interface, interface_name) - }) - { - return true; - } - current = info.parent.as_deref(); - } - false -} - -/// Returns true when an interface extends the requested ancestor interface. -fn interface_extends_interface_for_ir( - ctx: &LoweringContext<'_, '_>, - interface_name: &str, - ancestor_name: &str, -) -> bool { - if php_symbol_key(interface_name.trim_start_matches('\\')) - == php_symbol_key(ancestor_name.trim_start_matches('\\')) +) -> LoweredValue { + let args = reflection_class_new_instance_args(args); + let constructor_sig = + reflection_class_new_instance_constructor_signature(ctx, object_expr, &args).cloned(); + if args.iter().any(is_spread_arg) + || (crate::types::call_args::has_named_args(&args) && constructor_sig.is_none()) { - return true; + return lower_reflection_class_new_instance_unsupported(ctx, expr); } - let Some(info) = ctx.interfaces.get(interface_name.trim_start_matches('\\')) else { - return false; + let class_name = lower_property_get_from_value(ctx, object, "__name", Op::PropGet, expr); + let mut operands = vec![class_name.value]; + operands.extend(lower_args_with_signature( + ctx, + constructor_sig.as_ref(), + &args, + )); + ctx.emit_value( + Op::DynamicObjectNewMixed, + operands, + None, + PhpType::Mixed, + Op::DynamicObjectNewMixed.default_effects(), + Some(expr.span), + ) +} + +/// Lowers `ReflectionClass::newInstanceArgs()` by unpacking one static argument array. +fn lower_reflection_class_new_instance_args( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + object: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let Some(forwarded_args) = reflection_class_new_instance_args_array(ctx, args) else { + return lower_reflection_class_new_instance_args_unsupported(ctx, expr); }; - info.parents.iter().any(|parent| { - let parent = parent.trim_start_matches('\\'); - php_symbol_key(parent) == php_symbol_key(ancestor_name.trim_start_matches('\\')) - || interface_extends_interface_for_ir(ctx, parent, ancestor_name) - }) + lower_reflection_class_new_instance(ctx, object_expr, object, &forwarded_args, expr) } -/// Returns a method return type from class metadata, following parent classes. -fn class_method_return_type_for_ir( - ctx: &LoweringContext<'_, '_>, - class_name: &str, - method_key: &str, -) -> Option { - let mut current = Some(class_name.trim_start_matches('\\')); - while let Some(candidate) = current { - let info = ctx.classes.get(candidate)?; - if let Some(sig) = info.methods.get(method_key) { - return Some(sig.return_type.clone()); - } - current = info.parent.as_deref(); +/// Lowers `ReflectionClass::newInstanceWithoutConstructor()` to constructorless allocation. +fn lower_reflection_class_new_instance_without_constructor( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + if !args.is_empty() { + return lower_reflection_class_new_instance_without_constructor_unsupported(ctx, expr); } - None + let class_name = lower_property_get_from_value(ctx, object, "__name", Op::PropGet, expr); + ctx.emit_value( + Op::DynamicObjectNewWithoutConstructorMixed, + vec![class_name.value], + None, + PhpType::Mixed, + Op::DynamicObjectNewWithoutConstructorMixed.default_effects(), + Some(expr.span), + ) } -/// Returns a method return type from interface metadata, following interface parents. -fn interface_method_return_type_for_ir( - ctx: &LoweringContext<'_, '_>, - interface_name: &str, - method_key: &str, -) -> Option { - let mut visited = std::collections::HashSet::new(); - let mut queue = vec![interface_name.trim_start_matches('\\').to_string()]; - while let Some(name) = queue.pop() { - if !visited.insert(name.clone()) { - continue; +/// Lowers live static-property value access for statically-known `ReflectionClass` calls. +fn lower_reflection_class_static_property_value_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; + match php_symbol_key(method).as_str() { + "getstaticproperties" => { + lower_reflection_class_get_static_properties(ctx, &class_name, args, expr) } - let Some(info) = ctx.interfaces.get(&name) else { - continue; - }; - if let Some(sig) = info.methods.get(method_key) { - return Some(sig.return_type.clone()); + "getstaticpropertyvalue" => { + lower_reflection_class_get_static_property_value(ctx, &class_name, args, expr) } - queue.extend(info.parents.iter().cloned()); + "setstaticpropertyvalue" => { + lower_reflection_class_set_static_property_value(ctx, &class_name, args, expr) + } + _ => None, } - None } -/// Lowers a ternary expression with lazy branch evaluation. -fn lower_ternary( +/// Lowers statically-known filtered ReflectionClass member-list calls. +fn lower_reflection_class_member_list_call( ctx: &mut LoweringContext<'_, '_>, - condition: &Expr, - then_expr: &Expr, - else_expr: &Expr, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], expr: &Expr, -) -> LoweredValue { - let cond = lower_expr(ctx, condition); - let cond = ctx.truthy(cond, Some(condition.span)); - let result_type = branch_merge_result_type(ctx, then_expr, else_expr, expr); - let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let split_initialized = ctx.initialized_slots_snapshot(); - let then_block = ctx.builder.create_named_block("ternary.then", Vec::new()); - let else_block = ctx.builder.create_named_block("ternary.else", Vec::new()); - let merge = ctx.builder.create_named_block("ternary.merge", Vec::new()); - ctx.builder.terminate(Terminator::CondBr { - cond: cond.value, - then_target: then_block, - then_args: Vec::new(), - else_target: else_block, - else_args: Vec::new(), - }); +) -> Option { + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; + let (member_class, items): (&str, Vec) = match php_symbol_key(method).as_str() { + "getproperties" => { + let filter = reflection_class_get_properties_filter_arg(ctx, args)?; + ( + "ReflectionProperty", + reflection_class_property_names_for_filter(ctx, &class_name, filter)? + .into_iter() + .map(|property| { + reflection_member_constructor_expr( + "ReflectionProperty", + &class_name, + &property, + expr.span, + ) + }) + .collect::>(), + ) + } + "getmethods" => { + let filter = reflection_class_get_methods_filter_arg(ctx, args)?; + ( + "ReflectionMethod", + reflection_class_method_names_for_filter(ctx, &class_name, filter)? + .into_iter() + .map(|method| { + reflection_member_constructor_expr( + "ReflectionMethod", + &class_name, + &method, + expr.span, + ) + }) + .collect::>(), + ) + } + _ => return None, + }; + Some(lower_reflection_member_array( + ctx, + member_class, + &items, + expr, + )) +} - ctx.builder.position_at_end(then_block); - ctx.restore_initialized_slots(split_initialized.clone()); - store_expr_into_temp(ctx, &temp_name, result_type.clone(), then_expr, expr.span); - let then_reachable = !ctx.builder.insertion_block_is_terminated(); - let then_initialized = ctx.initialized_slots_snapshot(); - branch_to(ctx, merge); +/// Lowers a statically materialized Reflection member list with an explicit element type. +fn lower_reflection_member_array( + ctx: &mut LoweringContext<'_, '_>, + member_class: &str, + items: &[Expr], + expr: &Expr, +) -> LoweredValue { + let elem_ty = PhpType::Object(member_class.to_string()); + let array_ty = PhpType::Array(Box::new(elem_ty.clone())); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty, + Op::ArrayNew.default_effects(), + Some(expr.span), + ); + for item in items { + let value = lower_expr(ctx, item); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + release_value_after_retaining_insert(ctx, Some(&elem_ty), value, item.span); + } + array +} - ctx.builder.position_at_end(else_block); - ctx.restore_initialized_slots(split_initialized.clone()); - store_expr_into_temp(ctx, &temp_name, result_type, else_expr, expr.span); - let else_reachable = !ctx.builder.insertion_block_is_terminated(); - let else_initialized = ctx.initialized_slots_snapshot(); - branch_to(ctx, merge); +/// Builds a direct Reflection member constructor expression for known metadata. +fn reflection_member_constructor_expr( + reflection_class: &str, + reflected_class: &str, + member: &str, + span: Span, +) -> Expr { + Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified(reflection_class), + args: vec![ + Expr::new(ExprKind::StringLiteral(reflected_class.to_string()), span), + Expr::new(ExprKind::StringLiteral(member.to_string()), span), + ], + }, + span, + ) +} - ctx.builder.position_at_end(merge); - ctx.restore_initialized_slots(merge_initialized_slots_for_expr( - &split_initialized, - then_initialized, - then_reachable, - else_initialized, - else_reachable, - )); - take_owned_temp(ctx, &temp_name, expr.span) +/// Lowers reflected function invocation for statically-known `ReflectionFunction` objects. +fn lower_reflection_function_invoke_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let method_key = php_symbol_key(method); + let object_expr = object_expr?; + let function_name = reflection_function_reflected_target(ctx, object_expr)?; + let Some(forwarded_args) = (match method_key.as_str() { + "invoke" => Some(reflection_function_invoke_args(args)), + "invokeargs" => reflection_function_invoke_args_array(ctx, args), + _ => return None, + }) else { + return Some(lower_reflection_function_invoke_unsupported( + ctx, + &method_key, + expr, + )); + }; + if let Some(signature) = first_class_builtin_signature(&function_name) { + return Some(lower_reflection_builtin_function_call( + ctx, + &function_name, + &signature, + &forwarded_args, + expr, + )); + } + let name = Name::from(function_name); + Some(lower_function_call(ctx, &name, &forwarded_args, expr)) } -/// Lowers a cast expression. -fn lower_cast(ctx: &mut LoweringContext<'_, '_>, target: &CastType, inner: &Expr, expr: &Expr) -> LoweredValue { - let value = lower_expr(ctx, inner); - let php_type = cast_php_type(target); - let result = ctx.emit_value( - Op::Cast, - vec![value.value], - Some(Immediate::CastTarget(value_ir_type(&php_type))), +/// Lowers reflected invocation of a supported callable builtin. +fn lower_reflection_builtin_function_call( + ctx: &mut LoweringContext<'_, '_>, + function_name: &str, + signature: &FunctionSig, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let operands = lower_builtin_call_args(ctx, function_name, Some(signature), args); + let php_type = call_return_type_for_args(ctx, function_name, args, &operands) + .unwrap_or_else(|| call_return_type(ctx, function_name, &operands)); + let data = ctx.intern_function_name(function_name); + ctx.emit_value( + Op::BuiltinCall, + operands, + Some(Immediate::Data(data)), php_type, - Op::Cast.default_effects(), + effects_lookup::builtin_effects(function_name), Some(expr.span), - ); - if matches!(target, CastType::String) { - release_stringified_source_if_owned(ctx, value, Some(expr.span)); + ) +} + +/// Returns direct `ReflectionFunction::invoke(...$args)` arguments after static spread expansion. +fn reflection_function_invoke_args(args: &[Expr]) -> Vec { + reflection_class_new_instance_args(args) +} + +/// Extracts the argument list passed to `ReflectionFunction::invokeArgs($args)`. +fn reflection_function_invoke_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [forwarded] => reflection_class_new_instance_args_value(ctx, forwarded), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionFunction") + .and_then(|class_info| class_info.methods.get(&php_symbol_key("invokeArgs")))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; } + let forwarded_arg = planned_regular_arg_expr(plan.regular_args.first()?)?; + reflection_class_new_instance_args_value(ctx, forwarded_arg) +} + +/// Emits a runtime fatal for ReflectionFunction invocation forms not yet lowered. +fn lower_reflection_function_invoke_unsupported( + ctx: &mut LoweringContext<'_, '_>, + method_key: &str, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let method_name = if method_key == "invokeargs" { + "invokeArgs" + } else { + "invoke" + }; + let message = ctx.intern_string(&format!( + "Fatal error: unsupported ReflectionFunction::{}() target or argument forwarding\n", + method_name + )); + ctx.builder.terminate(Terminator::Fatal { message }); result } -/// Releases an owned source whose string result cannot alias the original storage. -fn release_stringified_source_if_owned( +/// Lowers reflected method invocation for statically-known `ReflectionMethod` objects. +fn lower_reflection_method_invoke_call( ctx: &mut LoweringContext<'_, '_>, - source: LoweredValue, - span: Option, -) { - if !ctx.value_is_owning_temporary(source) { - return; - } - match ctx.builder.value_php_type(source.value).codegen_repr() { - PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { - crate::ir_lower::ownership::release_if_owned(ctx, source, span); - } - _ => {} + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let method_key = php_symbol_key(method); + let object_expr = object_expr?; + let (class_name, reflected_method) = reflection_method_reflected_target(ctx, object_expr)?; + let Some((object_arg, forwarded_args)) = (match method_key.as_str() { + "invoke" => reflection_method_invoke_args(args), + "invokeargs" => reflection_method_invoke_args_array(ctx, args), + _ => return None, + }) else { + return Some(lower_reflection_method_invoke_unsupported( + ctx, + &method_key, + expr, + )); + }; + let Some(target_kind) = reflection_method_target_kind(ctx, &class_name, &reflected_method) + else { + return Some(lower_reflection_method_invoke_unsupported( + ctx, + &method_key, + expr, + )); + }; + match target_kind { + ReflectionMethodTargetKind::Static => Some(lower_reflection_static_method_invoke( + ctx, + &class_name, + &reflected_method, + &object_arg, + &forwarded_args, + expr, + )), + ReflectionMethodTargetKind::Instance => Some(lower_reflection_instance_method_invoke( + ctx, + &reflected_method, + &object_arg, + &forwarded_args, + expr, + )), } } -/// Returns the PHP type produced by a cast. -fn cast_php_type(target: &CastType) -> PhpType { - match target { - CastType::Int => PhpType::Int, - CastType::Float => PhpType::Float, - CastType::String => PhpType::Str, - CastType::Bool => PhpType::Bool, - CastType::Array => PhpType::Array(Box::new(PhpType::Mixed)), +/// Lowers a static reflected-method invocation after evaluating the ignored object slot. +fn lower_reflection_static_method_invoke( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + reflected_method: &str, + object_arg: &Expr, + forwarded_args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let ignored_object = lower_expr(ctx, object_arg); + if ctx.value_is_owning_temporary(ignored_object) { + crate::ir_lower::ownership::release_if_owned(ctx, ignored_object, Some(object_arg.span)); } + let receiver = StaticReceiver::Named(Name::from(class_name.to_string())); + lower_static_method_call(ctx, &receiver, reflected_method, forwarded_args, expr) } -/// Lowers a closure expression into a callable descriptor backed by an EIR closure function. -fn lower_closure( +/// Lowers an instance reflected-method invocation using the first invoke argument as receiver. +fn lower_reflection_instance_method_invoke( ctx: &mut LoweringContext<'_, '_>, - params: &[(String, Option, Option, bool)], - variadic: Option<&str>, - return_type: Option<&TypeExpr>, - body: &[crate::parser::ast::Stmt], - captures: &[String], - capture_refs: &[String], + reflected_method: &str, + object_arg: &Expr, + forwarded_args: &[Expr], expr: &Expr, - is_static: bool, ) -> LoweredValue { - lower_closure_with_context( + let object = lower_expr(ctx, object_arg); + if value_is_definitely_null(ctx, object.value) { + let null_value = lower_null(ctx, expr); + terminate_method_call_on_null(ctx, reflected_method); + return null_value; + } + if value_is_nullable(ctx, object.value) { + return lower_nullable_regular_method_call( + ctx, + object, + reflected_method, + forwarded_args, + expr, + ); + } + lower_method_call_with_receiver( ctx, - params, - variadic, - return_type, - body, - captures, - capture_refs, + object, + reflected_method, + forwarded_args, + Op::MethodCall, expr, - &[], - None, - is_static, ) } -/// Lowers a closure assigned to a local and specializes self by-reference captures as callable. -pub(crate) fn lower_closure_for_assignment( - ctx: &mut LoweringContext<'_, '_>, - assigned_name: &str, - value: &Expr, -) -> Option { - let ExprKind::Closure { - params, - variadic, - return_type, - body, - captures, - capture_refs, - is_static, - .. - } = &value.kind - else { +/// Splits `ReflectionMethod::invoke($object, ...$args)` into receiver and method args. +fn reflection_method_invoke_args(args: &[Expr]) -> Option<(Expr, Vec)> { + let args = reflection_class_new_instance_args(args); + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [object, forwarded @ ..] => Some((object.clone(), forwarded.to_vec())), + _ => None, + }; + } + let mut object = None; + let mut forwarded = Vec::new(); + let mut args = args.into_iter(); + if let Some(first) = args.next() { + match first.kind { + ExprKind::NamedArg { + ref name, + ref value, + } if php_symbol_key(name) == "object" => { + object = Some((**value).clone()); + } + ExprKind::NamedArg { .. } => forwarded.push(first), + _ => object = Some(first), + } + } + for arg in args { + match arg.kind { + ExprKind::NamedArg { + ref name, + ref value, + } if php_symbol_key(name) == "object" => { + if object.replace((**value).clone()).is_some() { + return None; + } + } + _ => forwarded.push(arg), + } + } + object.map(|object| (object, forwarded)) +} + +/// Splits `ReflectionMethod::invokeArgs($object, $args)` into receiver and method args. +fn reflection_method_invoke_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option<(Expr, Vec)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { return None; - }; - if !capture_refs.iter().any(|capture| capture == assigned_name) { + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [object, forwarded] => { + let forwarded = reflection_class_new_instance_args_value(ctx, forwarded)?; + Some((object.clone(), forwarded)) + } + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionMethod") + .and_then(|class_info| class_info.methods.get(&php_symbol_key("invokeArgs")))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { return None; } - Some(lower_closure_with_context( - ctx, - params, - variadic.as_deref(), - return_type.as_ref(), - body, - captures, - capture_refs, - value, - &[], - Some(assigned_name), - *is_static, - )) + let object = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let forwarded_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?; + let forwarded = reflection_class_new_instance_args_value(ctx, forwarded_arg)?; + Some((object, forwarded)) } -/// Lowers a closure expression, applying contextual types to unannotated parameters. -fn lower_closure_with_context( +/// Classifies whether a known reflected method is static or instance-dispatched. +fn reflection_method_target_kind( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let method_key = php_symbol_key(method); + if class_info.static_methods.contains_key(&method_key) { + return Some(ReflectionMethodTargetKind::Static); + } + if class_info.methods.contains_key(&method_key) { + return Some(ReflectionMethodTargetKind::Instance); + } + None +} + +/// Dispatch kind for a statically-known reflected method. +#[derive(Clone, Copy)] +enum ReflectionMethodTargetKind { + Instance, + Static, +} + +/// Emits a runtime fatal for ReflectionMethod invocation forms not yet lowered. +fn lower_reflection_method_invoke_unsupported( ctx: &mut LoweringContext<'_, '_>, - params: &[(String, Option, Option, bool)], - variadic: Option<&str>, - return_type: Option<&TypeExpr>, - body: &[crate::parser::ast::Stmt], - captures: &[String], - capture_refs: &[String], + method_key: &str, expr: &Expr, - contextual_arg_types: &[PhpType], - self_ref_callable_capture: Option<&str>, - is_static: bool, ) -> LoweredValue { - // PHP auto-binds `$this` to non-static closures (including arrow functions) - // defined inside an instance method, with no `use($this)` needed. The parser - // never lists `$this` as a capture, so thread it through the existing capture - // machinery here: load the enclosing `this` and append it to the captures so - // the closure body gets a `this` local. Only capture when the body actually - // references `$this` (directly or in a nested closure) — adding an unused - // capture would push otherwise capture-free closures through capture-only - // runtime paths. Nested closures compose: each level captures `this` from the - // level above. - // A method-defined closure loads the enclosing `this`; a top-level closure - // that uses `$this` (bound later via `Closure::bind`) gets a null `this` - // slot the bind fills, typed `Mixed` for runtime-dispatched member access. - let with_this; - let captures: &[String] = if !is_static - && !captures.iter().any(|name| name == "this") - && crate::types::checker::closure_body_uses_this(body) - { - with_this = captures - .iter() - .cloned() - .chain(std::iter::once("this".to_string())) - .collect::>(); - &with_this + let result = lower_boxed_null(ctx, expr); + let method_name = if method_key == "invokeargs" { + "invokeArgs" } else { - captures + "invoke" }; - let mut captured_values = Vec::with_capacity(captures.len()); - let mut capture_params = Vec::with_capacity(captures.len()); - for capture in captures { - let by_ref = capture_refs.iter().any(|name| name == capture); - let (captured, php_type) = if capture == "this" && !ctx.local_slots.contains_key("this") { - // Top-level closure: no enclosing `$this`. Start with a null receiver - // that `Closure::bind` overwrites; `Mixed` so members dispatch at - // runtime against the bound object's class. - (lower_null(ctx, expr), PhpType::Mixed) - } else { - let captured = ctx.load_local(capture, Some(expr.span)); - let php_type = if by_ref && self_ref_callable_capture == Some(capture.as_str()) { - PhpType::Callable - } else { - ctx.builder.value_php_type(captured.value) - }; - (captured, php_type) - }; - let immediate = by_ref.then_some(Immediate::I64(1)); - ctx.emit_void(Op::ClosureCapture, vec![captured.value], immediate, Op::ClosureCapture.default_effects(), Some(expr.span)); - if by_ref { - ctx.mark_ref_bound_local(capture); + let message = ctx.intern_string(&format!( + "Fatal error: unsupported ReflectionMethod::{}() target or argument forwarding\n", + method_name + )); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + +/// Lowers `ReflectionProperty::getValue($object)` when the reflected property is known. +fn lower_reflection_property_value_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object_expr = object_expr?; + match php_symbol_key(method).as_str() { + "getvalue" => { + if let Some((declaring_class, property, property_ty)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_get_static_value( + ctx, + &declaring_class, + &property, + property_ty, + args, + expr, + ); + } + let (_, property, _) = reflection_property_instance_target(ctx, object_expr)?; + lower_reflection_property_get_value(ctx, &property, args, expr) } - captured_values.push(ClosureCapture { value: captured.value }); - capture_params.push((capture.clone(), php_type, by_ref)); + "setvalue" => { + if let Some((declaring_class, property, _)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_set_static_value( + ctx, + &declaring_class, + &property, + args, + expr, + ); + } + let (_, property, _) = reflection_property_instance_target(ctx, object_expr)?; + lower_reflection_property_set_value(ctx, &property, args, expr) + } + "isinitialized" => { + if let Some((declaring_class, property, _)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_static_is_initialized( + ctx, + &declaring_class, + &property, + args, + expr, + ); + } + let (_, property, _) = reflection_property_any_instance_target(ctx, object_expr)?; + lower_reflection_property_is_initialized(ctx, &property, args, expr) + } + _ => None, } - let name = ctx.next_closure_name(); - let by_ref_return = matches!(&expr.kind, ExprKind::Closure { by_ref_return: true, .. }); - let signature = if contextual_arg_types.is_empty() { - function::lower_closure_function( - ctx, - &name, - params, - variadic, - return_type, - body, - &capture_params, - self_ref_callable_capture, - by_ref_return, - ) - } else { - function::lower_closure_function_with_context( - ctx, - &name, - params, - variadic, - return_type, - body, - &capture_params, - contextual_arg_types, - self_ref_callable_capture, - by_ref_return, - ) - }; - let data = ctx.intern_string(&name); - let closure_operands = captured_values - .iter() - .map(|capture| capture.value) - .collect::>(); - ctx.set_pending_static_callable_result(StaticCallableBinding::Closure { - name, - signature, - captures: captured_values, - }); - let closure = ctx.emit_value( - Op::ClosureNew, - closure_operands, +} + +/// Lowers `ReflectionProperty::getValue($object)` to a direct property read. +fn lower_reflection_property_get_value( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object_arg = reflection_property_get_value_arg(args)?; + let object = lower_expr(ctx, &object_arg); + Some(lower_property_get_from_value( + ctx, + object, + property, + Op::PropGet, + expr, + )) +} + +/// Lowers `ReflectionProperty::setValue($object, $value)` to a direct property write. +fn lower_reflection_property_set_value( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let (object_arg, value_arg) = reflection_property_set_value_args(args)?; + let target = Expr::new( + ExprKind::PropertyAccess { + object: Box::new(object_arg), + property: property.to_string(), + }, + expr.span, + ); + lower_non_local_assignment_write(ctx, &target, &value_arg, expr.span); + Some(lower_null(ctx, expr)) +} + +/// Lowers `ReflectionProperty::isInitialized($object)` to a direct slot probe. +fn lower_reflection_property_is_initialized( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object_arg = reflection_property_get_value_arg(args)?; + let object = lower_expr(ctx, &object_arg); + let data = ctx.intern_string(property); + Some(ctx.emit_value( + Op::PropInitialized, + vec![object.value], Some(Immediate::Data(data)), - PhpType::Callable, - Op::ClosureNew.default_effects(), + PhpType::Bool, + Op::PropInitialized.default_effects(), Some(expr.span), - ); - if let Some(capture) = self_ref_callable_capture { - ctx.set_local_logical_type(capture, PhpType::Callable); - } - closure + )) } -/// Lowers a closure variable call. -fn lower_closure_call(ctx: &mut LoweringContext<'_, '_>, var: &str, args: &[Expr], expr: &Expr) -> LoweredValue { - if let Some(value) = lower_invokable_object_variable_call(ctx, var, args, expr) { - return value; - } - let mut result_type = None; - let mut instance_signature = None; - if let Some(target) = ctx.static_callable_local(var) { - result_type = Some(static_callable_return_type(ctx, &target)); - instance_signature = instance_callable_signature(&target).cloned(); - if let Some(value) = lower_static_callable_call(ctx, target, args, expr) { - return value; - } - } - let callable = ctx.load_local(var, Some(expr.span)); - let result_type = result_type.unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); - if instance_signature.is_none() { - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { - return emit_callable_descriptor_invoke( - ctx, - callable, - arg_container, - result_type, - expr.span, - ); - } +/// Lowers static `ReflectionProperty::getValue()` to a reflection static-property read. +fn lower_reflection_property_get_static_value( + ctx: &mut LoweringContext<'_, '_>, + declaring_class: &str, + property: &str, + property_ty: PhpType, + args: &[Expr], + expr: &Expr, +) -> Option { + if let Some(ignored_object) = reflection_property_static_get_value_ignored_arg(args)? { + lower_ignored_reflection_argument(ctx, &ignored_object); } - let mut operands = vec![callable.value]; - operands.extend(lower_args_with_signature(ctx, instance_signature.as_ref(), args)); - ctx.emit_value(Op::ClosureCall, operands, None, result_type, Op::ClosureCall.default_effects(), Some(expr.span)) + Some(lower_reflection_static_property_get_by_class_name( + ctx, + declaring_class, + property, + property_ty, + expr, + )) } -/// Lowers `$object(...)` when the local object has an `__invoke` method. -fn lower_invokable_object_variable_call( +/// Lowers static `ReflectionProperty::isInitialized()` to a direct static-slot probe. +fn lower_reflection_property_static_is_initialized( ctx: &mut LoweringContext<'_, '_>, - var: &str, + declaring_class: &str, + property: &str, args: &[Expr], expr: &Expr, ) -> Option { - let object = Expr::new(ExprKind::Variable(var.to_string()), expr.span); - lower_invokable_object_expr_call(ctx, &object, args, expr) + if let Some(ignored_object) = reflection_property_static_get_value_ignored_arg(args)? { + lower_ignored_reflection_argument(ctx, &ignored_object); + } + Some(lower_reflection_static_property_initialized_by_class_name( + ctx, + declaring_class, + property, + expr, + )) } -/// Lowers invokable object calls through the normal method-call path. -fn lower_invokable_object_expr_call( +/// Lowers static `ReflectionProperty::setValue(null, $value)` to a reflection static-property write. +fn lower_reflection_property_set_static_value( ctx: &mut LoweringContext<'_, '_>, - callee: &Expr, + declaring_class: &str, + property: &str, args: &[Expr], expr: &Expr, ) -> Option { - if !is_invokable_object_expr(ctx, callee) { + let (ignored_object, value_arg) = reflection_property_static_set_value_args(args)?; + lower_ignored_reflection_argument(ctx, &ignored_object); + let value = lower_expr(ctx, &value_arg); + store_reflection_static_property_by_class_name( + ctx, + declaring_class, + property, + value.value, + expr.span, + ); + Some(lower_null(ctx, expr)) +} + +/// Evaluates an ignored Reflection argument and releases temporary objects. +fn lower_ignored_reflection_argument(ctx: &mut LoweringContext<'_, '_>, arg: &Expr) { + let value = lower_expr(ctx, arg); + if ctx.value_is_owning_temporary(value) { + crate::ir_lower::ownership::release_if_owned(ctx, value, Some(arg.span)); + } +} + +/// Returns the explicit object argument passed to `ReflectionProperty::getValue()`. +fn reflection_property_get_value_arg(args: &[Expr]) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { return None; } - Some(lower_method_call(ctx, callee, "__invoke", args, Op::MethodCall, expr)) + let object = if !crate::types::call_args::has_named_args(&args) { + match args.as_slice() { + [object] => object.clone(), + _ => return None, + } + } else { + reflection_property_named_object_arg(&args)? + }; + (!matches!(&object.kind, ExprKind::Null)).then_some(object) } -/// Returns true when an expression is known to evaluate to an object with `__invoke`. -fn is_invokable_object_expr( - ctx: &LoweringContext<'_, '_>, - callee: &Expr, -) -> bool { - instance_callable_object_class(ctx, callee) - .and_then(|class_name| class_method_signature(ctx, &class_name, "__invoke")) - .is_some() +/// Returns the explicit object and value arguments passed to `ReflectionProperty::setValue()`. +fn reflection_property_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (object, value) = + reflection_class_static_property_regular_args(&args, "object", Some("value"))?; + let object = object?; + if matches!(&object.kind, ExprKind::Null) { + return None; + } + Some((object, value?)) } -/// Lowers an expression call. -fn lower_expr_call(ctx: &mut LoweringContext<'_, '_>, callee: &Expr, args: &[Expr], expr: &Expr) -> LoweredValue { - if let Some(value) = lower_invokable_object_expr_call(ctx, callee, args, expr) { - return value; +/// Returns the optional ignored object argument for static `ReflectionProperty::getValue()`. +fn reflection_property_static_get_value_ignored_arg(args: &[Expr]) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } - if let Some(value) = lower_first_class_callable_expr_call(ctx, callee, args, expr) { - return value; + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [] => Some(None), + [object] => Some(Some(object.clone())), + _ => None, + }; } - if let Some(value) = lower_literal_callable_array_expr_call(ctx, callee, args, expr) { - return value; + reflection_property_named_optional_object_arg(&args) +} + +/// Returns the ignored object and value arguments for static `ReflectionProperty::setValue()`. +fn reflection_property_static_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } - if let Some(callback) = static_call_user_func_callback(ctx, callee) { - if let Some(value) = lower_static_callable_call(ctx, callback, args, expr) { - return value; + let (object, value) = + reflection_class_static_property_regular_args(&args, "object", Some("value"))?; + Some((object?, value?)) +} + +/// Returns a required named `object` argument for ReflectionProperty value access. +fn reflection_property_named_object_arg(args: &[Expr]) -> Option { + reflection_property_named_optional_object_arg(args)? +} + +/// Returns an optional named `object` argument for ReflectionProperty value access. +fn reflection_property_named_optional_object_arg(args: &[Expr]) -> Option> { + let mut object = None; + for arg in args { + match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "object" => { + object = Some((**value).clone()); + } + _ => return None, } } - if let Some(callback) = static_assignment_callable_target(ctx, callee) { - lower_expr(ctx, callee); - if let Some(value) = lower_static_callable_call(ctx, callback, args, expr) { - return value; - } + Some(object) +} + +/// Resolves an inline `new ReflectionProperty(Known::class, "prop")` instance property target. +fn reflection_property_instance_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + if class_info + .static_properties + .iter() + .any(|(name, _)| name == &property) + { + return None; } - // `Closure::bind(fn &() => $this->prop, $obj, $obj)()` invokes the bound closure. Lower it - // as a direct call to the closure with `$obj` boxed as its `$this` capture, so a - // by-reference return passes the property's ref-cell pointer through (the generic runtime - // descriptor invoker boxes results and cannot). - if let Some(value) = lower_bound_closure_immediate_call(ctx, callee, args, expr) { - return value; + if class_info.property_visibilities.get(&property) != Some(&Visibility::Public) { + return None; } - let lowered_callee = lower_expr(ctx, callee); - // An immediately-invoked closure literal (`(fn &() => …)()`) registers its static - // callable binding while lowering. Call it directly through the static-callable path - // (as `$f()` does) so the closure body's signature — including a by-reference return — - // drives the call instead of the generic descriptor-invoke path, which cannot return - // every result type. - if let Some(target) = ctx.take_pending_static_callable_result() { - if let Some(value) = lower_static_callable_call(ctx, target, args, expr) { - return value; + let (_, (_, property_ty)) = class_info.visible_property(&property)?; + Some(( + class_name, + property, + normalize_value_php_type(property_ty.codegen_repr()), + )) +} + +/// Resolves a known non-static ReflectionProperty target without enforcing visibility. +fn reflection_property_any_instance_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + if class_info + .static_properties + .iter() + .any(|(name, _)| name == &property) + { + return None; + } + let (_, (_, property_ty)) = class_info.visible_property(&property)?; + Some(( + class_name, + property, + normalize_value_php_type(property_ty.codegen_repr()), + )) +} + +/// Resolves an inline `ReflectionProperty` target for a static property. +fn reflection_property_static_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; + let (declaring_class, property_ty) = + reflection_class_static_property_target(ctx, &class_name, &property)?; + Some((declaring_class, property, property_ty)) +} + +/// Extracts the known class and property name from a supported ReflectionProperty source. +fn reflection_property_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + reflection_property_constructor_target(ctx, object_expr) + .or_else(|| reflection_property_class_get_property_target(ctx, object_expr)) + .or_else(|| reflection_property_class_get_properties_index_target(ctx, object_expr)) + .or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_property_local(name) + }) +} + +/// Extracts the known class and method name from a supported ReflectionMethod source. +fn reflection_method_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + reflection_method_constructor_target(ctx, object_expr) + .or_else(|| reflection_method_class_get_constructor_target(ctx, object_expr)) + .or_else(|| reflection_method_class_get_method_target(ctx, object_expr)) + .or_else(|| reflection_method_class_get_methods_index_target(ctx, object_expr)) + .or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_method_local(name) + }) +} + +/// Extracts the known function name from a supported ReflectionFunction source. +fn reflection_function_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + reflection_function_constructor_target(ctx, object_expr).or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_function_local(name) + }) +} + +/// Extracts a known ReflectionMethod from `ReflectionClass::getMethods()[N]`. +fn reflection_method_class_get_methods_index_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::ArrayAccess { array, index } = &object_expr.kind else { + return None; + }; + let ExprKind::IntLiteral(raw_index) = &index.kind else { + return None; + }; + if *raw_index < 0 { + return None; + } + let ExprKind::MethodCall { + object, + method, + args, + } = &array.kind + else { + return None; + }; + if php_symbol_key(method) != "getmethods" { + return None; + } + let filter = reflection_class_get_methods_filter_arg(ctx, args)?; + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = + reflection_class_method_name_at_index(ctx, &class_name, *raw_index as usize, filter)?; + Some((class_name, method)) +} + +/// Returns the `ReflectionClass::getMethods()` method name at a known index. +fn reflection_class_method_name_at_index( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + index: usize, + filter: Option, +) -> Option { + reflection_class_method_names_for_filter(ctx, class_name, filter)? + .into_iter() + .nth(index) +} + +/// Extracts a known ReflectionProperty from `ReflectionClass::getProperties()[N]`. +fn reflection_property_class_get_properties_index_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::ArrayAccess { array, index } = &object_expr.kind else { + return None; + }; + let ExprKind::IntLiteral(raw_index) = &index.kind else { + return None; + }; + if *raw_index < 0 { + return None; + } + let ExprKind::MethodCall { + object, + method, + args, + } = &array.kind + else { + return None; + }; + if php_symbol_key(method) != "getproperties" { + return None; + } + let filter = reflection_class_get_properties_filter_arg(ctx, args)?; + let class_name = reflection_class_reflected_class(ctx, object)?; + let property = + reflection_class_property_name_at_index(ctx, &class_name, *raw_index as usize, filter)?; + Some((class_name, property)) +} + +/// Returns the `ReflectionClass::getProperties()` property name at a known index. +fn reflection_class_property_name_at_index( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + index: usize, + filter: Option, +) -> Option { + reflection_class_property_names_for_filter(ctx, class_name, filter)? + .into_iter() + .nth(index) +} + +/// Returns `ReflectionClass::getProperties()` names after applying a known filter. +fn reflection_class_property_names_for_filter( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + filter: Option, +) -> Option> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + Some( + class_info + .properties + .iter() + .chain(class_info.static_properties.iter()) + .map(|(name, _)| name) + .filter(|name| reflection_property_matches_filter(class_info, name, filter)) + .cloned() + .collect(), + ) +} + +/// Returns `ReflectionClass::getMethods()` names after applying a known filter. +fn reflection_class_method_names_for_filter( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + filter: Option, +) -> Option> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in class_info + .methods + .keys() + .chain(class_info.static_methods.keys()) + { + if seen.insert(php_symbol_key(name)) + && reflection_method_matches_filter(class_info, name, filter) + { + names.push(name.clone()); } } - let result_type = dynamic_callable_result_type(ctx, lowered_callee.value, expr); - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { - return emit_callable_descriptor_invoke( - ctx, - lowered_callee, - arg_container, - result_type, - expr.span, - ); - } - let mut operands = vec![lowered_callee.value]; - operands.extend(lower_args(ctx, args)); - ctx.emit_value(Op::ExprCall, operands, None, result_type, Op::ExprCall.default_effects(), Some(expr.span)) + Some(names) } -/// Lowers direct calls to literal callable arrays through descriptor metadata. -fn lower_literal_callable_array_expr_call( - ctx: &mut LoweringContext<'_, '_>, - callee: &Expr, +/// Returns the optional `ReflectionClass::getProperties()` modifier filter. +fn reflection_class_get_properties_filter_arg( + ctx: &LoweringContext<'_, '_>, args: &[Expr], - expr: &Expr, -) -> Option { - let ExprKind::ArrayLiteral(items) = &callee.kind else { +) -> Option> { + reflection_class_member_filter_arg(ctx, args, "ReflectionProperty") +} + +/// Returns the optional `ReflectionClass::getMethods()` modifier filter. +fn reflection_class_get_methods_filter_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + reflection_class_member_filter_arg(ctx, args, "ReflectionMethod") +} + +/// Returns the optional ReflectionClass member-list modifier filter. +fn reflection_class_member_filter_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + constant_class: &str, +) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { return None; - }; - if let Some(StaticCallableBinding::StaticMethodDescriptor { receiver, method }) = - static_array_callable_descriptor_target(ctx, items) - { - return Some(lower_static_method_descriptor_call(ctx, &receiver, &method, args, expr)); } - instance_array_callable_target(ctx, items)?; - let lowered_callee = lower_expr(ctx, callee); - let result_type = dynamic_callable_result_type(ctx, lowered_callee.value, expr); - let arg_container = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; - Some(emit_callable_descriptor_invoke( - ctx, - lowered_callee, - arg_container, - result_type, - expr.span, - )) + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [] => Some(None), + [filter] => reflection_member_filter_value(ctx, filter, constant_class), + _ => None, + }; + } + let (filter, _) = reflection_class_static_property_regular_args(&args, "filter", None)?; + filter + .as_ref() + .map(|filter| reflection_member_filter_value(ctx, filter, constant_class)) + .unwrap_or(Some(None)) } -/// Lowers an expression call once the callable expression is already evaluated. -fn lower_expr_call_from_value( - ctx: &mut LoweringContext<'_, '_>, - callee: LoweredValue, - args: &[Expr], +/// Returns a known integer modifier filter expression. +fn reflection_member_filter_value( + ctx: &LoweringContext<'_, '_>, expr: &Expr, -) -> LoweredValue { - let result_type = dynamic_callable_result_type(ctx, callee.value, expr); - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { - return emit_callable_descriptor_invoke( - ctx, - callee, - arg_container, - result_type, - expr.span, - ); + constant_class: &str, +) -> Option> { + match &expr.kind { + ExprKind::Null => Some(None), + ExprKind::IntLiteral(value) => Some(Some(*value)), + ExprKind::ScopedConstantAccess { receiver, name } => Some(Some( + reflection_member_filter_constant(ctx, receiver, name, constant_class)?, + )), + _ => None, } - let mut operands = vec![callee.value]; - operands.extend(lower_args(ctx, args)); - ctx.emit_value( - Op::ExprCall, - operands, - None, - result_type, - Op::ExprCall.default_effects(), - Some(expr.span), - ) } -/// Lowers explicit named arguments for signature-unknown descriptor invocations. -fn lower_untyped_descriptor_invoker_arg_container( - ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - span: Span, -) -> Option { - if crate::types::call_args::has_named_args(args) { - return Some(lower_untyped_descriptor_invoker_hash_container(ctx, args, span)); +/// Resolves a `Reflection*::IS_*` class constant to its integer value. +fn reflection_member_filter_constant( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, + name: &str, + constant_class: &str, +) -> Option { + let class_name = static_receiver_class_name(ctx, receiver)?; + if php_symbol_key(class_name.trim_start_matches('\\')) != php_symbol_key(constant_class) { + return None; } - Some(lower_untyped_descriptor_invoker_indexed_container(ctx, args, span)) + let value = ctx.scoped_constant_value(&class_name, name)?; + let ExprKind::IntLiteral(value) = value.kind else { + return None; + }; + Some(value) } -/// Builds an indexed descriptor-invoker container for signature-unknown calls. -fn lower_untyped_descriptor_invoker_indexed_container( - ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - span: Span, -) -> LoweredValue { - let elem_ty = PhpType::Mixed; - let array_ty = PhpType::Array(Box::new(elem_ty.clone())); - let array = ctx.emit_value( - Op::ArrayNew, - Vec::new(), - Some(Immediate::Capacity(args.len() as u32)), - array_ty.clone(), - Op::ArrayNew.default_effects(), - Some(span), - ); - for arg in args { - if let ExprKind::Spread(inner) = &arg.kind { - let source = lower_expr(ctx, inner); - lower_indexed_array_spread_into_array(ctx, array, source, Some(&elem_ty), arg.span); - continue; - } - let value = lower_untyped_descriptor_invoker_arg_value(ctx, arg); - ctx.emit_void( - Op::ArrayPush, - vec![array.value, value.value], +/// Returns whether a method should be present for a modifier filter. +fn reflection_method_matches_filter( + class_info: &crate::types::ClassInfo, + method: &str, + filter: Option, +) -> bool { + let Some(filter) = filter else { + return true; + }; + reflection_method_filter_modifiers(class_info, method) + .is_some_and(|modifiers| modifiers & filter != 0) +} + +/// Returns whether a property should be present for a modifier filter. +fn reflection_property_matches_filter( + class_info: &crate::types::ClassInfo, + property: &str, + filter: Option, +) -> bool { + let Some(filter) = filter else { + return true; + }; + reflection_property_filter_modifiers(class_info, property) + .is_some_and(|modifiers| modifiers & filter != 0) +} + +/// Computes ReflectionMethod modifier bits for static filter resolution. +fn reflection_method_filter_modifiers( + class_info: &crate::types::ClassInfo, + method: &str, +) -> Option { + let method_key = php_symbol_key(method); + if class_info.methods.contains_key(&method_key) { + let visibility = class_info + .method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_method_filter_modifier_bits( + visibility, + false, + class_info.final_methods.contains(&method_key), + !class_info.method_impl_classes.contains_key(&method_key), + )); + } + if class_info.static_methods.contains_key(&method_key) { + let visibility = class_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_method_filter_modifier_bits( + visibility, + true, + class_info.final_static_methods.contains(&method_key), + !class_info + .static_method_impl_classes + .contains_key(&method_key), + )); + } + None +} + +/// Computes ReflectionProperty modifier bits for static filter resolution. +fn reflection_property_filter_modifiers( + class_info: &crate::types::ClassInfo, + property: &str, +) -> Option { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { + let visibility = class_info + .property_visibilities + .get(property) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_filter_modifier_bits( + visibility, + false, + class_info.final_properties.contains(property), + class_info.abstract_properties.contains(property), + class_info.readonly_properties.contains(property), + reflection_property_filter_is_virtual(class_info, property), + class_info.property_set_visibilities.get(property), + )); + } + if class_info + .static_properties + .iter() + .any(|(name, _)| name == property) + { + let visibility = class_info + .static_property_visibilities + .get(property) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_filter_modifier_bits( + visibility, + true, + class_info.final_static_properties.contains(property), + false, + false, + false, None, - Op::ArrayPush.default_effects(), - Some(arg.span), - ); - super::stmt::release_indexed_array_write_operand(ctx, Some(&elem_ty), value, arg.span); + )); } - array + None } -/// Builds an associative descriptor-invoker container for named or named/spread calls. -fn lower_untyped_descriptor_invoker_hash_container( - ctx: &mut LoweringContext<'_, '_>, - args: &[Expr], - span: Span, -) -> LoweredValue { - let hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), +/// Builds the ReflectionMethod modifier bitmask for filter matching. +fn reflection_method_filter_modifier_bits( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, }; - let hash = ctx.emit_value( - Op::HashNew, - Vec::new(), - Some(Immediate::Capacity(args.len() as u32)), - hash_ty, - Op::HashNew.default_effects(), - Some(span), - ); - let mut next_positional_key = emit_i64_at_span(ctx, 0, span); - for arg in args { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - let key = lower_string_literal(ctx, name, arg); - let value = lower_untyped_descriptor_invoker_arg_value(ctx, value); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(arg.span), - ); - } - ExprKind::Spread(inner) => { - let source = lower_expr(ctx, inner); - next_positional_key = lower_untyped_descriptor_invoker_spread_into_hash( - ctx, - hash, - source, - next_positional_key, - arg.span, - ); - } - _ => { - let key = next_positional_key; - let value = lower_untyped_descriptor_invoker_arg_value(ctx, arg); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(arg.span), - ); - let one = emit_i64_at_span(ctx, 1, arg.span); - next_positional_key = ctx.emit_value( - Op::IAdd, - vec![key.value, one.value], - None, - PhpType::Int, - Op::IAdd.default_effects(), - Some(arg.span), - ); + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Returns whether a property has hook metadata that makes it virtual. +fn reflection_property_filter_is_virtual( + class_info: &crate::types::ClassInfo, + property: &str, +) -> bool { + let get_method = php_symbol_key(&property_hook_get_method(property)); + let set_method = php_symbol_key(&property_hook_set_method(property)); + class_info.abstract_property_hooks.contains_key(property) + || class_info.methods.contains_key(&get_method) + || class_info.methods.contains_key(&set_method) +} + +/// Builds the ReflectionProperty modifier bitmask for filter matching. +fn reflection_property_filter_modifier_bits( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, + set_visibility: Option<&Visibility>, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(Visibility::Private) => modifiers |= 32 | 4096, + Some(Visibility::Protected) => modifiers |= 2048, + Some(Visibility::Public) | None => { + if is_readonly && visibility == &Visibility::Public { + modifiers |= 2048; } } } - ctx.emit_value( - Op::MixedBox, - vec![hash.value], - None, - PhpType::Mixed, - Op::MixedBox.default_effects(), - Some(span), - ) + modifiers +} + +/// Extracts the known function name from an inline ReflectionFunction constructor. +fn reflection_function_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionfunction" { + return None; + } + let function_arg = reflection_function_constructor_regular_arg(ctx, args)?; + let ExprKind::StringLiteral(function_name) = function_arg.kind else { + return None; + }; + resolve_known_reflection_function_name(ctx, &function_name) +} + +/// Resolves function names accepted by static `ReflectionFunction` metadata. +fn resolve_known_reflection_function_name( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> Option { + resolve_known_function_name(ctx, function_name) + .or_else(|| resolve_known_reflection_builtin_name(function_name)) } -/// Copies an indexed spread source into a descriptor-invoker hash with numeric keys. -fn lower_untyped_descriptor_invoker_spread_into_hash( - ctx: &mut LoweringContext<'_, '_>, - hash: LoweredValue, - source: LoweredValue, - start_key: LoweredValue, - span: Span, -) -> LoweredValue { - let source_elem_ty = match ctx.builder.value_php_type(source.value).codegen_repr() { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Mixed, +/// Resolves a supported callable builtin name for `ReflectionFunction`. +fn resolve_known_reflection_builtin_name(function_name: &str) -> Option { + let canonical = canonical_builtin_function_name(function_name.trim_start_matches('\\'))?; + first_class_builtin_signature(&canonical).map(|_| canonical) +} + +/// Extracts the known class and property name from an inline ReflectionProperty constructor. +fn reflection_property_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; }; - let len = ctx.emit_value( - Op::ArrayLen, - vec![source.value], - None, - PhpType::Int, - Op::ArrayLen.default_effects(), - Some(span), - ); - let zero = emit_i64_at_span(ctx, 0, span); - let header = ctx.builder.create_named_block("descriptor.spread.next", vec![(IrType::I64, PhpType::Int)]); - let body = ctx.builder.create_named_block("descriptor.spread.body", Vec::new()); - let exit = ctx.builder.create_named_block("descriptor.spread.exit", Vec::new()); - ctx.builder.terminate(Terminator::Br { target: header, args: vec![zero.value] }); + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionproperty" { + return None; + } + let (class_arg, property_arg) = reflection_property_constructor_regular_args(ctx, args)?; + let raw_class_name = match &class_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, + }; + let class_name = resolve_known_class_name(ctx, &raw_class_name)?; + let ExprKind::StringLiteral(property) = property_arg.kind else { + return None; + }; + Some((class_name, property)) +} - ctx.builder.position_at_end(header); - let index = ctx.builder.block_param(header, 0); - let has_next = ctx.emit_value( - Op::ICmp, - vec![index, len.value], - Some(Immediate::CmpPredicate(CmpPredicate::Slt)), - PhpType::Bool, - Op::ICmp.default_effects(), - Some(span), - ); - ctx.builder.terminate(Terminator::CondBr { - cond: has_next.value, - then_target: body, - then_args: Vec::new(), - else_target: exit, - else_args: Vec::new(), - }); +/// Extracts the known class and method name from an inline ReflectionMethod constructor. +fn reflection_method_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionmethod" { + return None; + } + let (class_arg, method_arg) = reflection_method_constructor_regular_args(ctx, args)?; + let raw_class_name = match &class_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, + }; + let class_name = resolve_known_class_name(ctx, &raw_class_name)?; + let ExprKind::StringLiteral(method) = method_arg.kind else { + return None; + }; + let method = resolve_known_class_method_name(ctx, &class_name, &method)?; + Some((class_name, method)) +} - ctx.builder.position_at_end(body); - let key = ctx.emit_value( - Op::IAdd, - vec![start_key.value, index], - None, - PhpType::Int, - Op::IAdd.default_effects(), - Some(span), - ); - let value = ctx.emit_value( - Op::ArrayGet, - vec![source.value, index], - None, - source_elem_ty, - Op::ArrayGet.default_effects(), - Some(span), - ); - let value = coerce_descriptor_invoker_mixed_value(ctx, value, span); - ctx.emit_void( - Op::HashSet, - vec![hash.value, key.value, value.value], - None, - Op::HashSet.default_effects(), - Some(span), - ); - release_value_after_retaining_insert(ctx, Some(&PhpType::Mixed), value, span); - let one = emit_i64_at_span(ctx, 1, span); - let next = ctx.emit_value( - Op::IAdd, - vec![index, one.value], - None, - PhpType::Int, - Op::IAdd.default_effects(), - Some(span), - ); - ctx.builder.terminate(Terminator::Br { target: header, args: vec![next.value] }); +/// Extracts the constructor target from inline `ReflectionClass::getConstructor()` calls. +fn reflection_method_class_get_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; + }; + if php_symbol_key(method) != "getconstructor" { + return None; + } + if !reflection_class_new_instance_args(args).is_empty() { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = resolve_known_class_method_name(ctx, &class_name, "__construct")?; + Some((class_name, method)) +} - ctx.builder.position_at_end(exit); - crate::ir_lower::ownership::release_if_owned(ctx, source, Some(span)); - ctx.emit_value( - Op::IAdd, - vec![start_key.value, len.value], - None, - PhpType::Int, - Op::IAdd.default_effects(), - Some(span), - ) +/// Extracts the property target from inline `ReflectionClass::getProperty()` calls. +fn reflection_property_class_get_property_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; + }; + if php_symbol_key(method) != "getproperty" { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let property = reflection_class_member_name_arg(args)?; + Some((class_name, property)) } -/// Lowers one untyped descriptor argument, preserving variables as ref markers. -fn lower_untyped_descriptor_invoker_arg_value( - ctx: &mut LoweringContext<'_, '_>, - arg: &Expr, -) -> LoweredValue { - let value = match &arg.kind { - ExprKind::Variable(name) => lower_invoker_ref_arg_marker(ctx, name, arg.span), - _ => lower_expr(ctx, arg), +/// Extracts the method target from inline `ReflectionClass::getMethod()` calls. +fn reflection_method_class_get_method_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; }; - coerce_descriptor_invoker_mixed_value(ctx, value, arg.span) + if php_symbol_key(method) != "getmethod" { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = reflection_class_member_name_arg(args)?; + let method = resolve_known_class_method_name(ctx, &class_name, &method)?; + Some((class_name, method)) } -/// Boxes a descriptor-invoker argument value into the Mixed slot shape. -fn coerce_descriptor_invoker_mixed_value( - ctx: &mut LoweringContext<'_, '_>, - value: LoweredValue, - span: Span, -) -> LoweredValue { - if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { - return value; +/// Returns the literal name argument passed to a ReflectionClass member lookup. +fn reflection_class_member_name_arg(args: &[Expr]) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } - ctx.emit_value( - Op::MixedBox, - vec![value.value], - None, - PhpType::Mixed, - Op::MixedBox.default_effects(), - Some(span), + let (name, _) = reflection_class_static_property_regular_args(&args, "name", None)?; + reflection_class_static_property_name_arg(name.as_ref()?) +} + +/// Returns normalized constructor args for `ReflectionFunction($function)`. +fn reflection_function_constructor_regular_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [function_arg] => Some(function_arg.clone()), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionFunction") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), ) + .ok()?; + if plan.has_spread_args() { + return None; + } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() } -/// Returns the result storage type for an indirect callable with no static signature. -fn dynamic_callable_result_type( +/// Returns normalized constructor args for `ReflectionProperty($class, $property)`. +fn reflection_property_constructor_regular_args( ctx: &LoweringContext<'_, '_>, - callable: ValueId, - expr: &Expr, -) -> PhpType { - match ctx.builder.value_php_type(callable).codegen_repr() { - PhpType::Callable | PhpType::Str | PhpType::Array(_) | PhpType::Mixed | PhpType::Union(_) => PhpType::Mixed, - _ => fallback_expr_type(expr), + args: &[Expr], +) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [class_arg, property_arg] => Some((class_arg.clone(), property_arg.clone())), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionProperty") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; } + let class_arg = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let property_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(); + Some((class_arg, property_arg)) } -/// Resolves an assignment-expression callee whose assigned value is a static callable. -fn static_assignment_callable_target( +/// Returns normalized constructor args for `ReflectionMethod($class, $method)`. +fn reflection_method_constructor_regular_args( ctx: &LoweringContext<'_, '_>, - callee: &Expr, -) -> Option { - let ExprKind::Assignment { target, value, .. } = &callee.kind else { + args: &[Expr], +) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if args.len() == 1 { + return reflection_method_constructor_single_target(ctx, &args[0]); + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [class_arg, method_arg] => Some((class_arg.clone(), method_arg.clone())), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionMethod") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + let class_arg = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let method_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(); + Some((class_arg, method_arg)) +} + +/// Splits deprecated `ReflectionMethod("Class::method")` constructor syntax. +fn reflection_method_constructor_single_target( + ctx: &LoweringContext<'_, '_>, + arg: &Expr, +) -> Option<(Expr, Expr)> { + let arg = match &arg.kind { + ExprKind::NamedArg { name, value } if name == "class_name" => value.as_ref(), + ExprKind::NamedArg { name, value } if name == "objectOrMethod" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, + }; + let ExprKind::StringLiteral(target) = &arg.kind else { return None; }; - if !matches!(target.kind, ExprKind::Variable(_)) { + let (raw_class_name, raw_method_name) = target.rsplit_once("::")?; + if raw_class_name.is_empty() || raw_method_name.is_empty() { return None; } - static_callable_binding_for_expr(ctx, value).and_then(direct_static_callable_binding) + let class_name = resolve_known_class_name(ctx, raw_class_name)?; + let method_name = resolve_known_class_method_name(ctx, &class_name, raw_method_name)?; + Some(( + Expr::new(ExprKind::StringLiteral(class_name), arg.span), + Expr::new(ExprKind::StringLiteral(method_name), arg.span), + )) } -/// Lowers direct invocation of a literal first-class callable target. -fn lower_first_class_callable_expr_call( +/// Lowers `ReflectionClass::getStaticProperties()` to a live static-property map. +fn lower_reflection_class_get_static_properties( ctx: &mut LoweringContext<'_, '_>, - callee: &Expr, + class_name: &str, args: &[Expr], expr: &Expr, ) -> Option { - match &callee.kind { - ExprKind::FirstClassCallable(CallableTarget::Function(name)) => { - Some(lower_function_call(ctx, name, args, expr)) - } - ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { - Some(lower_static_method_call(ctx, receiver, method, args, expr)) - } - ExprKind::FirstClassCallable(target @ CallableTarget::Method { .. }) => { - let signature = static_callable_binding_for_expr(ctx, callee) - .and_then(|target| signature_for_static_callable_binding(ctx, target)); - let callable = lower_first_class_callable(ctx, target, callee); - let result_type = signature - .as_ref() - .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); - let arg_container = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; - Some(emit_callable_descriptor_invoke( - ctx, - callable, - arg_container, - result_type, - expr.span, - )) - } - _ => None, + if !args.is_empty() { + return None; } -} - -/// Lowers fixed-class object construction. -fn lower_new_object(ctx: &mut LoweringContext<'_, '_>, class_name: &Name, args: &[Expr], expr: &Expr) -> LoweredValue { - let sig = constructor_signature(ctx, class_name).cloned(); - let operands = lower_args_with_signature(ctx, sig.as_ref(), args); - let php_type = PhpType::Object(class_name.as_str().to_string()); - let data = ctx.intern_class_name(class_name.as_str()); - ctx.emit_value( - Op::ObjectNew, - operands, - Some(Immediate::Data(data)), - php_type, - Op::ObjectNew.default_effects(), + let properties = reflection_class_static_property_map_entries(ctx, class_name)?; + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(properties.len() as u32)), + hash_ty, + Op::HashNew.default_effects(), Some(expr.span), - ) + ); + for (property, declaring_class, property_ty) in properties { + let key_expr = Expr::new(ExprKind::StringLiteral(property.clone()), expr.span); + let key = lower_string_literal(ctx, &property, &key_expr); + let value = lower_reflection_static_property_get_by_class_name( + ctx, + &declaring_class, + &property, + property_ty, + expr, + ); + let value = box_value_as_mixed(ctx, value, expr.span); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(expr.span), + ); + } + Some(hash) } -/// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. -fn lower_new_dynamic( +/// Lowers `ReflectionClass::getStaticPropertyValue()` to a live static-property read. +fn lower_reflection_class_get_static_property_value( ctx: &mut LoweringContext<'_, '_>, - name_expr: &Expr, + class_name: &str, args: &[Expr], expr: &Expr, -) -> LoweredValue { - let mut operands = vec![lower_expr(ctx, name_expr).value]; - operands.extend(lower_args(ctx, args)); - ctx.emit_value( - Op::DynamicObjectNewMixed, - operands, - None, - PhpType::Mixed, - Op::DynamicObjectNewMixed.default_effects(), - Some(expr.span), - ) +) -> Option { + let (property, default) = reflection_class_get_static_property_value_args(args)?; + if let Some((declaring_class, property_ty)) = + reflection_class_static_property_target(ctx, class_name, &property) + { + if default.is_none() { + return Some(lower_reflection_static_property_get_by_class_name( + ctx, + &declaring_class, + &property, + property_ty, + expr, + )); + } + return None; + } + Some(match default { + Some(default) => lower_expr(ctx, &default), + None => lower_reflection_class_missing_static_property(ctx, class_name, &property, expr), + }) } -/// Lowers dynamic object construction. -fn lower_new_dynamic_object( +/// Lowers `ReflectionClass::setStaticPropertyValue()` to a live static-property write. +fn lower_reflection_class_set_static_property_value( ctx: &mut LoweringContext<'_, '_>, - class_name: &Expr, - fallback_class: &Name, - required_parent: &Name, + class_name: &str, args: &[Expr], expr: &Expr, -) -> LoweredValue { - let mut operands = vec![lower_expr(ctx, class_name).value]; - operands.extend(lower_args(ctx, args)); - let name = format!("{}|{}", fallback_class.as_str(), required_parent.as_str()); - let data = ctx.intern_class_name(&name); - ctx.emit_value( - Op::DynamicObjectNew, - operands, - Some(Immediate::Data(data)), - PhpType::Object(fallback_class.as_str().to_string()), - Op::DynamicObjectNew.default_effects(), - Some(expr.span), - ) -} - -/// Returns constructor signature metadata when available for a fixed class. -fn constructor_signature<'a>( - ctx: &'a LoweringContext<'_, '_>, - class_name: &Name, -) -> Option<&'a FunctionSig> { - let key = php_symbol_key("__construct"); - ctx.classes - .get(class_name.as_str().trim_start_matches('\\')) - .and_then(|class_info| class_info.methods.get(&key)) +) -> Option { + let (property, value) = reflection_class_set_static_property_value_args(args)?; + let (declaring_class, _) = reflection_class_static_property_target(ctx, class_name, &property)?; + let value = lower_expr(ctx, &value); + store_reflection_static_property_by_class_name( + ctx, + &declaring_class, + &property, + value.value, + expr.span, + ); + Some(lower_null(ctx, expr)) } -/// Lowers an object property read. -fn lower_property_get( +/// Lowers a missing static-property lookup to PHP's catchable ReflectionException. +fn lower_reflection_class_missing_static_property( ctx: &mut LoweringContext<'_, '_>, - object: &Expr, + class_name: &str, property: &str, - op: Op, expr: &Expr, ) -> LoweredValue { - let object = lower_expr(ctx, object); - lower_property_get_from_value(ctx, object, property, op, expr) -} - -/// Lowers `$target = &$obj->prop`: binds the local `$target` to the reference cell -/// stored in the object's reference-property slot, so reads/writes of either side go -/// through the same cell (write-through). The property was promoted to a reference -/// property by the checker, so its slot holds a live cell pointer. -pub(crate) fn lower_ref_assign_property( - ctx: &mut LoweringContext<'_, '_>, - target: &str, - source: &Expr, - span: Span, -) { - let ExprKind::PropertyAccess { object, property } = &source.kind else { - return; - }; - let object = lower_expr(ctx, object); - let value_type = property_get_result_type(ctx, object.value, property, Op::PropGet, source); - let data = ctx.intern_string(property); - let cell_ptr = ctx.emit_value( - Op::LoadPropRefCell, - vec![object.value], - Some(Immediate::Data(data)), - value_type.clone(), - Op::LoadPropRefCell.default_effects(), - Some(span), + let message = format!( + "Property {}::${} does not exist", + class_name.trim_start_matches('\\'), + property ); - ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); + let exception = Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionException"), + args: vec![Expr::new(ExprKind::StringLiteral(message), expr.span)], + }, + expr.span, + ); + let placeholder = lower_null(ctx, expr); + let exception = lower_expr(ctx, &exception); + ctx.builder.terminate(Terminator::Throw { + value: exception.value, + }); + placeholder } -/// Lowers `$target = &call()`: binds `$target` to the reference cell returned by a -/// by-reference-returning callee. The call yields the cell pointer; the target shares it -/// non-owning (the owner is the object property the callee returned a reference to). -pub(crate) fn lower_ref_assign_call( - ctx: &mut LoweringContext<'_, '_>, - target: &str, - source: &Expr, - span: Span, -) { - let cell_ptr = lower_expr(ctx, source); - let value_type = ctx.builder.value_php_type(cell_ptr.value); - ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); +/// Returns synthetic array entries for current static-property values on a reflected class. +fn reflection_class_static_property_map_entries( + ctx: &LoweringContext<'_, '_>, + class_name: &str, +) -> Option> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + Some( + class_info + .static_properties + .iter() + .map(|(property, property_ty)| { + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let property_ty = normalize_value_php_type(property_ty.codegen_repr()); + (property.clone(), declaring_class, property_ty) + }) + .collect(), + ) } -/// Lowers `$target =& $arr[idx]`: promotes the indexed-array element's inline storage to a -/// reference cell and binds `$target` to it non-owning. The returned cell pointer addresses -/// the element within the array payload, so writes through `$target` propagate to `$arr[idx]` -/// and vice versa. The array must remain live while the alias is in use (the local does not -/// own the storage). Operands: the lowered array value and the lowered index value. -pub(crate) fn lower_ref_assign_array_elem( +/// Boxes a concrete PHP value into the runtime `Mixed` cell representation. +fn box_value_as_mixed( ctx: &mut LoweringContext<'_, '_>, - target: &str, - source: &Expr, + value: LoweredValue, span: Span, -) { - let ExprKind::ArrayAccess { array, index } = &source.kind else { - return; - }; - let array_value = lower_expr(ctx, array); - let mut index_value = lower_expr(ctx, index); - index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); - // Use the array's declared element type (the inline storage shape), not the - // null-capable `TaggedScalar` result type that `array_access_result_type` widens - // Int elements to. The ref-cell aliases the raw element slot, so loads and stores - // through the alias must match the element's storage width, not the read result. - let value_type = match ctx.builder.value_php_type(array_value.value).codegen_repr() { - PhpType::Array(elem_ty) => normalize_value_php_type(*elem_ty), - _ => array_access_result_type(ctx, array_value.value, Op::ArrayGet, source), - }; - let cell_ptr = ctx.emit_value( - Op::LoadArrayElemRefCell, - vec![array_value.value, index_value.value], - None, - value_type.clone(), - Op::LoadArrayElemRefCell.default_effects(), - Some(span), - ); - ctx.bind_local_ref_cell_ptr(target, cell_ptr, value_type, Some(span)); -} - -/// Lowers a named property read once the receiver is already evaluated. -fn lower_property_get_from_value( - ctx: &mut LoweringContext<'_, '_>, - object: LoweredValue, - property: &str, - op: Op, - expr: &Expr, ) -> LoweredValue { - if op == Op::NullsafePropGet && value_is_definitely_null(ctx, object.value) { - return lower_boxed_null(ctx, expr); - } - // Route a read of a get-hooked property to its synthetic accessor, except inside that property's - // own accessor, where `$this->prop` must read the raw backing slot to avoid infinite recursion. - // A nullsafe read (`$obj?->prop`) routes to a nullsafe call so the null short-circuit is kept. - if matches!(op, Op::PropGet | Op::NullsafePropGet) - && class_declares_hook_accessor(ctx, object.value, &property_hook_get_method(property)) - && !ctx.in_own_property_accessor(property) - { - let accessor = property_hook_get_method(property); - let call_op = if op == Op::NullsafePropGet { - Op::NullsafeMethodCall - } else { - Op::MethodCall - }; - return lower_method_call_with_receiver(ctx, object, &accessor, &[], call_op, expr); + if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { + return value; } - let data = ctx.intern_string(property); - let result_type = property_get_result_type(ctx, object.value, property, op, expr); ctx.emit_value( - op, - vec![object.value], - Some(Immediate::Data(data)), - result_type, - op.default_effects(), - Some(expr.span), + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), ) } -/// Returns true when value metadata proves the runtime value is PHP null. -fn value_is_definitely_null(ctx: &LoweringContext<'_, '_>, value: crate::ir::ValueId) -> bool { - matches!(ctx.builder.value_php_type(value), PhpType::Void | PhpType::Never) +/// Returns the literal property name and optional explicit default argument for a get call. +fn reflection_class_get_static_property_value_args( + args: &[Expr], +) -> Option<(String, Option)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (name, default) = + reflection_class_static_property_regular_args(&args, "name", Some("default"))?; + let property = reflection_class_static_property_name_arg(name.as_ref()?)?; + Some((property, default)) } -/// Returns true when value metadata permits PHP null at runtime. -fn value_is_nullable(ctx: &LoweringContext<'_, '_>, value: crate::ir::ValueId) -> bool { - match ctx.builder.value_php_type(value) { - PhpType::Void | PhpType::Never => true, - PhpType::Union(members) => members.iter().any(|member| matches!(member, PhpType::Void)), - _ => false, +/// Returns the literal property name and value expression for a set call. +fn reflection_class_set_static_property_value_args(args: &[Expr]) -> Option<(String, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } + let (name, value) = + reflection_class_static_property_regular_args(&args, "name", Some("value"))?; + let property = reflection_class_static_property_name_arg(name.as_ref()?)?; + let value = value?; + Some((property, value)) } -/// Returns precise PHP metadata for a named property read when class metadata is available. -fn property_get_result_type( - ctx: &LoweringContext<'_, '_>, - object: crate::ir::ValueId, - property: &str, - op: Op, - expr: &Expr, -) -> PhpType { - if op == Op::NullsafePropGet { - return PhpType::Mixed; - } - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, nullable)) = singular_object_class(&object_ty) else { - if matches!(object_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return PhpType::Mixed; - } - if let PhpType::Packed(class_name) = object_ty.codegen_repr() { - let normalized = class_name.trim_start_matches('\\'); - let Some(class_info) = ctx.packed_classes.get(normalized) else { - return fallback_expr_type(expr); - }; - let Some(field) = class_info.fields.iter().find(|field| field.name == property) else { - return fallback_expr_type(expr); - }; - return normalize_value_php_type(field.php_type.codegen_repr()); - } - return fallback_expr_type(expr); - }; - let normalized = class_name.trim_start_matches('\\'); - if is_builtin_stdclass_name(normalized) { - return if nullable { - nullable_result_type(PhpType::Mixed) - } else { - PhpType::Mixed - }; - } - let Some(class_info) = ctx.classes.get(normalized) else { - return fallback_expr_type(expr); - }; - if let Some(property_ty) = runtime_property_type_override(ctx, normalized, property) { - let property_ty = normalize_value_php_type(property_ty); - return if nullable { - nullable_result_type(property_ty) - } else { - property_ty +/// Normalizes supported static-property method arguments into parameter order. +fn reflection_class_static_property_regular_args( + args: &[Expr], + first_name: &str, + second_name: Option<&str>, +) -> Option<(Option, Option)> { + if !crate::types::call_args::has_named_args(args) { + return match args { + [first] => Some((Some(first.clone()), None)), + [first, second] => Some((Some(first.clone()), Some(second.clone()))), + _ => None, }; } - let Some((_, property_ty)) = class_info.properties.iter().find(|(name, _)| name == property) else { - if let Some(magic_ty) = magic_get_result_type(ctx, normalized) { - return if nullable { - nullable_result_type(magic_ty) - } else { - magic_ty - }; - } - if class_info.allow_dynamic_properties { - return if nullable { - nullable_result_type(PhpType::Mixed) - } else { - PhpType::Mixed - }; - } - return fallback_expr_type(expr); - }; - let property_ty = normalize_value_php_type(property_ty.clone()); - if nullable { - nullable_result_type(property_ty) - } else { - property_ty - } -} - -/// Returns the normalized return type for a class `__get` magic property hook. -fn magic_get_result_type(ctx: &LoweringContext<'_, '_>, class_name: &str) -> Option { - class_method_signature(ctx, class_name, &php_symbol_key("__get")) - .map(|signature| normalize_value_php_type(signature.return_type.clone())) -} -/// Adds nullability to a result type without nesting existing union metadata. -fn nullable_result_type(php_type: PhpType) -> PhpType { - match php_type { - PhpType::Union(mut members) => { - if !members.iter().any(|member| matches!(member, PhpType::Void)) { - members.push(PhpType::Void); + let mut first = None; + let mut second = None; + for arg in args { + match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == first_name => { + first = Some((**value).clone()); } - PhpType::Union(members) + ExprKind::NamedArg { name, value } + if second_name.is_some_and(|expected| php_symbol_key(name) == expected) => + { + second = Some((**value).clone()); + } + _ => return None, } - other => PhpType::Union(vec![other, PhpType::Void]), } + Some((first, second)) } -/// Returns true when the runtime class of `object` declares the synthetic property-hook accessor -/// `accessor_method` (`__propget_

` / `__propset_

`). Drives the decision to route a property -/// read/write to a hook; inherited (flattened) methods count, so subclasses inherit hooks. -fn class_declares_hook_accessor( - ctx: &LoweringContext<'_, '_>, - object: crate::ir::ValueId, - accessor_method: &str, -) -> bool { - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, _nullable)) = singular_object_class(&object_ty) else { - return false; - }; - let key = php_symbol_key(accessor_method); - ctx.classes - .get(class_name) - .is_some_and(|info| info.methods.contains_key(&key)) -} - -/// Returns the class name and nullability if `php_type` is a single object type (optionally -/// nullable). Heterogeneous unions and non-object types return `None`. -fn singular_object_class(php_type: &PhpType) -> Option<(&str, bool)> { - match php_type { - PhpType::Object(name) => Some((name.as_str(), false)), - PhpType::Union(members) => { - let mut found = None; - let mut nullable = false; - for member in members { - match member { - PhpType::Void => nullable = true, - PhpType::Object(name) => { - if found.is_some_and(|existing| existing != name.as_str()) { - return None; - } - found = Some(name.as_str()); - } - _ => return None, - } - } - found.map(|class_name| (class_name, nullable)) - } +/// Extracts a literal property name from a ReflectionClass static-property call argument. +fn reflection_class_static_property_name_arg(arg: &Expr) -> Option { + match &arg.kind { + ExprKind::StringLiteral(name) => Some(name.clone()), _ => None, } } -/// Returns precise runtime storage types for inherited SPL callback-filter internals. -fn runtime_property_type_override( +/// Returns the declaring class and retained PHP type for one reflected static property. +fn reflection_class_static_property_target( ctx: &LoweringContext<'_, '_>, class_name: &str, property: &str, -) -> Option { - if !class_extends_class(ctx, class_name, "CallbackFilterIterator") { - return None; - } - match property { - "callback" => Some(PhpType::Callable), - "callbackEnv" => Some(PhpType::Pointer(None)), - _ => None, - } +) -> Option<(String, PhpType)> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let property_ty = class_info + .static_properties + .iter() + .find(|(name, _)| name == property) + .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr()))?; + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + Some((declaring_class, property_ty)) } -/// Returns true when a class is or extends the target class. -fn class_extends_class( - ctx: &LoweringContext<'_, '_>, +/// Emits a visibility-bypassing reflection static-property read. +fn lower_reflection_static_property_get_by_class_name( + ctx: &mut LoweringContext<'_, '_>, class_name: &str, - target_class: &str, -) -> bool { - let target_key = php_symbol_key(target_class); - let mut current = Some(class_name.trim_start_matches('\\').to_string()); - while let Some(name) = current { - if php_symbol_key(&name) == target_key { - return true; - } - current = ctx - .classes - .get(name.as_str()) - .and_then(|class_info| class_info.parent.clone()); - } - false + property: &str, + result_type: PhpType, + expr: &Expr, +) -> LoweredValue { + lower_static_property_get_by_class_name_with_op( + ctx, + class_name, + property, + result_type, + expr, + Op::LoadReflectionStaticProperty, + ) } -/// Lowers a dynamic property read. -fn lower_dynamic_property_get(ctx: &mut LoweringContext<'_, '_>, object: &Expr, property: &Expr, expr: &Expr) -> LoweredValue { - let object = lower_expr(ctx, object); - lower_dynamic_property_get_from_value(ctx, object, property, expr) +/// Emits a visibility-bypassing static-property initialization probe. +fn lower_reflection_static_property_initialized_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + expr: &Expr, +) -> LoweredValue { + lower_static_property_get_by_class_name_with_op( + ctx, + class_name, + property, + PhpType::Bool, + expr, + Op::ReflectionStaticPropertyInitialized, + ) } -/// Lowers a dynamic property read once the receiver is already evaluated. -fn lower_dynamic_property_get_from_value( +/// Emits a static-property read using the requested static-property opcode. +fn lower_static_property_get_by_class_name_with_op( ctx: &mut LoweringContext<'_, '_>, - object: LoweredValue, - property: &Expr, + class_name: &str, + property: &str, + result_type: PhpType, expr: &Expr, + op: Op, ) -> LoweredValue { - let result_type = dynamic_property_get_result_type(ctx, object.value, property, expr); - let property = lower_expr(ctx, property); + let data = ctx.intern_string(&format!("{}::{}", class_name, property)); ctx.emit_value( - Op::DynamicPropGet, - vec![object.value, property.value], - None, + op, + Vec::new(), + Some(Immediate::Data(data)), result_type, - Op::DynamicPropGet.default_effects(), + op.default_effects(), Some(expr.span), ) } -/// Returns precise metadata for dynamic property reads when class slots are statically known. -fn dynamic_property_get_result_type( +/// Emits a visibility-bypassing reflection static-property write. +fn store_reflection_static_property_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + value: ValueId, + span: Span, +) { + store_static_property_by_class_name_with_op( + ctx, + class_name, + property, + value, + span, + Op::StoreReflectionStaticProperty, + ); +} + +/// Emits a static-property write using the requested static-property opcode. +fn store_static_property_by_class_name_with_op( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + value: ValueId, + span: Span, + op: Op, +) { + let data = ctx.intern_string(&format!("{}::{}", class_name, property)); + ctx.emit_void( + op, + vec![value], + Some(Immediate::Data(data)), + op.default_effects(), + Some(span), + ); +} + +/// Returns the source arguments that can be forwarded to `new $class(...)`. +fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { + if has_static_call_spread_args(args) { + return expand_static_call_spread_args(args); + } + args.to_vec() +} + +/// Returns constructor arguments carried by a static `newInstanceArgs()` array argument. +fn reflection_class_new_instance_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let args = reflection_class_new_instance_args(args); + match args.as_slice() { + [] => Some(Vec::new()), + [arg] => reflection_class_new_instance_args_value(ctx, arg), + _ => None, + } +} + +/// Extracts the actual array value passed to the `newInstanceArgs()` `$args` parameter. +fn reflection_class_new_instance_args_value( ctx: &LoweringContext<'_, '_>, - object: crate::ir::ValueId, - property: &Expr, - expr: &Expr, -) -> PhpType { - if let ExprKind::StringLiteral(name) = &property.kind { - return property_get_result_type(ctx, object, name, Op::DynamicPropGet, expr); - } - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, nullable)) = singular_object_class(&object_ty) else { - return fallback_expr_type(expr); + arg: &Expr, +) -> Option> { + let array_expr = match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "args" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, }; - let normalized = class_name.trim_start_matches('\\'); - if is_builtin_stdclass_name(normalized) { - return if nullable { - nullable_result_type(PhpType::Mixed) - } else { - PhpType::Mixed - }; + if let ExprKind::Variable(name) = &array_expr.kind { + return ctx.reflection_arg_array_local(name); } - let Some(class_info) = ctx.classes.get(normalized) else { - return fallback_expr_type(expr); + reflection_class_new_instance_args_value_without_locals(array_expr) +} + +/// Extracts an inline static array value passed to a reflection argument-array API. +fn reflection_class_new_instance_args_value_without_locals(arg: &Expr) -> Option> { + let array_expr = match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "args" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, }; - let members = class_info - .properties - .iter() - .map(|(_, property_ty)| { - let property_ty = normalize_value_php_type(property_ty.clone()); - if nullable { - nullable_result_type(property_ty) - } else { - property_ty - } - }) - .collect::>(); - normalize_union_members(members).unwrap_or_else(|| fallback_expr_type(expr)) + match &array_expr.kind { + ExprKind::ArrayLiteral(items) => Some(items.clone()), + ExprKind::ArrayLiteralAssoc(entries) => reflection_class_new_instance_assoc_args(entries), + _ => None, + } } -/// Returns true when the normalized class name refers to PHP's builtin stdClass. -fn is_builtin_stdclass_name(class_name: &str) -> bool { - crate::types::checker::builtin_stdclass::is_stdclass(class_name) +/// Converts a static associative argument array into positional and named call arguments. +fn reflection_class_new_instance_assoc_args(entries: &[(Expr, Expr)]) -> Option> { + entries + .iter() + .map(|(key, value)| reflection_class_new_instance_assoc_arg(key, value)) + .collect() } -/// Flattens and deduplicates union candidates, with `Mixed` absorbing all members. -fn normalize_union_members(members: Vec) -> Option { - let mut deduped = Vec::new(); - for member in members { - match member { - PhpType::Union(inner) => { - for inner_member in inner { - if inner_member == PhpType::Mixed { - return Some(PhpType::Mixed); - } - if !deduped.iter().any(|existing| existing == &inner_member) { - deduped.push(inner_member); - } - } - } - PhpType::Mixed => return Some(PhpType::Mixed), - other => { - if !deduped.iter().any(|existing| existing == &other) { - deduped.push(other); - } - } +/// Converts one `newInstanceArgs()` associative-array element into a constructor argument. +fn reflection_class_new_instance_assoc_arg(key: &Expr, value: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) | ExprKind::FloatLiteral(_) => { + Some(value.clone()) } - } - match deduped.len() { - 0 => None, - 1 => deduped.pop(), - _ => Some(PhpType::Union(deduped)), + ExprKind::StringLiteral(name) if crate::types::is_php_integer_array_key(name) => { + Some(value.clone()) + } + ExprKind::StringLiteral(name) => Some(Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(value.clone()), + }, + value.span, + )), + _ => None, } } -/// Lowers a static property read. -fn lower_static_property_get(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticReceiver, property: &str, expr: &Expr) -> LoweredValue { - let name = format!("{}::{}", receiver_name(receiver), property); - let data = ctx.intern_string(&name); - let result_type = static_property_result_type(ctx, receiver, property, expr); - ctx.emit_value( - Op::LoadStaticProperty, - Vec::new(), - Some(Immediate::Data(data)), - result_type, - Op::LoadStaticProperty.default_effects(), - Some(expr.span), - ) +/// Returns the reflected constructor signature when the ReflectionClass receiver +/// is an inline `new ReflectionClass(Known::class)` expression. +fn reflection_class_new_instance_constructor_signature<'a>( + ctx: &'a LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + forwarded_args: &[Expr], +) -> Option<&'a FunctionSig> { + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; + if forwarded_args.is_empty() && constructor_signature_for_class_name(ctx, &class_name).is_none() + { + return None; + } + constructor_signature_for_class_name(ctx, &class_name) } -/// Returns precise PHP metadata for a static property read when class metadata is available. -fn static_property_result_type( +/// Resolves the target class from an inline `ReflectionClass` construction when +/// its constructor argument is a literal class string or `ClassName::class`. +fn reflection_class_new_instance_reflected_class( ctx: &LoweringContext<'_, '_>, - receiver: &StaticReceiver, - property: &str, - expr: &Expr, -) -> PhpType { - let Some(class_name) = static_receiver_class_name(ctx, receiver) else { - return fallback_expr_type(expr); - }; - let Some(class_info) = ctx.classes.get(class_name.as_str()) else { - return fallback_expr_type(expr); - }; - let Some((_, property_ty)) = class_info - .static_properties - .iter() - .find(|(name, _)| name == property) - else { - return fallback_expr_type(expr); + object_expr: &Expr, +) -> Option { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; }; - normalize_value_php_type(property_ty.codegen_repr()) + match php_symbol_key(class_name.as_str().trim_start_matches('\\')).as_str() { + "reflectionclass" => reflection_class_reflected_class_from_args(ctx, args), + "reflectionobject" => reflection_object_reflected_class_from_args(ctx, args), + _ => None, + } } -/// Lowers an object method call. -fn lower_method_call( - ctx: &mut LoweringContext<'_, '_>, - object: &Expr, - method: &str, +/// Resolves the target class from a static `ReflectionClass(...)` argument list. +fn reflection_class_reflected_class_from_args( + ctx: &LoweringContext<'_, '_>, args: &[Expr], - op: Op, - expr: &Expr, -) -> LoweredValue { - // A statically-decided private/protected method access from an inaccessible - // scope raises a catchable `Error` in PHP rather than a compile-time error, - // but the receiver expression must still be evaluated first. - let throw_access_message = if op == Op::MethodCall { - ctx.throw_access_sites.get(&expr.span).and_then(|info| { - if let ThrowAccessKind::PrivateMethod { - visibility, - class_name, - method: m, - } = &info.kind - { - Some(format!( - "Call to {} method {}::{}() from global scope", - visibility, class_name, m - )) - } else { - None - } - }) - } else { - None +) -> Option { + let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; + let raw_class_name = match &reflected_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, }; - let object_expr = object; - let object = lower_expr(ctx, object_expr); - if let Some(message) = throw_access_message { - release_owning_receiver_temporary(ctx, object, expr.span); - return crate::ir_lower::stmt::lower_throw_access_error_expr(ctx, &message, expr.span); + resolve_known_class_name(ctx, &raw_class_name) +} + +/// Resolves the target class from a static `ReflectionObject(...)` argument list. +fn reflection_object_reflected_class_from_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let object_arg = reflection_object_constructor_object_arg(ctx, args)?; + isset_object_expr_class(ctx, &object_arg).map(|(class_name, _)| class_name) +} + +/// Resolves a reflected class from an inline constructor or tracked local receiver. +fn reflection_class_reflected_class( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + reflection_class_new_instance_reflected_class(ctx, object_expr).or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_class_local(name) + }) +} + +/// Returns the `ReflectionClass::__construct()` class-name argument after static +/// spread and named-argument normalization. +fn reflection_class_constructor_class_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } - if op == Op::MethodCall && value_is_definitely_null(ctx, object.value) { - let null_value = lower_null(ctx, expr); - terminate_method_call_on_null(ctx, method); - return null_value; + if !crate::types::call_args::has_named_args(&args) { + return args.first().cloned(); } - if op == Op::MethodCall && value_is_nullable(ctx, object.value) { - return lower_nullable_regular_method_call(ctx, object, method, args, expr); + let sig = ctx + .classes + .get("ReflectionClass") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; } - if matches!( - ctx.builder.value_php_type(object.value).codegen_repr(), - PhpType::Callable - ) { - if let Some(result) = lower_closure_bind_method(ctx, &object, method, args, expr) { - return result; - } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() +} + +/// Returns the `ReflectionObject::__construct()` object argument after normalization. +fn reflection_object_constructor_object_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } - let magic_args; - let (dispatch_method, args) = if let Some(args) = - magic_call_dispatch_args(ctx, object.value, method, args, object_expr.span) - { - magic_args = args; - ("__call", magic_args.as_slice()) - } else { - (method, args) - }; - let result_type = method_call_result_type(ctx, object.value, dispatch_method, op, expr); - let mut operands = vec![object.value]; - let sig = method_call_argument_signature(ctx, object_expr, object.value, dispatch_method); - let arg_values = lower_args_with_signature(ctx, sig.as_ref(), args); - operands.extend(arg_values.iter().copied()); - let data = ctx.intern_string(dispatch_method); - let call = ctx.emit_value( - op, - operands, - Some(Immediate::Data(data)), - result_type, - op.default_effects(), - Some(expr.span), + if !crate::types::call_args::has_named_args(&args) { + return args.first().cloned(); + } + let sig = ctx + .classes + .get("ReflectionObject") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() +} + +/// Resolves a PHP class name case-insensitively against known class metadata. +fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> Option { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.classes + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) + .cloned() +} + +/// Resolves a PHP function name case-insensitively against known user functions. +fn resolve_known_function_name( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> Option { + let key = php_symbol_key(function_name.trim_start_matches('\\')); + ctx.functions + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) + .cloned() +} + +/// Resolves a PHP method name case-insensitively against known class metadata. +fn resolve_known_class_method_name( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let key = php_symbol_key(method); + class_info + .methods + .keys() + .chain(class_info.static_methods.keys()) + .find(|candidate| php_symbol_key(candidate) == key) + .cloned() +} + +/// Returns constructor signature metadata for a known class name. +fn constructor_signature_for_class_name<'a>( + ctx: &'a LoweringContext<'_, '_>, + class_name: &str, +) -> Option<&'a FunctionSig> { + let key = php_symbol_key("__construct"); + ctx.classes + .get(class_name.trim_start_matches('\\')) + .and_then(|class_info| class_info.methods.get(&key)) +} + +/// Emits a runtime fatal for ReflectionClass newInstance argument forms not yet lowered. +fn lower_reflection_class_new_instance_unsupported( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstance() argument forwarding\n", ); - release_owned_call_arg_temporaries(ctx, &arg_values, Some(call.value), expr.span); - release_owning_receiver_temporary(ctx, object, expr.span); - call + ctx.builder.terminate(Terminator::Fatal { message }); + result } -/// Lowers the `Closure` rebinding methods on a closure (`Callable`) receiver: -/// `$closure->bindTo($newThis [, $scope])` and `$closure->call($newThis, ...$args)`. -/// Returns `None` for any other method so normal dispatch (and its diagnostics) -/// still apply. The `$scope` argument is accepted and ignored — visibility is -/// resolved at compile time in elephc's closed-world model. -fn lower_closure_bind_method( +/// Emits a runtime fatal for unsupported `newInstanceArgs()` argument-array forms. +fn lower_reflection_class_new_instance_args_unsupported( ctx: &mut LoweringContext<'_, '_>, - closure: &LoweredValue, - method: &str, - args: &[Expr], expr: &Expr, -) -> Option { - match php_symbol_key(method).as_str() { - "bindto" => { - let new_this = match args.first() { - Some(arg) => lower_expr(ctx, arg), - None => lower_null(ctx, expr), - }; - Some(emit_closure_bind(ctx, closure.value, new_this.value, expr)) - } - "call" => { - // `$closure->call($newThis, ...$args)`: bind `$this` then invoke the - // bound closure with the remaining arguments in one step. - let new_this = match args.first() { - Some(arg) => lower_expr(ctx, arg), - None => lower_null(ctx, expr), - }; - let bound = emit_closure_bind(ctx, closure.value, new_this.value, expr); - let call_args = &args[args.len().min(1)..]; - let arg_container = - lower_untyped_descriptor_invoker_arg_container(ctx, call_args, expr.span)?; - Some(ctx.emit_value( - Op::CallableDescriptorInvoke, - vec![bound.value, arg_container.value], - None, - PhpType::Mixed, - Op::CallableDescriptorInvoke.default_effects(), - Some(expr.span), - )) - } - _ => None, - } +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstanceArgs() argument array\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result } -/// Emits the `closure_bind` runtime call that rebinds a closure's captured -/// `$this`, yielding a new closure (`Callable`) descriptor. -fn emit_closure_bind( +/// Emits a runtime fatal for unsupported `newInstanceWithoutConstructor()` argument forms. +fn lower_reflection_class_new_instance_without_constructor_unsupported( ctx: &mut LoweringContext<'_, '_>, - closure: crate::ir::ValueId, - new_this: crate::ir::ValueId, expr: &Expr, ) -> LoweredValue { - let data = ctx.intern_function_name("closure_bind"); - ctx.emit_value( - Op::BuiltinCall, - vec![closure, new_this], - Some(Immediate::Data(data)), - PhpType::Callable, - Op::BuiltinCall.default_effects(), - Some(expr.span), - ) + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstanceWithoutConstructor() arguments\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result } -/// Builds synthetic `__call` arguments when a class lacks the requested method. -fn magic_call_dispatch_args( +/// Returns true when a method call targets the built-in `ReflectionClass::newInstance()`. +fn is_reflection_class_new_instance_call( ctx: &LoweringContext<'_, '_>, - object: crate::ir::ValueId, + object: ValueId, method: &str, - args: &[Expr], - span: Span, -) -> Option> { - if method_signature(ctx, object, method).is_some() { - return None; +) -> bool { + if php_symbol_key(method) != "newinstance" { + return false; } - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, _)) = singular_object_class(&object_ty) else { - return None; - }; - let normalized = class_name.trim_start_matches('\\'); - class_method_signature(ctx, normalized, &php_symbol_key("__call"))?; - Some(vec![ - Expr::new(ExprKind::StringLiteral(method.to_string()), span), - Expr::new(ExprKind::ArrayLiteral(args.to_vec()), span), - ]) + is_reflection_class_construction_receiver(ctx, object) } -/// Returns the signature to use for method-call argument normalization. -fn method_call_argument_signature( +/// Returns true when a method call targets `ReflectionClass::newInstanceArgs()`. +fn is_reflection_class_new_instance_args_call( ctx: &LoweringContext<'_, '_>, - object_expr: &Expr, - object: crate::ir::ValueId, + object: ValueId, method: &str, -) -> Option { - if method_is_fiber_start(ctx, object, method) { - return crate::ir_lower::fibers::start_sig_for_expr(ctx, object_expr); +) -> bool { + if php_symbol_key(method) != "newinstanceargs" { + return false; } - method_signature(ctx, object, method) + is_reflection_class_construction_receiver(ctx, object) } -/// Returns true when a method call targets PHP's built-in `Fiber::start()`. -fn method_is_fiber_start( +/// Returns true when a method call targets `ReflectionClass::newInstanceWithoutConstructor()`. +fn is_reflection_class_new_instance_without_constructor_call( ctx: &LoweringContext<'_, '_>, - object: crate::ir::ValueId, + object: ValueId, method: &str, ) -> bool { - if php_symbol_key(method) != "start" { + if php_symbol_key(method) != "newinstancewithoutconstructor" { return false; } + is_reflection_class_construction_receiver(ctx, object) +} + +/// Returns true when a receiver can use ReflectionClass construction helper lowering. +fn is_reflection_class_construction_receiver( + ctx: &LoweringContext<'_, '_>, + object: ValueId, +) -> bool { let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, _)) = singular_object_class(&object_ty) else { + let Some((class_name, false)) = singular_object_class(&object_ty) else { return false; }; - php_symbol_key(class_name.trim_start_matches('\\')) == "fiber" -} - -/// Lowers `?Object->method()` calls so null receivers fatal before argument evaluation. -fn lower_nullable_regular_method_call( - ctx: &mut LoweringContext<'_, '_>, - object: LoweredValue, - method: &str, - args: &[Expr], - expr: &Expr, -) -> LoweredValue { - let result_type = method_call_result_type(ctx, object.value, method, Op::MethodCall, expr); - let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let fatal_block = ctx.builder.create_named_block("method.null.fatal", Vec::new()); - let call_block = ctx.builder.create_named_block("method.non_null.call", Vec::new()); - let merge = ctx.builder.create_named_block("method.nullable.merge", Vec::new()); - let is_null = ctx.emit_value( - Op::IsNull, - vec![object.value], - None, - PhpType::Bool, - Op::IsNull.default_effects(), - Some(expr.span), - ); - ctx.builder.terminate(Terminator::CondBr { - cond: is_null.value, - then_target: fatal_block, - then_args: Vec::new(), - else_target: call_block, - else_args: Vec::new(), - }); - - ctx.builder.position_at_end(fatal_block); - terminate_method_call_on_null(ctx, method); - - ctx.builder.position_at_end(call_block); - let call = lower_method_call_with_receiver(ctx, object, method, args, Op::MethodCall, expr); - store_value_into_temp(ctx, &temp_name, result_type.clone(), call, expr.span); - branch_to(ctx, merge); - - ctx.builder.position_at_end(merge); - take_owned_temp(ctx, &temp_name, expr.span) + matches!( + php_symbol_key(class_name.trim_start_matches('\\')).as_str(), + "reflectionclass" | "reflectionobject" + ) } /// Emits the PHP fatal terminator for an ordinary method call on null. @@ -9162,6 +13439,18 @@ fn lower_method_call_with_receiver( op: Op, expr: &Expr, ) -> LoweredValue { + if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { + return lower_reflection_class_new_instance(ctx, None, object, args, expr); + } + if op == Op::MethodCall && is_reflection_class_new_instance_args_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_args(ctx, None, object, args, expr); + } + if op == Op::MethodCall + && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_without_constructor(ctx, object, args, expr); + } let magic_args; let (dispatch_method, args) = if let Some(args) = magic_call_dispatch_args(ctx, object.value, method, args, expr.span) { @@ -9189,6 +13478,39 @@ fn lower_method_call_with_receiver( call } +/// Lowers a nullsafe dynamic instance method call after the receiver was evaluated and guarded. +/// +/// The non-null receiver is stored in a hidden temp so the existing +/// `call_user_func([$obj, $method], ...)` lowering can be reused without +/// evaluating the original receiver expression again. +pub(super) fn lower_dynamic_method_call_with_receiver( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + method: &Expr, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let receiver_type = strip_void_from_union(ctx.builder.value_php_type(object.value)); + let receiver_name = ctx.declare_hidden_temp(receiver_type.clone()); + ctx.store_local(&receiver_name, object, receiver_type, Some(expr.span)); + let receiver = Expr::new(ExprKind::Variable(receiver_name), expr.span); + let callback = Expr::new( + ExprKind::ArrayLiteral(vec![receiver, method.clone()]), + expr.span, + ); + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(callback); + call_args.extend(args.iter().cloned()); + let call = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("call_user_func"), + args: call_args, + }, + expr.span, + ); + lower_expr(ctx, &call) +} + /// Releases normalized call arguments that cannot be returned by this call. fn release_owned_call_arg_temporaries( ctx: &mut LoweringContext<'_, '_>, @@ -9291,6 +13613,9 @@ fn method_signature( return class_method_signature(ctx, normalized, &key).cloned(); } if dynamic_method_receiver_needs_mixed_fallback(&object_ty) { + if ctx.has_eval_barrier() { + return None; + } return common_dynamic_method_signature(ctx, &key); } None @@ -9399,24 +13724,54 @@ fn lower_static_method_call( return emit_closure_bind(ctx, closure.value, new_this.value, expr); } } - let sig = static_method_implementation_signature(ctx, receiver, method) - .or_else(|| lexical_instance_static_call_signature(ctx, receiver, method)) - .cloned(); - // PHP `__callStatic`: an undefined static method forwards to the class's - // `__callStatic($name, $args)` when the class declares one. - if sig.is_none() { - if let Some(class_name) = magic_callstatic_receiver_class(ctx, receiver, method) { - return lower_magic_callstatic(ctx, &class_name, method, args, expr); + + let magic_args; + let (dispatch_method, call_args) = if let Some(args) = + magic_static_call_dispatch_args(ctx, receiver, method, args, expr.span) + { + magic_args = args; + ("__callStatic", magic_args.as_slice()) + } else { + (method, args) + }; + if ctx.has_eval_barrier() + && matches!(receiver, StaticReceiver::Named(_)) + && plain_positional_call_args(args) + { + if let Some(class_name) = static_receiver_class_name(ctx, receiver) { + if !ctx.classes.contains_key(class_name.as_str()) { + let operands = lower_args_with_signature(ctx, None, args); + let name = format!("{}::{}", class_name, dispatch_method); + let data = ctx.intern_string(&name); + return ctx.emit_value( + Op::EvalStaticMethodCall, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalStaticMethodCall.default_effects(), + Some(expr.span), + ); + } } } - let operands = lower_args_with_signature(ctx, sig.as_ref(), args); - let operands = coerce_int_backed_enum_string_argument(ctx, receiver, method, operands, expr); - let name = format!("{}::{}", receiver_name(receiver), method); + let sig = static_method_implementation_signature(ctx, receiver, dispatch_method) + .or_else(|| lexical_instance_static_call_signature(ctx, receiver, dispatch_method)) + .cloned(); + let operands = lower_args_with_signature(ctx, sig.as_ref(), call_args); + let operands = + coerce_int_backed_enum_string_argument(ctx, receiver, dispatch_method, operands, expr); + let name = format!("{}::{}", receiver_name(receiver), dispatch_method); let data = ctx.intern_string(&name); let result_type = sig .as_ref() .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| fallback_expr_type(expr)); + .unwrap_or_else(|| { + if ctx.has_eval_barrier() && matches!(receiver, StaticReceiver::Named(_)) { + PhpType::Mixed + } else { + fallback_expr_type(expr) + } + }); ctx.emit_value( Op::StaticMethodCall, operands, @@ -9491,57 +13846,29 @@ fn coerce_int_backed_enum_string_argument( operands } -/// Resolves the class whose `__callStatic` should handle an otherwise-undefined -/// static call `Receiver::method(...)`. Returns `None` when the receiver already -/// has a real static method of that name or declares no `__callStatic`. -fn magic_callstatic_receiver_class( +/// Builds synthetic `__callStatic` arguments when a class lacks the requested static method. +fn magic_static_call_dispatch_args( ctx: &LoweringContext<'_, '_>, receiver: &StaticReceiver, method: &str, -) -> Option { + args: &[Expr], + span: Span, +) -> Option> { + if static_method_implementation_signature(ctx, receiver, method).is_some() + || lexical_instance_static_call_signature(ctx, receiver, method).is_some() + { + return None; + } let class_name = static_receiver_class_name(ctx, receiver)?; let class_info = ctx.classes.get(class_name.as_str())?; - if class_info.static_methods.contains_key(&php_symbol_key(method)) { + if class_info.methods.contains_key(&php_symbol_key(method)) { return None; } - class_info - .static_methods - .contains_key("__callstatic") - .then_some(class_name) -} - -/// Lowers `Class::method(args)` as a forward to `Class::__callStatic("method", [args])`. -fn lower_magic_callstatic( - ctx: &mut LoweringContext<'_, '_>, - class_name: &str, - method: &str, - args: &[Expr], - expr: &Expr, -) -> LoweredValue { - let sig = ctx - .classes - .get(class_name) - .and_then(|info| info.static_methods.get("__callstatic")) - .cloned(); - let magic_args = vec![ - Expr::new(ExprKind::StringLiteral(method.to_string()), expr.span), - Expr::new(ExprKind::ArrayLiteral(args.to_vec()), expr.span), - ]; - let operands = lower_args_with_signature(ctx, sig.as_ref(), &magic_args); - let name = format!("{}::__callStatic", class_name); - let data = ctx.intern_string(&name); - let result_type = sig - .as_ref() - .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| fallback_expr_type(expr)); - ctx.emit_value( - Op::StaticMethodCall, - operands, - Some(Immediate::Data(data)), - result_type, - Op::StaticMethodCall.default_effects(), - Some(expr.span), - ) + static_method_implementation_signature(ctx, receiver, "__callStatic")?; + Some(vec![ + Expr::new(ExprKind::StringLiteral(method.to_string()), span), + Expr::new(ExprKind::ArrayLiteral(args.to_vec()), span), + ]) } /// Lowers a static-method callable-array call through a descriptor invoker. @@ -9757,7 +14084,7 @@ fn lower_scoped_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticRec Op::ScopedConstantGet, Vec::new(), Some(Immediate::Data(data)), - fallback_expr_type(expr), + PhpType::Mixed, Op::ScopedConstantGet.default_effects(), Some(expr.span), ) @@ -10023,8 +14350,10 @@ fn lower_yield_from_array( let body = ctx.builder.create_named_block("yieldfrom.body", Vec::new()); let exit = ctx.builder.create_named_block("yieldfrom.exit", Vec::new()); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder - .terminate(Terminator::Br { target: header, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target: header, + args: Vec::new(), + }); } ctx.builder.position_at_end(header); @@ -10073,8 +14402,10 @@ fn lower_yield_from_array( Some(span), ); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder - .terminate(Terminator::Br { target: header, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target: header, + args: Vec::new(), + }); } ctx.builder.position_at_end(exit); diff --git a/src/ir_lower/expr/nullsafe_chain.rs b/src/ir_lower/expr/nullsafe_chain.rs index 274c17b677..35f009ff61 100644 --- a/src/ir_lower/expr/nullsafe_chain.rs +++ b/src/ir_lower/expr/nullsafe_chain.rs @@ -68,6 +68,12 @@ enum PostfixSegment<'a> { args: &'a [Expr], nullsafe: bool, }, + DynamicMethod { + expr: &'a Expr, + method: &'a Expr, + args: &'a [Expr], + nullsafe: bool, + }, Array { expr: &'a Expr, index: &'a Expr, @@ -92,6 +98,9 @@ impl PostfixSegment<'_> { } | PostfixSegment::Method { nullsafe: true, .. + } | PostfixSegment::DynamicMethod { + nullsafe: true, + .. } ) } @@ -162,6 +171,19 @@ fn flatten_nullsafe_postfix_chain(expr: &Expr) -> Option> { }); base = object; } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + segments.push(PostfixSegment::DynamicMethod { + expr: base, + method, + args, + nullsafe: true, + }); + base = object; + } ExprKind::ArrayAccess { array, index } => { segments.push(PostfixSegment::Array { expr: base, index }); base = array; @@ -284,6 +306,19 @@ fn lower_nullsafe_postfix_segment( expr, )) } + PostfixSegment::DynamicMethod { + expr, + method, + args, + nullsafe, + } => { + if nullsafe && !guard_nullsafe_chain_receiver(ctx, current, null_block, expr) { + return None; + } + Some(super::lower_dynamic_method_call_with_receiver( + ctx, current, method, args, expr, + )) + } PostfixSegment::Array { expr, index } => { Some(lower_array_access_from_value( ctx, diff --git a/src/ir_lower/fibers.rs b/src/ir_lower/fibers.rs index 4f5f2ce7df..428715e708 100644 --- a/src/ir_lower/fibers.rs +++ b/src/ir_lower/fibers.rs @@ -159,8 +159,15 @@ fn direct_new_fiber_callback_sig(expr: &Expr) -> Option { let callback = args.first()?; match &callback.kind { ExprKind::Closure { - params, variadic, .. - } => Some(callback_sig_from_closure_params(params, variadic.as_deref())), + params, + variadic, + variadic_by_ref, + .. + } => Some(callback_sig_from_closure_params( + params, + variadic.as_deref(), + *variadic_by_ref, + )), _ => None, } } @@ -172,8 +179,15 @@ fn callback_sig_for_expr( ) -> Option { match &callback.kind { ExprKind::Closure { - params, variadic, .. - } => Some(callback_sig_from_closure_params(params, variadic.as_deref())), + params, + variadic, + variadic_by_ref, + .. + } => Some(callback_sig_from_closure_params( + params, + variadic.as_deref(), + *variadic_by_ref, + )), ExprKind::Variable(name) => ctx .callable_param_signature(name) .cloned() @@ -194,6 +208,8 @@ fn callback_sig_for_binding( StaticCallableBinding::ExternFunction(name) => { ctx.extern_functions.get(name.as_str()).map(|sig| FunctionSig { params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, @@ -294,6 +310,7 @@ fn class_method_sig( fn callback_sig_from_closure_params( params: &[(String, Option, Option, bool)], variadic: Option<&str>, + variadic_by_ref: bool, ) -> FunctionSig { let mut sig = FunctionSig { params: params @@ -307,6 +324,11 @@ fn callback_sig_from_closure_params( ) }) .collect(), + param_type_exprs: params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .collect(), + param_attributes: Vec::new(), defaults: params .iter() .map(|(_, _, default, _)| default.clone()) @@ -323,8 +345,9 @@ fn callback_sig_from_closure_params( if !sig.params.iter().any(|(name, _)| name == variadic_name) { sig.params .push((variadic_name.to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); + sig.param_type_exprs.push(None); sig.defaults.push(None); - sig.ref_params.push(false); + sig.ref_params.push(variadic_by_ref); sig.declared_params.push(false); } } @@ -342,6 +365,8 @@ fn start_sig_from_callback_sig(sig: &FunctionSig) -> Option { .iter() .map(|(name, _)| (name.clone(), PhpType::Mixed)) .collect(), + param_type_exprs: sig.param_type_exprs.clone(), + param_attributes: Vec::new(), defaults: sig.defaults.clone(), return_type: PhpType::Mixed, declared_return: false, diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 588df57ead..6f4154dc74 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -18,12 +18,25 @@ use crate::ir_lower::context::{ StaticCallableBinding, }; use crate::ir_lower::effects_lookup; -use crate::parser::ast::{Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr}; +use crate::names::php_symbol_key; +use crate::parser::ast::{ + AttributeGroup, ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr, +}; use crate::span::Span; -use crate::types::{CheckResult, ClassInfo, FunctionSig, PackedClassInfo, PhpType, TypeEnv}; +use crate::types::{ + collect_attribute_args, collect_attribute_names, CheckResult, ClassInfo, FunctionSig, + PackedClassInfo, PhpType, TypeEnv, +}; /// AST parameter tuple shape used by function, method, and closure declarations. -type AstParams = [(String, Option, Option, bool)]; +type AstParams = [( + String, + Option, + Option, + bool, +)]; + +const EVAL_AOT_SCOPE_PARAM: &str = "__eir_eval_scope"; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; @@ -69,6 +82,8 @@ pub(crate) fn lower_main( None, true, all_global_var_names, + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -125,10 +140,7 @@ fn collect_global_var_names_in_body( collect_global_var_names_in_body(body, names); } crate::parser::ast::StmtKind::For { - init, - update, - body, - .. + init, update, body, .. } => { if let Some(init) = init { collect_global_var_names_in_body(std::slice::from_ref(init.as_ref()), names); @@ -176,6 +188,7 @@ pub(crate) fn lower_user_function( name: &str, params: &AstParams, return_type: Option<&TypeExpr>, + attributes: &[AttributeGroup], body: &[Stmt], module: &mut Module, check_result: &CheckResult, @@ -184,18 +197,14 @@ pub(crate) fn lower_user_function( ) { let fallback = signature_from_ast(params, return_type); let signature = check_result.functions.get(name).unwrap_or(&fallback); - let eir_signature = eir_signature_with_php_param_contracts( - name, - signature, - &check_result.callable_param_sigs, - ); + let eir_signature = + eir_signature_with_php_param_contracts(name, signature, &check_result.callable_param_sigs); // A generator's compiled body is a coroutine that returns the value passed // to `return` (Mixed, read back via `Generator::getReturn()`), not the // `Generator` object itself. The public signature stays `Generator` for // callers; only the EIR body return type becomes Mixed so `return $x` // lowers to a plain boxed Mixed return instead of a Generator coercion. - let body_return_type = - generator_body_return_type(body, &eir_signature.return_type); + let body_return_type = generator_body_return_type(body, &eir_signature.return_type); let mut function = Function::new( name.to_string(), return_ir_type(&body_return_type), @@ -205,6 +214,16 @@ pub(crate) fn lower_user_function( function.flags.by_ref_return = signature.by_ref_return; function.source_signature = Some(source_signature(name, &eir_signature)); function.signature = Some(eir_runtime_metadata_signature(&eir_signature)); + function.attribute_names = check_result + .function_attribute_names + .get(name) + .cloned() + .unwrap_or_else(|| collect_attribute_names(attributes)); + function.attribute_args = check_result + .function_attribute_args + .get(name) + .cloned() + .unwrap_or_else(|| collect_attribute_args(attributes)); attach_generator_source_if_needed(&mut function, body, eir_signature.params.len()); let closures = lower_body_into_function( &mut function, @@ -229,6 +248,8 @@ pub(crate) fn lower_user_function( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -248,11 +269,12 @@ pub(crate) fn lower_class_method( fiber_return_sigs: &std::collections::HashMap, ) { let fallback = signature_from_ast(params, return_type); - let signature = check_result - .classes + let signature = module + .class_infos .get(class_name) .and_then(|class| method_signature(class, method_name, is_static)) - .unwrap_or(&fallback); + .cloned() + .unwrap_or(fallback); let name = format!("{}::{}", class_name, method_name); // Generator methods lower their body as a Mixed-returning coroutine; see // `generator_body_return_type`. @@ -268,9 +290,9 @@ pub(crate) fn lower_class_method( by_ref_return: signature.by_ref_return, ..FunctionFlags::default() }; - function.source_signature = Some(source_signature(&name, signature)); - function.signature = Some(eir_runtime_metadata_signature(signature)); - let mut env = env_from_signature(signature); + function.source_signature = Some(source_signature(&name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let mut env = env_from_signature(&signature); let mut body_params = signature.params.clone(); if is_static { let hidden_called_class = (CALLED_CLASS_ID_PARAM.to_string(), PhpType::Int); @@ -295,7 +317,7 @@ pub(crate) fn lower_class_method( env.insert("this".to_string(), this_type.clone()); body_params.insert(0, ("this".to_string(), this_type)); } - function.params.extend(function_params(signature)); + function.params.extend(function_params(&signature)); attach_generator_source_if_needed(&mut function, body, body_params.len()); let closures = lower_body_into_function( &mut function, @@ -320,11 +342,194 @@ pub(crate) fn lower_class_method( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.class_methods.push(function); } +/// Lowers one no-scope literal eval fragment as an internal EIR function. +pub(crate) fn lower_eval_aot_function( + name: &str, + body: &[Stmt], + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let return_type = PhpType::Mixed; + let signature = FunctionSig { + params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: return_type.clone(), + declared_return: false, + by_ref_return: false, + ref_params: Vec::new(), + declared_params: Vec::new(), + variadic: None, + deprecation: None, + }; + let mut function = Function::new( + name.to_string(), + return_ir_type(&return_type), + return_type.clone(), + ); + function.source_signature = Some(source_signature(name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let closures = lower_body_into_function( + &mut function, + &mut module.data, + body, + TypeEnv::new(), + check_result.global_env.clone(), + &check_result.functions, + &check_result.extern_functions, + &check_result.extern_globals, + &check_result.callable_param_sigs, + fiber_return_sigs, + &check_result.classes, + &check_result.enums, + &check_result.interfaces, + &check_result.packed_classes, + &check_result.throw_access_sites, + constants, + None, + return_type, + &[], + None, + false, + collect_global_var_names(body), + module.source_path.clone(), + None, + ); + add_closures(module, closures); + module.add_function(function); +} + +/// Lowers one literal eval fragment as an internal EIR function that reads eval scope. +pub(crate) fn lower_eval_aot_scope_read_function( + name: &str, + body: &[Stmt], + scope_reads: &std::collections::BTreeSet, + scope_direct_writes: &std::collections::BTreeSet, + scope_flush_writes: &std::collections::BTreeSet, + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let return_type = PhpType::Mixed; + let use_read_params = + !scope_reads.is_empty() && scope_direct_writes.is_empty() && scope_flush_writes.is_empty(); + let params = if use_read_params { + scope_reads + .iter() + .map(|name| (name.clone(), PhpType::Mixed)) + .collect::>() + } else { + vec![(EVAL_AOT_SCOPE_PARAM.to_string(), PhpType::Int)] + }; + let signature = FunctionSig { + params, + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: return_type.clone(), + declared_return: false, + by_ref_return: false, + ref_params: vec![ + false; + if use_read_params { + scope_reads.len() + } else { + 1 + } + ], + declared_params: vec![ + false; + if use_read_params { + scope_reads.len() + } else { + 1 + } + ], + variadic: None, + deprecation: None, + }; + let mut function = Function::new( + name.to_string(), + return_ir_type(&return_type), + return_type.clone(), + ); + function.params = function_params(&signature); + function.source_signature = Some(source_signature(name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let mut env = TypeEnv::new(); + for (param_name, param_type) in &signature.params { + env.insert(param_name.clone(), param_type.clone()); + } + let eval_scope_reads = (!use_read_params).then(|| { + ( + EVAL_AOT_SCOPE_PARAM.to_string(), + scope_reads.iter().cloned().collect(), + scope_direct_writes.iter().cloned().collect(), + scope_flush_writes.clone(), + ) + }); + let closures = lower_body_into_function( + &mut function, + &mut module.data, + body, + env, + check_result.global_env.clone(), + &check_result.functions, + &check_result.extern_functions, + &check_result.extern_globals, + &check_result.callable_param_sigs, + fiber_return_sigs, + &check_result.classes, + &check_result.enums, + &check_result.interfaces, + &check_result.packed_classes, + &check_result.throw_access_sites, + constants, + None, + return_type, + &signature.params, + None, + false, + collect_global_var_names(body), + module.source_path.clone(), + eval_scope_reads, + ); + add_closures(module, closures); + module.add_function(function); +} + +/// Builds fallback method signature metadata from parsed class-like method syntax. +pub(crate) fn method_signature_from_ast(method: &ClassMethod) -> FunctionSig { + let mut signature = signature_from_ast_with_variadic( + &method.params, + method.return_type.as_ref(), + method.variadic.as_deref(), + method.variadic_by_ref, + ); + if !method.variadic_by_ref { + if let Some(variadic_type) = &method.variadic_type { + if let Some((_, php_type)) = signature.params.last_mut() { + *php_type = type_expr_to_php_type(variadic_type); + } + if let Some(declared) = signature.declared_params.last_mut() { + *declared = true; + } + } + } + signature +} + /// Lowers a synthetic `_class_propinit_` function for dynamic by-name allocation. pub(crate) fn lower_property_init_thunk( class_name: &str, @@ -351,6 +556,8 @@ pub(crate) fn lower_property_init_thunk( }); let sig = FunctionSig { params: vec![("this".to_string(), this_type.clone())], + param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Void, declared_return: false, @@ -388,6 +595,8 @@ pub(crate) fn lower_property_init_thunk( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -432,13 +641,22 @@ pub(crate) fn lower_closure_function( name: &str, params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], self_ref_callable_capture: Option<&str>, by_ref_return: bool, ) -> FunctionSig { - let mut signature = closure_signature_from_ast(params, variadic, return_type, body, captures, parent.classes); + let mut signature = closure_signature_from_ast( + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + parent.classes, + ); signature.by_ref_return = by_ref_return; lower_closure_function_with_signature( parent, @@ -456,6 +674,7 @@ pub(crate) fn lower_closure_function_with_context( name: &str, params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], @@ -463,7 +682,15 @@ pub(crate) fn lower_closure_function_with_context( self_ref_callable_capture: Option<&str>, by_ref_return: bool, ) -> FunctionSig { - let mut signature = closure_signature_from_ast(params, variadic, return_type, body, captures, parent.classes); + let mut signature = closure_signature_from_ast( + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + parent.classes, + ); signature.by_ref_return = by_ref_return; for (idx, (_, type_ann, _, _)) in params.iter().enumerate() { if type_ann.is_none() { @@ -513,16 +740,15 @@ fn lower_closure_function_with_signature( attach_generator_source_if_needed(&mut function, body, signature.params.len()); let env = env_with_closure_captures(&signature, captures); let lowered_params = params_with_closure_captures(&signature, captures); - let recursive_binding = - self_ref_callable_capture.map(|local_name| RecursiveClosureBinding { - local_name: local_name.to_string(), - closure_name: name.to_string(), - signature: signature.clone(), - capture_names: captures - .iter() - .map(|(capture_name, _, _)| capture_name.clone()) - .collect(), - }); + let recursive_binding = self_ref_callable_capture.map(|local_name| RecursiveClosureBinding { + local_name: local_name.to_string(), + closure_name: name.to_string(), + signature: signature.clone(), + capture_names: captures + .iter() + .map(|(capture_name, _, _)| capture_name.clone()) + .collect(), + }); let closures = lower_body_into_function( &mut function, parent.data, @@ -546,6 +772,8 @@ fn lower_closure_function_with_signature( recursive_binding, false, collect_global_var_names(body), + parent.source_path().map(str::to_string), + None, ); parent.extend_closures(std::iter::once(function).chain(closures)); signature @@ -575,6 +803,13 @@ fn lower_body_into_function( recursive_closure_binding: Option, in_main: bool, all_global_var_names: std::collections::HashSet, + source_path: Option, + eval_scope_reads: Option<( + String, + std::collections::HashSet, + std::collections::HashSet, + std::collections::BTreeSet, + )>, ) -> Vec { let owner_name = function.name.clone(); let function_by_ref_return = function.flags.by_ref_return; @@ -608,8 +843,12 @@ fn lower_body_into_function( return_php_type, in_main, all_global_var_names, + source_path, ); ctx.by_ref_return = function_by_ref_return; + if let Some((scope_param, read_names, write_names, flush_names)) = eval_scope_reads { + ctx.enable_eval_scope_access(scope_param, read_names, write_names, flush_names); + } for (index, (name, php_type)) in params.iter().enumerate() { ctx.declare_local(name, php_type.clone()); ctx.mark_local_initialized(name); @@ -702,17 +941,20 @@ fn terminate_open_block(ctx: &mut LoweringContext<'_, '_>) { return; } if matches!(ctx.return_php_type, PhpType::Never) { - let message = - ctx.intern_string("Fatal error: A never-returning function must not implicitly return\n"); + let message = ctx + .intern_string("Fatal error: A never-returning function must not implicitly return\n"); ctx.builder.terminate(Terminator::Fatal { message }); return; } if ctx.return_type == IrType::Void { + ctx.emit_eval_scope_finalizer(None); ctx.builder.terminate(Terminator::Return { value: None }); return; } + ctx.emit_eval_scope_finalizer(None); let value = emit_default_return_value(ctx); - ctx.builder.terminate(Terminator::Return { value: Some(value) }); + ctx.builder + .terminate(Terminator::Return { value: Some(value) }); } /// Emits a placeholder value compatible with the function return storage type. @@ -832,7 +1074,7 @@ fn function_params(signature: &FunctionSig) -> Vec { } /// Returns an EIR ABI signature that keeps dynamic untyped PHP parameters boxed. -fn eir_signature_with_php_param_contracts( +pub(crate) fn eir_signature_with_php_param_contracts( owner_name: &str, signature: &FunctionSig, callable_param_sigs: &std::collections::HashMap<(String, String), FunctionSig>, @@ -840,11 +1082,21 @@ fn eir_signature_with_php_param_contracts( let mut eir_signature = signature.clone(); let mut has_dynamic_untyped_param = false; for (index, (name, php_type)) in eir_signature.params.iter_mut().enumerate() { - let declared = signature.declared_params.get(index).copied().unwrap_or(false); + let declared = signature + .declared_params + .get(index) + .copied() + .unwrap_or(false); let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); let variadic = signature.variadic.as_deref() == Some(name.as_str()); if !declared && !by_ref && !variadic { - if preserve_untyped_eir_param_contract(owner_name, name, php_type, callable_param_sigs) { + if preserve_untyped_eir_param_contract( + owner_name, + index, + name, + php_type, + callable_param_sigs, + ) { continue; } *php_type = PhpType::Mixed; @@ -873,14 +1125,53 @@ fn eir_runtime_metadata_signature(signature: &FunctionSig) -> FunctionSig { /// Returns true when an inferred untyped parameter has an EIR-safe concrete ABI contract. fn preserve_untyped_eir_param_contract( owner_name: &str, + param_index: usize, param_name: &str, php_type: &PhpType, callable_param_sigs: &std::collections::HashMap<(String, String), FunctionSig>, ) -> bool { - matches!(php_type.codegen_repr(), PhpType::Callable) + magic_method_param_keeps_eir_contract(owner_name, param_index, php_type) + || matches!(php_type.codegen_repr(), PhpType::Callable) || callable_param_sigs.contains_key(&(owner_name.to_string(), param_name.to_string())) } +/// Returns whether a checker-patched magic-method parameter must keep its real ABI type. +fn magic_method_param_keeps_eir_contract( + owner_name: &str, + param_index: usize, + php_type: &PhpType, +) -> bool { + let Some((_, method_name)) = owner_name.rsplit_once("::") else { + return false; + }; + let method_key = php_symbol_key(method_name); + match method_key.as_str() { + "__get" | "__isset" | "__unset" => { + param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str) + } + "__set" => { + param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str) + } + "__call" | "__callstatic" => { + // The $args array keeps its contract only once call sites have + // specialized the element type. The checker seeds it as + // Array; eval-only magic calls never specialize it, and a + // Never element would lower every $args[N] read to an empty + // constant, so those fall back to the boxed Mixed widening. + (param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str)) + || (param_index == 1 + && matches!(php_type.codegen_repr(), PhpType::Array(_)) + // Check the raw element type: codegen_repr normalizes the + // Never seed to Void and would hide it. + && !matches!( + php_type, + PhpType::Array(elem) if matches!(elem.as_ref(), PhpType::Never) + )) + } + _ => false, + } +} + /// Widens inferred container return elements that may be built from dynamic params. fn dynamic_param_container_return_type(return_type: &PhpType) -> PhpType { match return_type.codegen_repr() { @@ -950,19 +1241,21 @@ fn params_with_closure_captures( /// Builds a fallback function signature from AST syntax when checker metadata is unavailable. fn signature_from_ast(params: &AstParams, return_type: Option<&TypeExpr>) -> FunctionSig { - signature_from_ast_with_variadic(params, return_type, None) + signature_from_ast_with_variadic(params, return_type, None, false) } /// Builds an EIR closure signature and infers fallthrough-only closures as `void`. fn closure_signature_from_ast( params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], classes: &std::collections::HashMap, ) -> FunctionSig { - let mut signature = signature_from_ast_with_variadic(params, return_type, variadic); + let mut signature = + signature_from_ast_with_variadic(params, return_type, variadic, variadic_by_ref); if crate::types::checker::yield_validation::body_contains_yield(body) { signature.return_type = PhpType::Object("Generator".to_string()); return signature; @@ -1091,10 +1384,7 @@ fn stmt_contains_value_return(stmt: &Stmt) -> bool { | StmtKind::IncludeOnceGuard { body, .. } | StmtKind::Synthetic(body) => body_contains_value_return(body), StmtKind::For { - init, - update, - body, - .. + init, update, body, .. } => { init.as_ref() .is_some_and(|stmt| stmt_contains_value_return(stmt.as_ref())) @@ -1133,6 +1423,7 @@ fn signature_from_ast_with_variadic( params: &AstParams, return_type: Option<&TypeExpr>, variadic: Option<&str>, + variadic_by_ref: bool, ) -> FunctionSig { let mut signature = FunctionSig { params: params @@ -1140,11 +1431,21 @@ fn signature_from_ast_with_variadic( .map(|(name, ty, _, _)| { ( name.clone(), - ty.as_ref().map(type_expr_to_php_type).unwrap_or(PhpType::Mixed), + ty.as_ref() + .map(type_expr_to_php_type) + .unwrap_or(PhpType::Mixed), ) }) .collect(), - defaults: params.iter().map(|(_, _, default, _)| default.clone()).collect(), + param_type_exprs: params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .collect(), + param_attributes: Vec::new(), + defaults: params + .iter() + .map(|(_, _, default, _)| default.clone()) + .collect(), return_type: return_type .map(type_expr_to_php_type) .unwrap_or(PhpType::Mixed), @@ -1155,12 +1456,12 @@ fn signature_from_ast_with_variadic( variadic: variadic.map(str::to_string), deprecation: None, }; - append_variadic_param_slot(&mut signature); + append_variadic_param_slot(&mut signature, variadic_by_ref); signature } /// Adds the variadic `array` parameter slot omitted from parsed parameter tuples. -fn append_variadic_param_slot(signature: &mut FunctionSig) { +fn append_variadic_param_slot(signature: &mut FunctionSig, variadic_by_ref: bool) { let Some(variadic) = signature.variadic.clone() else { return; }; @@ -1170,8 +1471,9 @@ fn append_variadic_param_slot(signature: &mut FunctionSig) { signature .params .push((variadic, PhpType::Array(Box::new(PhpType::Mixed)))); + signature.param_type_exprs.push(None); signature.defaults.push(None); - signature.ref_params.push(false); + signature.ref_params.push(variadic_by_ref); signature.declared_params.push(false); } diff --git a/src/ir_lower/mod.rs b/src/ir_lower/mod.rs index 7d43f8adc9..febfa3e545 100644 --- a/src/ir_lower/mod.rs +++ b/src/ir_lower/mod.rs @@ -18,12 +18,14 @@ mod fibers; mod function; mod ownership; mod program; +mod reflection; mod stmt; #[cfg(test)] mod tests; use std::fmt; +use std::path::Path; use crate::codegen::platform::Target; use crate::ir::{Module, ValidationError}; @@ -36,7 +38,17 @@ pub fn lower_program( check_result: &CheckResult, target: Target, ) -> Result { - program::lower(program, check_result, target) + program::lower(program, check_result, target, None) +} + +/// Lowers `program` into an EIR module and records the main PHP source path. +pub fn lower_program_with_source_path( + program: &Program, + check_result: &CheckResult, + target: Target, + source_path: &Path, +) -> Result { + program::lower(program, check_result, target, Some(source_path)) } /// Error produced while building or validating EIR. diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 14e4ce6b01..ff76839262 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -9,34 +9,66 @@ //! statements themselves are no-ops inside `main`. //! - The module is validated before it is returned to CLI/test callers. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::path::Path; use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; use crate::intrinsics::IntrinsicCall; use crate::ir::{ - validate_module, ExternDecl, ExternParamDecl, Function, Immediate, IrType, Module, Op, + validate_module, ExternDecl, ExternParamDecl, Function, Immediate, IrType, LocalKind, Module, + Op, TraitMethodInfo, }; use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; -use crate::parser::ast::{ClassMethod, ExprKind, Program, Stmt, StmtKind}; -use crate::types::{CheckResult, ClassInfo, InterfaceInfo, PhpType}; +use crate::parser::ast::{ + ClassMethod, Expr, ExprKind, Program, StaticReceiver, Stmt, StmtKind, Visibility, +}; +use crate::types::{CheckResult, ClassInfo, FunctionSig, InterfaceInfo, PhpType}; /// Lowers an optimized typed AST program into a validated EIR module. pub(crate) fn lower( program: &Program, check_result: &CheckResult, target: Target, + source_path: Option<&Path>, ) -> Result { let mut module = Module::new(target); + module.source_path = source_path.map(canonical_source_path); let constants = crate::codegen::collect_constants(program, target.platform); + module.global_constants = constants.clone(); let fiber_return_sigs = crate::ir_lower::fibers::collect_fiber_return_sigs(program); populate_metadata(&mut module, program, check_result); - lower_function_declarations(program, &mut module, check_result, &constants, &fiber_return_sigs); - lower_class_like_methods(program, &mut module, check_result, &constants, &fiber_return_sigs); + lower_function_declarations( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); + lower_class_like_methods( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); lower_property_init_thunks(&mut module, check_result, &constants, &fiber_return_sigs); - lower_builtin_reflection_methods(&mut module, check_result, &constants, &fiber_return_sigs); - function::lower_main(program, &mut module, check_result, &constants, &fiber_return_sigs); + function::lower_main( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); + lower_literal_eval_aot_functions(&mut module, check_result, &constants, &fiber_return_sigs); + include_lowered_runtime_features(&mut module); + super::reflection::lower_referenced_builtin_methods( + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); lower_referenced_builtin_spl_methods(&mut module, check_result, &constants, &fiber_return_sigs); builtin_datetime::lower_referenced_builtin_datetime_methods( &mut module, @@ -49,6 +81,15 @@ pub(crate) fn lower( Ok(module) } +/// Converts a PHP source path into the canonical display string stored in EIR metadata. +fn canonical_source_path(source_path: &Path) -> String { + source_path + .canonicalize() + .unwrap_or_else(|_| source_path.to_path_buf()) + .display() + .to_string() +} + /// Copies declaration metadata into the EIR module placeholder tables. fn populate_metadata(module: &mut Module, program: &Program, check_result: &CheckResult) { module.class_table.names = sorted_keys(&check_result.classes); @@ -59,8 +100,18 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_interface_names = collect_declared_interface_names(program, &check_result.interfaces); module.declared_trait_names = collect_declared_trait_names(program); + module.declared_trait_source_lines = collect_declared_trait_source_lines(program); module.declared_trait_uses = collect_declared_trait_uses(program); + module.declared_trait_method_names = collect_declared_trait_method_names(program); + module.declared_trait_methods = collect_declared_trait_methods(program); + module.declared_trait_property_names = collect_declared_trait_property_names(program); + module.declared_trait_constant_names = collect_declared_trait_constant_names(program); + module.declared_trait_constants = collect_declared_trait_constants(program); + module.declared_trait_constant_visibilities = + collect_declared_trait_constant_visibilities(program); + module.declared_trait_final_constants = collect_declared_trait_final_constants(program); module.class_infos = check_result.classes.clone(); + normalize_class_method_signatures_for_eir(module, &check_result.callable_param_sigs); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); module.extern_class_infos = check_result.extern_classes.clone(); @@ -91,19 +142,239 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec crate::codegen::runtime_features_for_program_and_classes(program, &check_result.classes); } +/// Normalizes class method metadata to the ABI contracts emitted in EIR. +fn normalize_class_method_signatures_for_eir( + module: &mut Module, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) { + for (class_name, class_info) in module.class_infos.iter_mut() { + normalize_method_map_for_eir( + class_name, + &mut class_info.methods, + &class_info.method_decls, + false, + callable_param_sigs, + ); + normalize_method_map_for_eir( + class_name, + &mut class_info.static_methods, + &class_info.method_decls, + true, + callable_param_sigs, + ); + } +} + +/// Normalizes one instance/static method table for EIR call and bridge metadata. +fn normalize_method_map_for_eir( + class_name: &str, + methods: &mut HashMap, + method_decls: &[ClassMethod], + is_static: bool, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) { + // Stream-wrapper and user-filter contract methods are invoked through + // runtime vtables with raw fixed-ABI arguments; widening their untyped + // params to boxed Mixed would desynchronize the dispatcher and the body. + let is_wrapper_class = methods.contains_key("stream_open"); + let is_filter_class = methods.contains_key("filter"); + for (method_key, signature) in methods.iter_mut() { + if (is_wrapper_class + && crate::codegen_support::runtime::is_user_wrapper_contract_method(method_key)) + || (is_filter_class + && crate::codegen_support::runtime::is_user_filter_contract_method(method_key)) + { + continue; + } + let owner_name = format!("{}::{}", class_name, method_key); + let mut normalized = function::eir_signature_with_php_param_contracts( + &owner_name, + signature, + callable_param_sigs, + ); + if method_decls + .iter() + .find(|method| { + method.is_static == is_static && php_symbol_key(&method.name) == *method_key + }) + .is_some_and(|method| { + method_return_exposes_dynamic_param( + method, + signature, + &owner_name, + callable_param_sigs, + ) + }) + { + normalized.return_type = PhpType::Mixed; + } + *signature = normalized; + } +} + +/// Returns true when an untyped method return can expose a dynamic parameter directly. +fn method_return_exposes_dynamic_param( + method: &ClassMethod, + signature: &FunctionSig, + owner_name: &str, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) -> bool { + if signature.declared_return { + return false; + } + let dynamic_params = dynamic_untyped_param_names(owner_name, signature, callable_param_sigs); + !dynamic_params.is_empty() && body_returns_dynamic_param(&method.body, &dynamic_params) +} + +/// Collects untyped by-value parameter names that need a boxed EIR ABI. +fn dynamic_untyped_param_names( + owner_name: &str, + signature: &FunctionSig, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) -> HashSet { + let mut names = HashSet::new(); + for (index, (name, php_type)) in signature.params.iter().enumerate() { + let declared = signature + .declared_params + .get(index) + .copied() + .unwrap_or(false); + let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); + let variadic = signature.variadic.as_deref() == Some(name.as_str()); + let preserved = matches!(php_type.codegen_repr(), PhpType::Callable) + || callable_param_sigs.contains_key(&(owner_name.to_string(), name.to_string())); + if !declared && !by_ref && !variadic && !preserved { + names.insert(name.clone()); + } + } + names +} + +/// Recursively scans a method body for returns that expose dynamic parameters. +fn body_returns_dynamic_param(body: &[Stmt], dynamic_params: &HashSet) -> bool { + body.iter() + .any(|stmt| stmt_returns_dynamic_param(stmt, dynamic_params)) +} + +/// Returns true when one statement can return a dynamic parameter directly. +fn stmt_returns_dynamic_param(stmt: &Stmt, dynamic_params: &HashSet) -> bool { + match &stmt.kind { + StmtKind::Return(Some(expr)) => expr_exposes_dynamic_param(expr, dynamic_params), + StmtKind::Return(None) => false, + StmtKind::If { + then_body, + elseif_clauses, + else_body, + .. + } => { + body_returns_dynamic_param(then_body, dynamic_params) + || elseif_clauses + .iter() + .any(|(_, body)| body_returns_dynamic_param(body, dynamic_params)) + || else_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + body_returns_dynamic_param(then_body, dynamic_params) + || else_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::While { body, .. } + | StmtKind::DoWhile { body, .. } + | StmtKind::Foreach { body, .. } + | StmtKind::NamespaceBlock { body, .. } + | StmtKind::IncludeOnceGuard { body, .. } + | StmtKind::Synthetic(body) => body_returns_dynamic_param(body, dynamic_params), + StmtKind::For { + init, update, body, .. + } => { + init.as_ref() + .is_some_and(|stmt| stmt_returns_dynamic_param(stmt.as_ref(), dynamic_params)) + || update + .as_ref() + .is_some_and(|stmt| stmt_returns_dynamic_param(stmt.as_ref(), dynamic_params)) + || body_returns_dynamic_param(body, dynamic_params) + } + StmtKind::Switch { cases, default, .. } => { + cases + .iter() + .any(|(_, body)| body_returns_dynamic_param(body, dynamic_params)) + || default + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + body_returns_dynamic_param(try_body, dynamic_params) + || catches + .iter() + .any(|catch| body_returns_dynamic_param(&catch.body, dynamic_params)) + || finally_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + _ => false, + } +} + +/// Returns true when an expression can yield one of the dynamic parameters directly. +fn expr_exposes_dynamic_param(expr: &Expr, dynamic_params: &HashSet) -> bool { + match &expr.kind { + ExprKind::Variable(name) => dynamic_params.contains(name), + ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { + expr_exposes_dynamic_param(value, dynamic_params) + || expr_exposes_dynamic_param(default, dynamic_params) + } + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => { + expr_exposes_dynamic_param(then_expr, dynamic_params) + || expr_exposes_dynamic_param(else_expr, dynamic_params) + } + ExprKind::Match { arms, default, .. } => { + arms.iter() + .any(|(_, arm)| expr_exposes_dynamic_param(arm, dynamic_params)) + || default + .as_ref() + .is_some_and(|expr| expr_exposes_dynamic_param(expr, dynamic_params)) + } + ExprKind::ErrorSuppress(inner) => expr_exposes_dynamic_param(inner, dynamic_params), + _ => false, + } +} + /// Adds optional runtime features referenced by synthetic or lowered EIR functions. -fn include_lowered_runtime_features(module: &mut Module) { +pub(super) fn include_lowered_runtime_features(module: &mut Module) { let features = lowered_runtime_features(module); module.required_runtime_features.regex |= features.regex; module.required_runtime_features.phar_archive |= features.phar_archive; module.required_runtime_features.descriptor_invoker |= features.descriptor_invoker; + module.required_runtime_features.eval_bridge |= features.eval_bridge; + module.required_runtime_features.eval_scope |= features.eval_scope; } /// Derives optional runtime features from the actual EIR instruction stream. fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { let mut features = RuntimeFeatures::none(); for function in all_lowered_functions(module) { - for inst in &function.instructions { + if function_contains_eval_scope_state(function) { + features.eval_scope = true; + } + if function_contains_eval_context_state(function) { + features.eval_bridge = true; + } + for (inst_index, inst) in function.instructions.iter().enumerate() { match inst.op { Op::BuiltinCall => { if builtin_call_requires_regex(module, inst) { @@ -115,19 +386,571 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { if builtin_call_requires_descriptor_invoker(module, function, inst) { features.descriptor_invoker = true; } + if builtin_call_requires_eval(module, inst) { + features.eval_bridge = true; + } + } + Op::EvalLiteralCall => { + if eval_literal_call_requires_bridge(module, function, inst_index, inst) { + features.eval_bridge = true; + } + } + Op::EvalScopeGet | Op::EvalScopeSet => { + features.eval_scope = true; + } + Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalFunctionExists + | Op::EvalClassExists + | Op::EvalConstantExists + | Op::EvalConstantFetch + | Op::EvalStaticMethodCall => { + features.eval_bridge = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { features.descriptor_invoker = true; } - _ => {} + _ => {} + } + } + } + features +} + +/// Returns true when a lowered function owns hidden eval scope handle slots. +/// Scope-only functions use the native scope helpers and must not force the +/// magician bridge staticlib into the link. +fn function_contains_eval_scope_state(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns true when a lowered function owns an interpreter context handle, +/// which requires the full magician eval bridge runtime. +fn function_contains_eval_context_state(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| matches!(local.kind, LocalKind::EvalContext)) +} + +/// Returns true when a literal eval call still needs the magician bridge runtime. +fn eval_literal_call_requires_bridge( + module: &Module, + function: &Function, + inst_index: usize, + inst: &crate::ir::Instruction, +) -> bool { + let Some(Immediate::Data(data)) = inst.immediate else { + return true; + }; + let Some(fragment) = module.data.strings.get(data.as_raw() as usize) else { + return true; + }; + if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + return false; + } + if eval_literal_call_can_use_direct_read_write(function, inst_index, fragment) { + return false; + } + if eval_literal_call_can_use_local_scalar_direct_sync(module, function, fragment) { + return false; + } + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_module(module, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_module(module, receiver, method, args) + }, + ); + if plan.uses_scope_read_params() { + return !eval_literal_call_can_use_scope_read_params(module, function, inst_index, &plan); + } + if plan.requires_runtime_eval_scope() + && !eval_literal_call_scope_constraints_supported(module, function, inst_index, &plan) + { + return true; + } + plan.requires_runtime_eval_bridge() +} + +/// Returns true when a boxed read/write eval can read and update caller locals directly. +fn eval_literal_call_can_use_direct_read_write( + function: &Function, + inst_index: usize, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_direct_local_read_write_writes(fragment) + else { + return false; + }; + writes.iter().all(|(name, kind)| { + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_direct_read_write_slot_initialized(function, slot.id, inst_index) + && eval_direct_read_write_kind_supported(function, slot, inst_index, *kind) + }) +} + +/// Returns true when a local slot is initialized before the eval instruction. +fn eval_direct_read_write_slot_initialized( + function: &Function, + slot: crate::ir::LocalSlotId, + inst_index: usize, +) -> bool { + if function + .params + .get(slot.as_raw() as usize) + .is_some_and(|param| !param.by_ref) + { + return true; + } + function + .instructions + .iter() + .take(inst_index) + .any(|inst| inst.op == Op::StoreLocal && inst.immediate == Some(Immediate::LocalSlot(slot))) +} + +/// Returns true when a direct read/write value kind fits the caller slot type. +fn eval_direct_read_write_kind_supported( + function: &Function, + slot: &crate::ir::LocalSlot, + inst_index: usize, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match slot.php_type.codegen_repr() { + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Mixed | PhpType::Union(_) => { + kind == crate::eval_aot::DirectLocalStoreScalarKind::Float + && eval_direct_read_write_previous_store_type(function, slot.id, inst_index) + .is_some_and(|ty| matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float)) + } + _ => false, + } +} + +/// Returns the source type of the latest direct local store before an eval instruction. +fn eval_direct_read_write_previous_store_type( + function: &Function, + slot: crate::ir::LocalSlotId, + inst_index: usize, +) -> Option { + function + .instructions + .iter() + .take(inst_index) + .rev() + .find(|inst| { + inst.op == Op::StoreLocal && inst.immediate == Some(Immediate::LocalSlot(slot)) + }) + .and_then(|inst| inst.operands.first().copied()) + .and_then(|value| function.value(value)) + .map(|value| value.php_type.codegen_repr()) +} + +/// Returns true when a read-only eval call can pass direct Mixed params safely. +fn eval_literal_call_can_use_scope_read_params( + module: &Module, + function: &Function, + inst_index: usize, + plan: &crate::eval_aot::EvalAotPlan, +) -> bool { + plan.reads().iter().all(|name| { + eval_literal_call_scope_read_param_supported(module, function, inst_index, name) + }) && plan.array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_array_param_supported(module, function, inst_index, name) + }) && plan.assoc_array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_assoc_array_param_supported(module, function, inst_index, name) + }) && plan.float_predicate_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_float_predicate_param_supported( + module, function, inst_index, name, + ) + }) +} + +/// Returns true when scope-based eval AOT satisfies caller-side type constraints. +fn eval_literal_call_scope_constraints_supported( + module: &Module, + function: &Function, + inst_index: usize, + plan: &crate::eval_aot::EvalAotPlan, +) -> bool { + plan.array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_array_param_supported(module, function, inst_index, name) + }) && plan.assoc_array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_assoc_array_param_supported(module, function, inst_index, name) + }) && plan.float_predicate_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_float_predicate_param_supported( + module, function, inst_index, name, + ) + }) +} + +/// Returns true when one caller read can be boxed or represented as undefined null. +fn eval_literal_call_scope_read_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return true; + }; + eval_scope_read_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read is initialized with an array-compatible type. +fn eval_literal_call_scope_read_array_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_array_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read is initialized with an associative-array type. +fn eval_literal_call_scope_read_assoc_array_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_assoc_array_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read can feed IEEE float predicates safely. +fn eval_literal_call_scope_read_float_predicate_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_float_predicate_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when a caller local can be boxed into a direct eval read param. +fn eval_scope_read_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Void + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Returns true when a caller local satisfies array-only read-param semantics. +fn eval_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} + +/// Returns true when a caller local satisfies associative-array-only semantics. +fn eval_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) +} + +/// Returns true when a caller local can feed IEEE float predicates without TypeError. +fn eval_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) +} + +/// Returns true when the legacy local-scalar AOT path can avoid eval scope runtime. +fn eval_literal_call_can_use_local_scalar_direct_sync( + module: &Module, + function: &Function, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_local_scalar_writes_with_static_calls( + fragment, + |name, args| eval_literal_static_function_supported_by_module(module, name, args), + ) else { + return false; + }; + writes.iter().all(|(name, kind)| { + eval_local_scalar_direct_sync_slot(function, name) + .is_none_or(|slot| eval_local_scalar_direct_sync_kind_supported(&slot.php_type, *kind)) + }) +} + +/// Returns the caller local slot that would receive a direct local-scalar eval write. +fn eval_local_scalar_direct_sync_slot<'a>( + function: &'a Function, + name: &str, +) -> Option<&'a crate::ir::LocalSlot> { + function + .locals + .iter() + .find(|local| local.name.as_deref() == Some(name) && local.kind == LocalKind::PhpLocal) +} + +/// Returns true when a local-scalar value can be stored in the caller slot type. +fn eval_local_scalar_direct_sync_kind_supported( + target_ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::TaggedScalar => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + _ => false, + } +} + +/// Returns true when a static function call matches the codegen-supported subset. +fn eval_literal_static_function_supported_by_module( + module: &Module, + name: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(function) = module + .functions + .iter() + .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) + else { + return false; + }; + let Some(signature) = &function.signature else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Returns true when a static method call matches the codegen-supported subset. +fn eval_literal_static_method_supported_by_module( + module: &Module, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + let class_name = class_name.as_str().trim_start_matches('\\'); + let method_key = php_symbol_key(method); + let Some(receiver_info) = module.class_infos.get(class_name) else { + return false; + }; + if receiver_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; + } + let impl_class = receiver_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + let Some(signature) = module + .class_infos + .get(impl_class) + .and_then(|class_info| class_info.static_methods.get(&method_key)) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Adds internal EIR functions for literal eval fragments accepted by the EIR AOT subset. +fn lower_literal_eval_aot_functions( + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let candidates = collect_literal_eval_aot_function_candidates(module); + let mut lowered_names = all_lowered_functions(module) + .map(|function| function.name.clone()) + .collect::>(); + for (name, body) in candidates { + match body { + EvalAotFunctionCandidate::NoScope { body } => { + if !lowered_names.insert(name.clone()) { + continue; + } + function::lower_eval_aot_function( + &name, + &body, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + EvalAotFunctionCandidate::ScopeRead { + body, + reads, + direct_writes, + flush_writes, + } => { + if !lowered_names.insert(name.clone()) { + continue; + } + function::lower_eval_aot_scope_read_function( + &name, + &body, + &reads, + &direct_writes, + &flush_writes, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + } + } +} + +/// Candidate shape for an internal eval AOT EIR function. +enum EvalAotFunctionCandidate { + NoScope { + body: Program, + }, + ScopeRead { + body: Program, + reads: BTreeSet, + direct_writes: BTreeSet, + flush_writes: BTreeSet, + }, +} + +/// Collects unique literal eval fragments that can be emitted as no-scope EIR functions. +fn collect_literal_eval_aot_function_candidates( + module: &Module, +) -> Vec<(String, EvalAotFunctionCandidate)> { + let mut candidates = Vec::new(); + let mut seen = HashSet::new(); + for function in all_lowered_functions(module) { + for (inst_index, inst) in function.instructions.iter().enumerate() { + let Some(fragment) = eval_literal_fragment_from_inst(module, inst) else { + continue; + }; + let mut plan = + crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + &fragment, + module.source_path.as_deref(), + |name, args| { + eval_literal_static_function_supported_by_module(module, name, args) + }, + |receiver, method, args| { + eval_literal_static_method_supported_by_module( + module, receiver, method, args, + ) + }, + ); + if let Some(name) = plan.take_function_name() { + if !seen.insert(name.clone()) { + continue; + } + let Some(program) = plan.take_eir_program() else { + continue; + }; + candidates.push((name, EvalAotFunctionCandidate::NoScope { body: program })); + continue; } + if crate::eval_aot::literal_fragment_direct_local_store_writes(&fragment).is_some() + || eval_literal_call_can_use_direct_read_write(function, inst_index, &fragment) + || eval_literal_call_can_use_local_scalar_direct_sync(module, function, &fragment) + { + continue; + } + let Some(name) = plan.take_scope_read_function_name() else { + continue; + }; + if !seen.insert(name.clone()) { + continue; + }; + let reads = plan.reads().clone(); + let direct_writes = plan.direct_writes().clone(); + let flush_writes = plan.flush_writes().clone(); + let Some(program) = plan.take_scope_read_eir_program() else { + continue; + }; + candidates.push(( + name, + EvalAotFunctionCandidate::ScopeRead { + body: program, + reads, + direct_writes, + flush_writes, + }, + )); } } - features + candidates +} + +/// Returns the string payload from an `EvalLiteralCall` instruction. +fn eval_literal_fragment_from_inst( + module: &Module, + inst: &crate::ir::Instruction, +) -> Option { + if inst.op != Op::EvalLiteralCall { + return None; + } + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module.data.strings.get(data.as_raw() as usize).cloned() } /// Iterates every function-like body already materialized into the EIR module. -fn all_lowered_functions(module: &Module) -> impl Iterator { +pub(super) fn all_lowered_functions(module: &Module) -> impl Iterator { module .functions .iter() @@ -190,6 +1013,14 @@ fn builtin_call_requires_descriptor_invoker( .is_some_and(|value| value.php_type.codegen_repr() == PhpType::Str) } +/// Returns true when a lowered builtin call references the optional eval bridge. +fn builtin_call_requires_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + let Some(name) = builtin_call_name(module, inst) else { + return false; + }; + is_eval_builtin_name(name) +} + /// Returns the canonical builtin name attached to a lowered builtin instruction. fn builtin_call_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { @@ -214,6 +1045,11 @@ fn string_callback_operand_index(name: &str) -> Option { } } +/// Returns true when a builtin name denotes PHP's eval language construct. +fn is_eval_builtin_name(name: &str) -> bool { + php_symbol_key(name.trim_start_matches('\\')) == "eval" +} + /// Returns true when a builtin name is lowered through the regex runtime helpers. fn is_regex_builtin_name(name: &str) -> bool { matches!( @@ -265,6 +1101,9 @@ fn lower_property_init_thunks( let mut classes = check_result.classes.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { + if super::reflection::canonical_builtin_reflection_class_name(class_name).is_some() { + continue; + } function::lower_property_init_thunk( class_name, class_info, @@ -338,7 +1177,24 @@ fn collect_declared_trait_names(program: &Program) -> Vec { names } -/// Collects direct trait-use declarations keyed by the declaring trait name. +/// Collects source line metadata for user-declared traits, keyed by trait name. +fn collect_declared_trait_source_lines(program: &Program) -> HashMap { + let mut lines = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { name, .. } => { + lines.insert(name.clone(), stmt.span.line); + } + StmtKind::NamespaceBlock { body, .. } => { + lines.extend(collect_declared_trait_source_lines(body)); + } + _ => {} + } + } + lines +} + +/// Collects direct trait-to-trait use declarations keyed by declaring trait name. fn collect_declared_trait_uses(program: &Program) -> HashMap> { let mut uses = HashMap::new(); for stmt in program { @@ -364,6 +1220,210 @@ fn collect_declared_trait_uses(program: &Program) -> HashMap uses } +/// Collects direct PHP method names declared by each trait in source order. +fn collect_declared_trait_method_names(program: &Program) -> HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| php_symbol_key(&method.name)) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_method_names(body)); + } + _ => {} + } + } + methods +} + +/// Collects direct trait method metadata keyed by trait and PHP method key. +fn collect_declared_trait_methods( + program: &Program, +) -> HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| { + let method_key = php_symbol_key(&method.name); + let info = TraitMethodInfo { + signature: function::method_signature_from_ast(method), + visibility: method.visibility.clone(), + is_static: method.is_static, + is_final: method.is_final, + is_abstract: method.is_abstract, + }; + (method_key, info) + }) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_methods(body)); + } + _ => {} + } + } + methods +} + +/// Collects direct PHP property names declared by each trait in source order. +fn collect_declared_trait_property_names(program: &Program) -> HashMap> { + let mut properties = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + properties: trait_properties, + .. + } => { + properties.insert( + name.clone(), + trait_properties + .iter() + .map(|property| property.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + properties.extend(collect_declared_trait_property_names(body)); + } + _ => {} + } + } + properties +} + +/// Collects direct PHP constant names declared by each trait in source order. +fn collect_declared_trait_constant_names(program: &Program) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constant_names(body)); + } + _ => {} + } + } + constants +} + +/// Collects direct PHP constant expressions declared by each trait. +fn collect_declared_trait_constants(program: &Program) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| (constant.name.clone(), constant.value.clone())) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constants(body)); + } + _ => {} + } + } + constants +} + +/// Collects direct PHP constant visibilities declared by each trait. +fn collect_declared_trait_constant_visibilities( + program: &Program, +) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| (constant.name.clone(), constant.visibility.clone())) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constant_visibilities(body)); + } + _ => {} + } + } + constants +} + +/// Collects direct PHP final constant names declared by each trait. +fn collect_declared_trait_final_constants(program: &Program) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .filter(|constant| constant.is_final) + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_final_constants(body)); + } + _ => {} + } + } + constants +} + /// Recursively collects source-declared names that are present in checked metadata. fn collect_program_declared_names( program: &Program, @@ -441,13 +1501,16 @@ fn lower_function_declarations( name, params, variadic: _, + variadic_by_ref: _, variadic_type: _, return_type, body, + .. } => function::lower_user_function( name, params, return_type.as_ref(), + &stmt.attributes, body, module, check_result, @@ -457,7 +1520,13 @@ fn lower_function_declarations( StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) | StmtKind::IncludeOnceGuard { body, .. } => { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::If { then_body, @@ -465,12 +1534,30 @@ fn lower_function_declarations( else_body, .. } => { - lower_function_declarations(then_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for (_, body) in elseif_clauses { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = else_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::IfDef { @@ -478,23 +1565,53 @@ fn lower_function_declarations( else_body, .. } => { - lower_function_declarations(then_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); if let Some(body) = else_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::While { body, .. } | StmtKind::DoWhile { body, .. } | StmtKind::For { body, .. } | StmtKind::Foreach { body, .. } => { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::Switch { cases, default, .. } => { for (_, body) in cases { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = default { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::Try { @@ -502,12 +1619,30 @@ fn lower_function_declarations( catches, finally_body, } => { - lower_function_declarations(try_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + try_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for catch in catches { - lower_function_declarations(&catch.body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + &catch.body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = finally_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } _ => {} @@ -531,11 +1666,25 @@ fn lower_class_like_methods( .get(name) .map(|class_info| class_info.method_decls.as_slice()) .unwrap_or(methods.as_slice()); - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::TraitDecl { .. } => {} StmtKind::InterfaceDecl { name, methods, .. } => { - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::EnumDecl { name, methods, .. } => { // Enum methods are lowered like class methods on the case singleton; prefer the @@ -545,7 +1694,14 @@ fn lower_class_like_methods( .get(name) .map(|class_info| class_info.method_decls.as_slice()) .unwrap_or(methods.as_slice()); - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) @@ -558,12 +1714,30 @@ fn lower_class_like_methods( else_body, .. } => { - lower_class_like_methods(then_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for (_, body) in elseif_clauses { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = else_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::IfDef { @@ -571,9 +1745,21 @@ fn lower_class_like_methods( else_body, .. } => { - lower_class_like_methods(then_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); if let Some(body) = else_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::While { body, .. } @@ -584,10 +1770,22 @@ fn lower_class_like_methods( } StmtKind::Switch { cases, default, .. } => { for (_, body) in cases { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = default { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::Try { @@ -595,12 +1793,30 @@ fn lower_class_like_methods( catches, finally_body, } => { - lower_class_like_methods(try_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + try_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for catch in catches { - lower_class_like_methods(&catch.body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + &catch.body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = finally_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } _ => {} @@ -640,76 +1856,6 @@ fn lower_methods_for_class_like( } } -/// Lowers the synthetic reflection methods injected by the checker. -fn lower_builtin_reflection_methods( - module: &mut Module, - check_result: &CheckResult, - constants: &std::collections::HashMap, - fiber_return_sigs: &std::collections::HashMap, -) { - for class_name in [ - "ReflectionAttribute", - "ReflectionClass", - "ReflectionMethod", - "ReflectionProperty", - "ReflectionFunction", - "ReflectionParameter", - "ReflectionNamedType", - ] { - lower_builtin_reflection_class_methods(class_name, module, check_result, constants, fiber_return_sigs); - } -} - -/// Lowers all concrete synthetic methods for one builtin reflection class. -fn lower_builtin_reflection_class_methods( - class_name: &str, - module: &mut Module, - check_result: &CheckResult, - constants: &std::collections::HashMap, - fiber_return_sigs: &std::collections::HashMap, -) { - let Some(class_info) = check_result.classes.get(class_name) else { - return; - }; - let before = module.class_methods.len(); - for method in &class_info.method_decls { - if !method.has_body { - continue; - } - let generated_body; - let method_key = crate::names::php_symbol_key(&method.name); - let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { - generated_body = - crate::codegen::reflection::build_attribute_new_instance_body(&check_result.classes); - generated_body.as_slice() - } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { - // Materialize captured attribute arguments through the normal array - // lowering (named arguments and associative arrays included) rather - // than a bespoke codegen path. - generated_body = - crate::codegen::reflection::build_attribute_get_arguments_body(&check_result.classes); - generated_body.as_slice() - } else { - &method.body - }; - function::lower_class_method( - class_name, - &method.name, - method.is_static, - &method.params, - method.return_type.as_ref(), - body, - module, - check_result, - constants, - fiber_return_sigs, - ); - } - for method in module.class_methods.iter_mut().skip(before) { - method.flags.is_synthetic = true; - } -} - /// Lowers the small builtin SPL method slice currently consumed by the EIR backend. fn lower_referenced_builtin_spl_methods( module: &mut Module, @@ -731,7 +1877,14 @@ fn lower_referenced_builtin_spl_methods( let before = module.class_methods.len(); for (class_name, method_key) in methods { - lower_builtin_spl_method(&class_name, &method_key, module, check_result, constants, fiber_return_sigs); + lower_builtin_spl_method( + &class_name, + &method_key, + module, + check_result, + constants, + fiber_return_sigs, + ); } for method in module.class_methods.iter_mut().skip(before) { method.flags.is_synthetic = true; @@ -799,6 +1952,14 @@ fn referenced_builtin_spl_methods(module: &Module) -> Vec<(String, String)> { push_builtin_spl_metadata_methods(&mut methods, module, class_name); } } + Op::DynamicObjectNewWithoutConstructorMixed => { + for class_name in module.class_infos.keys() { + if !is_dynamic_new_mixed_metadata_candidate(class_name) { + continue; + } + push_builtin_spl_metadata_methods(&mut methods, module, class_name); + } + } Op::MethodCall | Op::NullsafeMethodCall => { let Some(receiver) = inst.operands.first().copied() else { continue; @@ -887,9 +2048,6 @@ fn supported_dynamic_new_builtin_class_name(class_name: &str) -> bool { | "overflowexception" | "rangeexception" | "recursivecallbackfilteriterator" - | "reflectionclass" - | "reflectionmethod" - | "reflectionproperty" | "runtimeexception" | "spldoublylinkedlist" | "splfixedarray" @@ -952,6 +2110,7 @@ fn known_dynamic_new_builtin_class_name(class_name: &str) -> bool { | "reflectionattribute" | "reflectionclass" | "reflectionmethod" + | "reflectionparameter" | "reflectionproperty" | "regexiterator" | "runtimeexception" @@ -996,7 +2155,10 @@ fn push_supported_builtin_spl_method_for_receiver( } /// Returns the class-name immediate attached to an instruction. -fn class_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { +pub(super) fn class_data_name<'a>( + module: &'a Module, + inst: &crate::ir::Instruction, +) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { return None; }; @@ -1008,7 +2170,7 @@ fn class_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Opt } /// Parses dynamic object factory fallback and required-parent metadata. -fn dynamic_object_new_metadata_names<'a>( +pub(super) fn dynamic_object_new_metadata_names<'a>( module: &'a Module, inst: &crate::ir::Instruction, ) -> Option<(&'a str, &'a str)> { @@ -1016,7 +2178,10 @@ fn dynamic_object_new_metadata_names<'a>( } /// Returns the string immediate attached to an instruction. -fn string_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { +pub(super) fn string_data_name<'a>( + module: &'a Module, + inst: &crate::ir::Instruction, +) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { return None; }; @@ -1028,7 +2193,7 @@ fn string_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Op } /// Normalizes a PHP method name for metadata lookups. -fn php_method_key(method_name: &str) -> String { +pub(super) fn php_method_key(method_name: &str) -> String { crate::names::php_symbol_key(method_name) } @@ -1064,7 +2229,11 @@ fn push_builtin_spl_interface_metadata_methods( return; }; let mut seen = HashSet::new(); - let mut stack = class_info.interfaces.iter().map(String::as_str).collect::>(); + let mut stack = class_info + .interfaces + .iter() + .map(String::as_str) + .collect::>(); while let Some(interface_name) = stack.pop() { if !seen.insert(interface_name.to_string()) { continue; @@ -1079,12 +2248,7 @@ fn push_builtin_spl_interface_metadata_methods( continue; } } - push_supported_builtin_spl_method_for_receiver( - methods, - module, - class_name, - method_key, - ); + push_supported_builtin_spl_method_for_receiver(methods, module, class_name, method_key); } stack.extend(interface_info.parents.iter().map(String::as_str)); } @@ -1363,18 +2527,14 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { "__construct" | "current" | "key" | "getflags" | "setflags" ), "GlobIterator" => matches!(method_key, "__construct" | "count" | "setflags"), - "RecursiveDirectoryIterator" => matches!( - method_key, - "__construct" | "haschildren" | "getchildren" - ), + "RecursiveDirectoryIterator" => { + matches!(method_key, "__construct" | "haschildren" | "getchildren") + } "RecursiveCachingIterator" => matches!( method_key, "__construct" | "haschildren" | "getchildren" | "__elephcassumerecursiveiterator" ), - "EmptyIterator" => matches!( - method_key, - "current" | "key" | "next" | "rewind" | "valid" - ), + "EmptyIterator" => matches!(method_key, "current" | "key" | "next" | "rewind" | "valid"), "ArrayIterator" => matches!( method_key, "__construct" @@ -1598,12 +2758,7 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { ), "IteratorIterator" => matches!( method_key, - "current" - | "key" - | "next" - | "rewind" - | "valid" - | "getinneriterator" + "current" | "key" | "next" | "rewind" | "valid" | "getinneriterator" ), "LimitIterator" => matches!( method_key, @@ -1745,7 +2900,11 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { } /// Returns true when this SPL method is implemented by an intrinsic runtime wrapper. -fn runtime_intrinsic_method_has_wrapper(class_name: &str, method_key: &str, is_static: bool) -> bool { +fn runtime_intrinsic_method_has_wrapper( + class_name: &str, + method_key: &str, + is_static: bool, +) -> bool { let intrinsic = if is_static { IntrinsicCall::static_method(class_name, method_key) } else { @@ -1794,7 +2953,7 @@ fn lower_builtin_spl_method( } /// Returns true when `module.class_methods` already contains a class-method body. -fn class_method_already_lowered( +pub(super) fn class_method_already_lowered( module: &Module, class_name: &str, method_key: &str, diff --git a/src/ir_lower/reflection.rs b/src/ir_lower/reflection.rs new file mode 100644 index 0000000000..40bdfb83e6 --- /dev/null +++ b/src/ir_lower/reflection.rs @@ -0,0 +1,349 @@ +//! Purpose: +//! Selects and lowers the synthetic builtin Reflection surface reachable from EIR. +//! +//! Called from: +//! - `crate::ir_lower::program::lower()` after user functions and literal eval AOT bodies. +//! +//! Key details: +//! - Native programs lower Reflection classes to a fixed point from EIR types and calls. +//! - Dynamic eval keeps the full Reflection surface because names are resolved at runtime. + +use std::collections::{BTreeSet, HashMap}; + +use crate::ir::{Function, Module, Op}; +use crate::parser::ast::ExprKind; +use crate::types::{CheckResult, FunctionSig, PhpType}; + +use super::function; +use super::program::{ + all_lowered_functions, class_data_name, class_method_already_lowered, + dynamic_object_new_metadata_names, include_lowered_runtime_features, php_method_key, + string_data_name, +}; + +/// Builtin Reflection classes whose concrete method bodies can be lowered into EIR. +const BUILTIN_REFLECTION_CLASS_NAMES: &[&str] = &[ + "ReflectionAttribute", + "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", + "ReflectionFunction", + "ReflectionMethod", + "ReflectionNamedType", + "ReflectionParameter", + "ReflectionProperty", + "ReflectionUnionType", + "ReflectionIntersectionType", +]; + +/// Lowers only synthetic Reflection classes reachable from native EIR, or the full +/// surface when the dynamic eval bridge can construct and invoke them by name. +pub(super) fn lower_referenced_builtin_methods( + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + loop { + let classes = referenced_builtin_reflection_classes(module); + let before = module.class_methods.len(); + for class_name in classes { + lower_builtin_reflection_property_init_thunk( + &class_name, + module, + check_result, + constants, + fiber_return_sigs, + ); + lower_builtin_reflection_class_methods( + &class_name, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + if module.class_methods.len() == before { + break; + } + include_lowered_runtime_features(module); + } +} + +/// Lowers a selected Reflection class's property-default thunk once it becomes reachable. +fn lower_builtin_reflection_property_init_thunk( + class_name: &str, + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + let Some(class_info) = check_result.classes.get(class_name) else { + return; + }; + let function_name = format!("_class_propinit_{}", class_info.class_id); + if module + .functions + .iter() + .any(|function| function.name == function_name) + { + return; + } + function::lower_property_init_thunk( + class_name, + class_info, + module, + check_result, + constants, + fiber_return_sigs, + ); +} + +/// Collects Reflection class owners reachable from lowered values and calls. +fn referenced_builtin_reflection_classes(module: &Module) -> BTreeSet { + let mut classes = BTreeSet::new(); + if module.required_runtime_features.eval_bridge { + for class_name in BUILTIN_REFLECTION_CLASS_NAMES { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + return classes; + } + + for function in all_lowered_functions(module) { + collect_builtin_reflection_class_from_type( + module, + &function.return_php_type, + &mut classes, + ); + for param in &function.params { + collect_builtin_reflection_class_from_type(module, ¶m.php_type, &mut classes); + } + for local in &function.locals { + collect_builtin_reflection_class_from_type(module, &local.php_type, &mut classes); + } + for value in &function.values { + collect_builtin_reflection_class_from_type(module, &value.php_type, &mut classes); + } + + for inst in &function.instructions { + match inst.op { + Op::ObjectNew => { + if let Some(class_name) = class_data_name(module, inst) { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + } + Op::DynamicObjectNew => { + if let Some((fallback_class, required_parent)) = + dynamic_object_new_metadata_names(module, inst) + { + insert_builtin_reflection_class(module, fallback_class, &mut classes); + insert_builtin_reflection_class(module, required_parent, &mut classes); + } + } + Op::StaticMethodCall => { + if let Some((class_name, _)) = + string_data_name(module, inst).and_then(|name| name.rsplit_once("::")) + { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + } + Op::MethodCall | Op::NullsafeMethodCall => { + collect_dynamic_reflection_method_candidates( + module, + function, + inst, + &mut classes, + ); + } + _ => {} + } + } + } + classes +} + +/// Adds Reflection implementations that a mixed/union method receiver may dispatch to. +fn collect_dynamic_reflection_method_candidates( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + classes: &mut BTreeSet, +) { + let Some(receiver) = inst.operands.first().copied() else { + return; + }; + let Some(receiver_type) = function.value(receiver).map(|value| value.php_type.codegen_repr()) + else { + return; + }; + if !matches!(receiver_type, PhpType::Mixed | PhpType::Union(_)) { + return; + } + let Some(method_name) = string_data_name(module, inst) else { + return; + }; + let method_key = php_method_key(method_name); + for (class_name, class_info) in &module.class_infos { + if class_info.methods.contains_key(&method_key) { + insert_builtin_reflection_class(module, class_name, classes); + } + } +} + +/// Adds Reflection class names nested in one EIR-visible PHP type. +fn collect_builtin_reflection_class_from_type( + module: &Module, + php_type: &PhpType, + classes: &mut BTreeSet, +) { + match php_type { + PhpType::Object(class_name) => { + insert_builtin_reflection_class(module, class_name, classes); + } + PhpType::Array(element) | PhpType::Buffer(element) => { + collect_builtin_reflection_class_from_type(module, element, classes); + } + PhpType::AssocArray { key, value } => { + collect_builtin_reflection_class_from_type(module, key, classes); + collect_builtin_reflection_class_from_type(module, value, classes); + } + PhpType::Union(members) => { + for member in members { + collect_builtin_reflection_class_from_type(module, member, classes); + } + } + PhpType::Pointer(Some(class_name)) => { + insert_builtin_reflection_class(module, class_name, classes); + } + PhpType::Int + | PhpType::Float + | PhpType::Str + | PhpType::Bool + | PhpType::False + | PhpType::Void + | PhpType::Never + | PhpType::Iterable + | PhpType::Mixed + | PhpType::Callable + | PhpType::Packed(_) + | PhpType::Pointer(None) + | PhpType::Resource(_) + | PhpType::TaggedScalar => {} + } +} + +/// Inserts one canonical Reflection class plus Reflection ancestors and method owners. +fn insert_builtin_reflection_class( + module: &Module, + class_name: &str, + classes: &mut BTreeSet, +) { + let Some(canonical) = canonical_builtin_reflection_class_name(class_name) else { + return; + }; + if !module.class_infos.contains_key(canonical) || !classes.insert(canonical.to_string()) { + return; + } + let Some(class_info) = module.class_infos.get(canonical) else { + return; + }; + let dependencies = class_info + .parent + .iter() + .chain(class_info.method_impl_classes.values()) + .chain(class_info.static_method_impl_classes.values()) + .cloned() + .collect::>(); + for dependency in dependencies { + insert_builtin_reflection_class(module, &dependency, classes); + } +} + +/// Resolves a class spelling to the canonical builtin Reflection name. +pub(super) fn canonical_builtin_reflection_class_name(class_name: &str) -> Option<&'static str> { + let key = crate::names::php_symbol_key(class_name.trim_start_matches('\\')); + BUILTIN_REFLECTION_CLASS_NAMES + .iter() + .copied() + .find(|candidate| crate::names::php_symbol_key(candidate) == key) +} + +/// Lowers all concrete synthetic methods for one builtin reflection class. +fn lower_builtin_reflection_class_methods( + class_name: &str, + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + let Some(class_info) = check_result.classes.get(class_name) else { + return; + }; + let before = module.class_methods.len(); + for method in &class_info.method_decls { + if !method.has_body { + continue; + } + let generated_body; + let method_key = crate::names::php_symbol_key(&method.name); + if class_method_already_lowered(module, class_name, &method_key, method.is_static) { + continue; + } + let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { + let function_attrs = function_attribute_sources(module); + generated_body = + crate::codegen::reflection::build_attribute_new_instance_body_with_extra( + &check_result.classes, + &function_attrs, + ); + generated_body.as_slice() + } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { + // Materialize captured attribute arguments through the normal array + // lowering (named arguments and associative arrays included) rather + // than a bespoke codegen path. + let function_attrs = function_attribute_sources(module); + generated_body = crate::codegen::reflection::build_attribute_get_arguments_body_with_extra( + &check_result.classes, + &function_attrs, + ); + generated_body.as_slice() + } else { + &method.body + }; + function::lower_class_method( + class_name, + &method.name, + method.is_static, + &method.params, + method.return_type.as_ref(), + body, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + for method in module.class_methods.iter_mut().skip(before) { + method.flags.is_synthetic = true; + } +} + +/// Returns reflection-visible top-level function attribute metadata sources. +fn function_attribute_sources( + module: &Module, +) -> Vec> { + module + .functions + .iter() + .filter(|function| !function.attribute_names.is_empty()) + .map(|function| { + ( + function.attribute_names.as_slice(), + function.attribute_args.as_slice(), + ) + }) + .collect() +} diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index a5cdb497b7..02383306ee 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -15,13 +15,17 @@ use crate::ir::{ BlockId, CmpPredicate, Immediate, IrType, LocalKind, LocalSlotId, Op, Ownership, SwitchCase, Terminator, }; -use crate::ir_lower::context::{FinallyFrame, LoopCleanup, LoopFrame, LoweredValue, LoweringContext}; +use crate::ir_lower::context::{ + FinallyFrame, LoopCleanup, LoopFrame, LoweredValue, LoweringContext, +}; use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, - static_callable_binding_for_expr, string_op_uses_scratch_storage, - type_satisfies_array_access_for_ir, + reflection_arg_array_binding_for_expr, reflection_class_binding_for_expr, + reflection_function_binding_for_expr, reflection_method_binding_for_expr, + reflection_property_binding_for_expr, static_callable_binding_for_expr, + string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, }; use crate::names::{php_symbol_key, property_hook_set_method}; use crate::parser::ast::{ @@ -45,7 +49,14 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { then_body, elseif_clauses, else_body, - } => lower_if(ctx, condition, then_body, elseif_clauses, else_body.as_deref(), stmt.span), + } => lower_if( + ctx, + condition, + then_body, + elseif_clauses, + else_body.as_deref(), + stmt.span, + ), StmtKind::IfDef { symbol, then_body, @@ -58,8 +69,18 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { condition, update, body, - } => lower_for(ctx, init.as_deref(), condition.as_ref(), update.as_deref(), body), - StmtKind::ArrayAssign { array, index, value } => { + } => lower_for( + ctx, + init.as_deref(), + condition.as_ref(), + update.as_deref(), + body, + ), + StmtKind::ArrayAssign { + array, + index, + value, + } => { lower_array_assign(ctx, array, index, value, stmt.span); } StmtKind::NestedArrayAssign { target, value } => { @@ -77,7 +98,14 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { value_var, value_by_ref, body, - } => lower_foreach(ctx, array, key_var.as_deref(), value_var, *value_by_ref, body), + } => lower_foreach( + ctx, + array, + key_var.as_deref(), + value_var, + *value_by_ref, + body, + ), StmtKind::Switch { subject, cases, @@ -201,6 +229,9 @@ fn lower_block(ctx: &mut LoweringContext<'_, '_>, body: &[Stmt]) { /// Emits EIR for `echo`. fn lower_echo(ctx: &mut LoweringContext<'_, '_>, expr: &Expr, span: Span) { let value = lower_expr(ctx, expr); + if ctx.builder.insertion_block_is_terminated() { + return; + } ctx.emit_void( Op::EchoValue, vec![value.value], @@ -234,6 +265,11 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa let direct_closure = matches!(value.kind, ExprKind::Closure { .. }) || bound_closure; ctx.clear_pending_static_callable_result(); let static_callable = static_callable_binding_for_expr(ctx, value); + let reflected_class = reflection_class_binding_for_expr(ctx, value); + let reflected_function = reflection_function_binding_for_expr(ctx, value); + let reflected_property = reflection_property_binding_for_expr(ctx, value); + let reflected_method = reflection_method_binding_for_expr(ctx, value); + let reflected_args = reflection_arg_array_binding_for_expr(value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -263,6 +299,21 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa ctx.bind_static_callable_local(name, target); } } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } + if let Some(reflected_function) = reflected_function { + ctx.bind_reflection_function_local(name, reflected_function); + } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } + if let Some((reflected_class, reflected_method)) = reflected_method { + ctx.bind_reflection_method_local(name, reflected_class, reflected_method); + } + if let Some(reflected_args) = reflected_args { + ctx.bind_reflection_arg_array_local(name, reflected_args); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -354,8 +405,15 @@ fn lower_if( span: Span, ) { let merge = ctx.builder.create_named_block("if.merge", Vec::new()); - let merge_reachable = - lower_if_chain(ctx, condition, then_body, elseif_clauses, else_body, merge, span); + let merge_reachable = lower_if_chain( + ctx, + condition, + then_body, + elseif_clauses, + else_body, + merge, + span, + ); ctx.builder.position_at_end(merge); if !merge_reachable { ctx.builder.terminate(Terminator::Unreachable); @@ -400,25 +458,26 @@ fn lower_if_chain( ctx.clear_static_callable_locals(); ctx.builder.position_at_end(else_block); ctx.restore_initialized_slots(split_initialized.clone()); - let else_reachable = if let Some(((next_condition, next_body), rest)) = elseif_clauses.split_first() { - lower_if_chain(ctx, next_condition, next_body, rest, else_body, merge, span) - } else if let Some(else_body) = else_body { - lower_block(ctx, else_body); - if !ctx.builder.insertion_block_is_terminated() { - branch_to(ctx, merge); - true - } else { - false - } - } else { - lower_noop(ctx, span); - if !ctx.builder.insertion_block_is_terminated() { - branch_to(ctx, merge); - true + let else_reachable = + if let Some(((next_condition, next_body), rest)) = elseif_clauses.split_first() { + lower_if_chain(ctx, next_condition, next_body, rest, else_body, merge, span) + } else if let Some(else_body) = else_body { + lower_block(ctx, else_body); + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, merge); + true + } else { + false + } } else { - false - } - }; + lower_noop(ctx, span); + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, merge); + true + } else { + false + } + }; merge_reachable |= else_reachable; let else_initialized = ctx.initialized_slots_snapshot(); ctx.restore_initialized_slots(merge_initialized_slots( @@ -593,7 +652,11 @@ fn lower_for( /// be freed (a per-write heap leak that exhausts the heap under `--web`). Non-string /// refcounted values (objects, arrays) are moved, or retained only when borrowed, /// by the write itself, so they must not be released here. -fn release_persisted_string_operand(ctx: &mut LoweringContext<'_, '_>, value: LoweredValue, span: Span) { +fn release_persisted_string_operand( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + span: Span, +) { let ty = ctx.builder.value_php_type(value.value); // Only release a FRESH owning string temporary (a call/concat result, etc.). // A borrowed load of a variable that still owns the string (e.g. the prelude's @@ -694,11 +757,24 @@ fn lower_array_assign( Some(span), ); let elem_ty = indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); - finish_indexed_array_local_write(ctx, array, array_value, updated_ty, needs_storeback, span); + finish_indexed_array_local_write( + ctx, + array, + array_value, + updated_ty, + needs_storeback, + span, + ); release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value_value, span); return; } - ctx.emit_void(op, vec![array_value.value, index_value.value, value_value.value], None, op.default_effects(), Some(span)); + ctx.emit_void( + op, + vec![array_value.value, index_value.value, value_value.value], + None, + op.default_effects(), + Some(span), + ); release_persisted_string_operand(ctx, index_value, span); release_persisted_string_operand(ctx, value_value, span); } @@ -788,9 +864,7 @@ fn lower_mixed_key_array_set( fn promoted_assoc_array_type(current_ty: PhpType, value_ty: PhpType) -> PhpType { let value_ty = normalize_array_write_element_type(value_ty.codegen_repr()); let assoc_value_ty = match current_ty.codegen_repr() { - PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => { - value_ty - } + PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => value_ty, PhpType::Array(elem_ty) => { let elem_ty = normalize_array_write_element_type(elem_ty.codegen_repr()); if elem_ty == value_ty { @@ -808,7 +882,12 @@ fn promoted_assoc_array_type(current_ty: PhpType, value_ty: PhpType) -> PhpType } /// Lowers a nested array assignment that already carries an expression target. -fn lower_nested_array_assign(ctx: &mut LoweringContext<'_, '_>, target: &Expr, value: &Expr, span: Span) { +fn lower_nested_array_assign( + ctx: &mut LoweringContext<'_, '_>, + target: &Expr, + value: &Expr, + span: Span, +) { let target = lower_expr(ctx, target); let value = lower_expr(ctx, value); ctx.emit_void( @@ -832,18 +911,38 @@ fn lower_array_push(ctx: &mut LoweringContext<'_, '_>, array: &str, value: &Expr Op::RuntimeCall }; if op == Op::ArrayPush { - let (array_value, updated_ty, needs_storeback) = if ref_bound_mixed_indexed_array_write(ctx, array, value) { - (array_value, Some(ctx.local_type(array)), true) - } else { - prepare_indexed_array_local_write(ctx, array_value, value, span) - }; - ctx.emit_void(op, vec![array_value.value, value.value], None, op.default_effects(), Some(span)); + let (array_value, updated_ty, needs_storeback) = + if ref_bound_mixed_indexed_array_write(ctx, array, value) { + (array_value, Some(ctx.local_type(array)), true) + } else { + prepare_indexed_array_local_write(ctx, array_value, value, span) + }; + ctx.emit_void( + op, + vec![array_value.value, value.value], + None, + op.default_effects(), + Some(span), + ); let elem_ty = indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); - finish_indexed_array_local_write(ctx, array, array_value, updated_ty, needs_storeback, span); + finish_indexed_array_local_write( + ctx, + array, + array_value, + updated_ty, + needs_storeback, + span, + ); release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, span); return; } - ctx.emit_void(op, vec![array_value.value, value.value], None, op.default_effects(), Some(span)); + ctx.emit_void( + op, + vec![array_value.value, value.value], + None, + op.default_effects(), + Some(span), + ); release_persisted_string_operand(ctx, value, span); } @@ -969,9 +1068,9 @@ pub(super) fn ref_bound_mixed_indexed_array_write( /// Returns the refined array type after writing a value into an indexed array. fn indexed_array_write_updated_type(current_ty: PhpType, value_ty: PhpType) -> Option { match current_ty.codegen_repr() { - PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => { - Some(PhpType::Array(Box::new(normalize_empty_array_write_element_type(value_ty)))) - } + PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => Some( + PhpType::Array(Box::new(normalize_empty_array_write_element_type(value_ty))), + ), PhpType::Array(elem_ty) if elem_ty.codegen_repr() == PhpType::Mixed => None, PhpType::Array(elem_ty) => { let elem_ty = elem_ty.codegen_repr(); @@ -997,8 +1096,7 @@ fn indexed_array_write_needs_mixed_conversion(current_ty: &PhpType, updated_ty: let PhpType::Array(updated_elem) = updated_ty.codegen_repr() else { return false; }; - updated_elem.codegen_repr() == PhpType::Mixed - && current_elem.codegen_repr() != PhpType::Mixed + updated_elem.codegen_repr() == PhpType::Mixed && current_elem.codegen_repr() != PhpType::Mixed } /// Returns true for the placeholder element type used by empty indexed arrays. @@ -1023,6 +1121,8 @@ fn lower_typed_assign( ctx.clear_pending_static_callable_result(); let php_type = ctx.type_expr_to_php_type_for_value(type_expr); let static_callable = static_callable_binding_for_expr(ctx, value); + let reflected_class = reflection_class_binding_for_expr(ctx, value); + let reflected_property = reflection_property_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -1045,6 +1145,12 @@ fn lower_typed_assign( if let Some(target) = static_callable { ctx.bind_static_callable_local(name, target); } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -1126,7 +1232,12 @@ fn lower_foreach( } else { let value_ty = foreach_value_type(&source_ty); if value_ty == PhpType::Mixed { - initialize_foreach_mixed_local_if_needed(ctx, value_var, value_needs_null_init, array.span); + initialize_foreach_mixed_local_if_needed( + ctx, + value_var, + value_needs_null_init, + array.span, + ); } else if value_needs_null_init { ctx.declare_local(value_var, value_ty.clone()); ctx.set_local_type(value_var, value_ty); @@ -1158,7 +1269,10 @@ fn lower_foreach( ctx.builder.position_at_end(body_block); let cleanup = ctx .value_is_owning_temporary(source) - .then_some(LoopCleanup { value: source, span: array.span }); + .then_some(LoopCleanup { + value: source, + span: array.span, + }); ctx.loop_stack.push(LoopFrame { break_block: exit, continue_block: header, @@ -1217,7 +1331,11 @@ fn lower_foreach( /// Returns the by-value foreach local type when Phase 04 can keep a concrete element. fn foreach_value_type(source_ty: &PhpType) -> PhpType { match source_ty.codegen_repr() { - PhpType::Array(elem) if elem.codegen_repr() == PhpType::Callable => PhpType::Callable, + PhpType::Array(elem) => match elem.codegen_repr() { + PhpType::Callable => PhpType::Callable, + PhpType::Object(class_name) => PhpType::Object(class_name), + _ => PhpType::Mixed, + }, PhpType::Object(class_name) if class_name == "Phar" || class_name == "PharData" => { PhpType::Object("PharFileInfo".to_string()) } @@ -1258,7 +1376,7 @@ fn initialize_foreach_mixed_local_if_needed( Op::MixedBox.default_effects(), Some(span), ); - ctx.store_local(name, boxed, PhpType::Mixed, Some(span)); + ctx.store_foreach_initializer_local_only(name, boxed, PhpType::Mixed, Some(span)); } /// Lowers a `switch` with source-ordered pattern evaluation and PHP fallthrough. @@ -1312,7 +1430,11 @@ fn lower_static_switch_dispatch( let Some(value) = int_case_value(case_expr) else { continue; }; - switch_cases.push(SwitchCase { value, target: *case_block, args: Vec::new() }); + switch_cases.push(SwitchCase { + value, + target: *case_block, + args: Vec::new(), + }); } } ctx.builder.terminate(Terminator::Switch { @@ -1375,7 +1497,12 @@ fn lower_dynamic_switch_dispatch( let case_value = coerce_to_int(ctx, case_value, Some(case_expr.span)); ctx.emit_value( Op::ICmp, - vec![int_subject.expect("non-string subject is pre-coerced").value, case_value.value], + vec![ + int_subject + .expect("non-string subject is pre-coerced") + .value, + case_value.value, + ], Some(Immediate::CmpPredicate(CmpPredicate::Eq)), PhpType::Bool, Op::ICmp.default_effects(), @@ -1419,29 +1546,40 @@ fn lower_switch_bodies( default_block: BlockId, exit: BlockId, ) { + let default_index = default + .and_then(|default| switch_default_source_index(cases, default)) + .unwrap_or(cases.len()); ctx.clear_static_callable_locals(); ctx.loop_stack.push(LoopFrame { break_block: exit, continue_block: exit, cleanup: None, }); - for (index, ((_, body), block)) in cases.iter().zip(blocks).enumerate() { - ctx.builder.position_at_end(*block); - lower_block(ctx, body); - if !ctx.builder.insertion_block_is_terminated() { - if let Some(next_block) = blocks.get(index + 1) { - branch_to(ctx, *next_block); - } else { - branch_to(ctx, default_block); + for index in 0..=cases.len() { + if default.is_some() && default_index == index { + ctx.builder.position_at_end(default_block); + if let Some(default) = default { + lower_block(ctx, default); + } + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, blocks.get(index).copied().unwrap_or(exit)); } + ctx.clear_static_callable_locals(); + } + if let Some((_, body)) = cases.get(index) { + ctx.builder.position_at_end(blocks[index]); + lower_block(ctx, body); + if !ctx.builder.insertion_block_is_terminated() { + branch_to( + ctx, + switch_next_body_block(index + 1, blocks, default_index, default_block, exit), + ); + } + ctx.clear_static_callable_locals(); } - ctx.clear_static_callable_locals(); - } - ctx.builder.position_at_end(default_block); - if let Some(default) = default { - lower_block(ctx, default); } - if !ctx.builder.insertion_block_is_terminated() { + if default.is_none() { + ctx.builder.position_at_end(default_block); branch_to(ctx, exit); } ctx.loop_stack.pop(); @@ -1449,8 +1587,59 @@ fn lower_switch_bodies( ctx.clear_static_callable_locals(); } +/// Returns the source-order insertion point for a non-empty switch default body. +fn switch_default_source_index( + cases: &[(Vec, Vec)], + default: &[Stmt], +) -> Option { + if cases.is_empty() { + return Some(0); + } + let default_start = default.first()?.span; + if default_start == Span::dummy() { + return None; + } + let mut default_index = 0; + for (conditions, _) in cases { + let case_start = conditions.first()?.span; + if case_start == Span::dummy() { + return None; + } + if span_is_before(case_start, default_start) { + default_index += 1; + } + } + Some(default_index) +} + +/// Returns the block that follows one source-ordered switch body. +fn switch_next_body_block( + next_index: usize, + blocks: &[BlockId], + default_index: usize, + default_block: BlockId, + exit: BlockId, +) -> BlockId { + if default_index == next_index { + default_block + } else { + blocks.get(next_index).copied().unwrap_or(exit) + } +} + +/// Returns true when `span` appears before `pivot` in the same source file. +fn span_is_before(span: Span, pivot: Span) -> bool { + span.line < pivot.line || (span.line == pivot.line && span.col < pivot.col) +} + /// Lowers include/require statements through a high-level runtime call. -fn lower_include(ctx: &mut LoweringContext<'_, '_>, path: &Expr, once: bool, required: bool, span: Span) { +fn lower_include( + ctx: &mut LoweringContext<'_, '_>, + path: &Expr, + once: bool, + required: bool, + span: Span, +) { let path = lower_expr(ctx, path); let label = format!("include once={} required={}", once, required); let data = ctx.intern_string(&label); @@ -1477,7 +1666,12 @@ fn lower_include_once_mark(ctx: &mut LoweringContext<'_, '_>, label: &str, span: } /// Lowers an include-once guarded body. -fn lower_include_once_guard(ctx: &mut LoweringContext<'_, '_>, label: &str, body: &[Stmt], span: Span) { +fn lower_include_once_guard( + ctx: &mut LoweringContext<'_, '_>, + label: &str, + body: &[Stmt], + span: Span, +) { let data = ctx.intern_string(label); let should_run = ctx .builder @@ -1492,8 +1686,12 @@ fn lower_include_once_guard(ctx: &mut LoweringContext<'_, '_>, label: &str, body Some(span), ) .expect("include_once_guard produces a branch condition"); - let body_block = ctx.builder.create_named_block("include_once_body", Vec::new()); - let after_block = ctx.builder.create_named_block("include_once_after", Vec::new()); + let body_block = ctx + .builder + .create_named_block("include_once_body", Vec::new()); + let after_block = ctx + .builder + .create_named_block("include_once_after", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: should_run, then_target: body_block, @@ -1538,11 +1736,12 @@ fn lower_try_catch( catches: &[CatchClause], span: Span, ) { - let handler_block = ctx.builder.create_named_block("try.catch_dispatch", Vec::new()); + let handler_block = ctx + .builder + .create_named_block("try.catch_dispatch", Vec::new()); let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; - ctx.clear_static_callable_locals(); ctx.emit_void( Op::TryPushHandler, Vec::new(), @@ -1557,6 +1756,7 @@ fn lower_try_catch( } ctx.builder.position_at_end(handler_block); + ctx.clear_static_callable_locals(); emit_try_pop_handler(ctx, handler_token, span); lower_catch_dispatch(ctx, catches, after_block, span); ctx.builder.position_at_end(after_block); @@ -1600,11 +1800,12 @@ fn lower_try_catch_finally( finally_body: &[Stmt], span: Span, ) { - let handler_block = ctx.builder.create_named_block("try.catch_dispatch", Vec::new()); + let handler_block = ctx + .builder + .create_named_block("try.catch_dispatch", Vec::new()); let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; - ctx.clear_static_callable_locals(); ctx.emit_void( Op::TryPushHandler, Vec::new(), @@ -1622,6 +1823,7 @@ fn lower_try_catch_finally( } ctx.builder.position_at_end(handler_block); + ctx.clear_static_callable_locals(); emit_try_pop_handler(ctx, handler_token, span); lower_catch_dispatch_with_finally(ctx, catches, after_block, finally_body, span); ctx.builder.position_at_end(after_block); @@ -1661,7 +1863,9 @@ fn lower_catch_dispatch( } let current = lower_current_exception(ctx, span); - ctx.builder.terminate(Terminator::Throw { value: current.value }); + ctx.builder.terminate(Terminator::Throw { + value: current.value, + }); } /// Lowers catch dispatch for `try`/`catch`/`finally`. @@ -1692,7 +1896,9 @@ fn lower_catch_dispatch_with_finally( let current = lower_current_exception(ctx, span); lower_block(ctx, finally_body); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder.terminate(Terminator::Throw { value: current.value }); + ctx.builder.terminate(Terminator::Throw { + value: current.value, + }); } } @@ -1713,7 +1919,8 @@ fn lower_catch_match( let mismatch = if idx + 1 == catch.exception_types.len() { next_catch } else { - ctx.builder.create_named_block("try.catch_type_next", Vec::new()) + ctx.builder + .create_named_block("try.catch_type_next", Vec::new()) }; let current = lower_current_exception(ctx, span); let data = ctx.intern_class_name(catch_type.as_str()); @@ -1752,12 +1959,15 @@ fn lower_current_exception(ctx: &mut LoweringContext<'_, '_>, span: Span) -> Low /// Binds and clears the active exception for a matched catch clause. fn lower_catch_bind(ctx: &mut LoweringContext<'_, '_>, catch: &CatchClause, span: Span) { - let (immediate, php_type) = catch.variable.as_ref().map_or((None, PhpType::Void), |variable| { - let php_type = catch_variable_type(catch); - let slot = ctx.declare_local(variable, php_type.clone()); - ctx.set_local_type(variable, php_type.clone()); - (Some(Immediate::LocalSlot(slot)), php_type) - }); + let (immediate, php_type) = catch + .variable + .as_ref() + .map_or((None, PhpType::Void), |variable| { + let php_type = catch_variable_type(catch); + let slot = ctx.declare_local(variable, php_type.clone()); + ctx.set_local_type(variable, php_type.clone()); + (Some(Immediate::LocalSlot(slot)), php_type) + }); ctx.builder.emit_with_effects( Op::CatchBind, Vec::new(), @@ -1773,7 +1983,11 @@ fn lower_catch_bind(ctx: &mut LoweringContext<'_, '_>, catch: &CatchClause, span /// Returns the local type to use for a catch variable. fn catch_variable_type(catch: &CatchClause) -> PhpType { if catch.exception_types.len() == 1 { - return PhpType::Object(catch.exception_types[0].trim_start_matches('\\').to_string()); + return PhpType::Object( + catch.exception_types[0] + .trim_start_matches('\\') + .to_string(), + ); } PhpType::Object("Throwable".to_string()) } @@ -1793,7 +2007,11 @@ fn lower_continue(ctx: &mut LoweringContext<'_, '_>, level: usize) { ctx.builder.terminate(Terminator::Unreachable); return; }; - terminate_branch(ctx, frame.continue_block, loop_cleanup_count_for_branch(level)); + terminate_branch( + ctx, + frame.continue_block, + loop_cleanup_count_for_branch(level), + ); } /// Lowers a return statement using the current function return contract. @@ -1908,13 +2126,7 @@ fn acquire_borrowed_return_value( } if !matches!( ctx.builder.value_defining_op(value.value), - Some( - Op::ArrayGet - | Op::HashGet - | Op::PropGet - | Op::DynamicPropGet - | Op::NullsafePropGet - ) + Some(Op::ArrayGet | Op::HashGet | Op::PropGet | Op::DynamicPropGet | Op::NullsafePropGet) ) { return value; } @@ -1930,6 +2142,7 @@ fn terminate_return(ctx: &mut LoweringContext<'_, '_>, value: Option, target: BlockId, loop_cle return; } emit_innermost_loop_cleanups(ctx, loop_cleanup_count); - ctx.builder.terminate(Terminator::Br { target, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target, + args: Vec::new(), + }); } /// Terminates with a throw after running finally bodies that apply to uncaught throws. @@ -2123,7 +2339,11 @@ fn lower_list_unpack(ctx: &mut LoweringContext<'_, '_>, vars: &[String], value: } /// Emits the positional integer key used to read one list-unpack element. -fn lower_list_unpack_index(ctx: &mut LoweringContext<'_, '_>, index: usize, span: Span) -> LoweredValue { +fn lower_list_unpack_index( + ctx: &mut LoweringContext<'_, '_>, + index: usize, + span: Span, +) -> LoweredValue { ctx.emit_value( Op::ConstI64, Vec::new(), @@ -2192,7 +2412,11 @@ fn lower_global(ctx: &mut LoweringContext<'_, '_>, vars: &[String]) { /// Lowers a static local variable initialization. fn lower_static_var(ctx: &mut LoweringContext<'_, '_>, name: &str, init: &Expr, span: Span) { let value = lower_expr(ctx, init); - let slot = ctx.declare_local_with_kind(name, ctx.builder.value_php_type(value.value), LocalKind::StaticLocal); + let slot = ctx.declare_local_with_kind( + name, + ctx.builder.value_php_type(value.value), + LocalKind::StaticLocal, + ); ctx.builder.emit_with_effects( Op::InitStaticLocal, vec![value.value], @@ -2282,12 +2506,14 @@ fn magic_set_receiver_has_method( let Some(class_info) = ctx.classes.get(normalized) else { return false; }; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return false; } - class_info - .methods - .contains_key(&php_symbol_key("__set")) + class_info.methods.contains_key(&php_symbol_key("__set")) } /// Lowers an undeclared property write to a normal `__set` instance-method call. @@ -2437,6 +2663,17 @@ fn lower_static_property_array_push( let property_value = load_static_property(ctx, receiver, property, span); let value = lower_expr(ctx, value); + if static_property_may_be_eval_dynamic(ctx, receiver) { + ctx.emit_void( + Op::MixedArrayAppend, + vec![property_value.value, value.value], + None, + Op::MixedArrayAppend.default_effects(), + Some(span), + ); + store_static_property(ctx, receiver, property, property_value.value, span); + return; + } ctx.emit_void( Op::RuntimeCall, vec![property_value.value, value.value], @@ -2474,9 +2711,8 @@ fn lower_static_property_array_assign( return; } - let property_value = if let Some(property_ty) = - static_property_type(ctx, receiver, property) - .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) + let property_value = if let Some(property_ty) = static_property_type(ctx, receiver, property) + .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) { load_static_property_as(ctx, receiver, property, property_ty, span) } else { @@ -2484,6 +2720,17 @@ fn lower_static_property_array_assign( }; let index = lower_expr(ctx, index); let value = lower_expr(ctx, value); + if static_property_may_be_eval_dynamic(ctx, receiver) { + ctx.emit_void( + Op::RuntimeCall, + vec![property_value.value, index.value, value.value], + None, + effects_lookup::runtime_effects(), + Some(span), + ); + store_static_property(ctx, receiver, property, property_value.value, span); + return; + } ctx.emit_void( Op::RuntimeCall, vec![property_value.value, index.value, value.value], @@ -2493,6 +2740,20 @@ fn lower_static_property_array_assign( ); } +/// Returns true when a named static-property receiver may resolve through eval metadata. +fn static_property_may_be_eval_dynamic( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, +) -> bool { + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + ctx.has_eval_barrier() + && !ctx + .classes + .contains_key(class_name.as_str().trim_start_matches('\\')) +} + /// Lowers `$object->prop[] = value`. fn lower_property_array_push( ctx: &mut LoweringContext<'_, '_>, @@ -2532,7 +2793,12 @@ fn lower_property_array_push( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } @@ -2589,7 +2855,12 @@ fn lower_property_array_assign( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } if let Some(property_ty) = @@ -2623,13 +2894,17 @@ fn lower_property_array_assign( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } - if let Some(property_ty) = - object_property_type(ctx, object.value, property) - .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) + if let Some(property_ty) = object_property_type(ctx, object.value, property) + .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) { let data = ctx.intern_string(property); let property_value = ctx.emit_value( @@ -2674,7 +2949,8 @@ fn release_property_assignment_source_after_retaining_store( if !ctx.value_is_owning_temporary(value) { return; } - if !property_store_keeps_independent_ref(property_ty, &ctx.builder.value_php_type(value.value)) { + if !property_store_keeps_independent_ref(property_ty, &ctx.builder.value_php_type(value.value)) + { return; } crate::ir_lower::ownership::release_if_owned(ctx, value, Some(span)); @@ -2739,7 +3015,13 @@ fn indexed_property_array_element_type(property_ty: &PhpType) -> Option /// Emits a no-op marker for declaration-only or frontend-only statements. fn lower_noop(ctx: &mut LoweringContext<'_, '_>, span: Span) { - ctx.emit_void(Op::Nop, Vec::new(), None, Op::Nop.default_effects(), Some(span)); + ctx.emit_void( + Op::Nop, + Vec::new(), + None, + Op::Nop.default_effects(), + Some(span), + ); } /// Records a function variant group in high-level EIR metadata form. @@ -2781,7 +3063,10 @@ fn lower_function_variant_mark( /// Emits a branch to `target` if the current block can still fall through. fn branch_to(ctx: &mut LoweringContext<'_, '_>, target: BlockId) { if !ctx.builder.insertion_block_is_terminated() { - ctx.builder.terminate(Terminator::Br { target, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target, + args: Vec::new(), + }); } } @@ -2856,7 +3141,10 @@ fn emit_const_bool( span, ) .expect("const_bool produces a value"); - LoweredValue { value, ir_type: IrType::I64 } + LoweredValue { + value, + ir_type: IrType::I64, + } } /// Emits a null sentinel value. @@ -2874,7 +3162,10 @@ fn emit_null_value(ctx: &mut LoweringContext<'_, '_>, span: Option) -> Low span, ) .expect("const_null produces a value"); - LoweredValue { value, ir_type: IrType::I64 } + LoweredValue { + value, + ir_type: IrType::I64, + } } /// Coerces a value to the current function return storage type when needed. @@ -2941,7 +3232,10 @@ fn coerce_to_tagged_scalar( if value.ir_type == IrType::TaggedScalar { return value; } - if matches!(ctx.builder.value_php_type(value.value).codegen_repr(), PhpType::Void) { + if matches!( + ctx.builder.value_php_type(value.value).codegen_repr(), + PhpType::Void + ) { return ctx.emit_value( Op::ConstNull, Vec::new(), @@ -2977,8 +3271,14 @@ fn coerce_container_to_return_type( Op::ArrayToMixed } ( - PhpType::AssocArray { value: source_value, .. }, - PhpType::AssocArray { value: return_value, .. }, + PhpType::AssocArray { + value: source_value, + .. + }, + PhpType::AssocArray { + value: return_value, + .. + }, ) if source_value.codegen_repr() != PhpType::Mixed && return_value.codegen_repr() == PhpType::Mixed => { @@ -3185,7 +3485,9 @@ fn static_receiver_class_name( StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), StaticReceiver::Parent => { let current = ctx.current_class.as_deref()?; - ctx.classes.get(current).and_then(|class_info| class_info.parent.clone()) + ctx.classes + .get(current) + .and_then(|class_info| class_info.parent.clone()) } } } @@ -3202,10 +3504,8 @@ fn object_property_type( }; ctx.classes .get(class_name.trim_start_matches('\\'))? - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr())) + .visible_property(property) + .map(|(_, (_, property_ty))| normalize_value_php_type(property_ty.codegen_repr())) } /// Returns true when a property type uses concrete indexed-array storage. diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 1a61484fa2..1ed84995a3 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -73,6 +73,8 @@ fn dummy_check_result() -> CheckResult { "f".to_string(), FunctionSig { params: vec![("x".to_string(), PhpType::Int)], + param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -120,6 +122,8 @@ fn dummy_check_result() -> CheckResult { CheckResult { global_env: HashMap::new(), functions, + function_attribute_names: HashMap::new(), + function_attribute_args: HashMap::new(), callable_param_sigs: HashMap::new(), callable_return_sigs: HashMap::new(), callable_array_return_sigs: HashMap::new(), @@ -140,6 +144,8 @@ fn dummy_check_result() -> CheckResult { fn class_info(_class_name: &str) -> ClassInfo { let method_sig = FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Int, declared_return: true, @@ -155,19 +161,25 @@ fn class_info(_class_name: &str) -> ClassInfo { static_methods.insert("sm".to_string(), method_sig); ClassInfo { class_id: 1, + declaration_span: crate::span::Span::dummy(), parent: None, is_abstract: false, is_final: false, is_readonly_class: false, allow_dynamic_properties: true, constants: HashMap::new(), + constant_visibilities: Default::default(), + final_constants: Default::default(), attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), + constant_attribute_names: HashMap::new(), + constant_attribute_args: HashMap::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), property_declaring_classes: HashMap::new(), @@ -175,10 +187,13 @@ fn class_info(_class_name: &str) -> ClassInfo { property_visibilities: HashMap::new(), property_set_visibilities: HashMap::new(), declared_properties: Default::default(), + property_declared_slots: Vec::new(), final_properties: Default::default(), readonly_properties: Default::default(), reference_properties: Default::default(), owned_reference_properties: Default::default(), + promoted_properties: Default::default(), + property_reference_slots: Vec::new(), abstract_properties: Default::default(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), @@ -280,6 +295,7 @@ fn lowers_every_expr_variant_smoke() { expr(ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -356,6 +372,7 @@ fn lowers_every_stmt_variant_smoke() { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(int(0)), span: sp(), attributes: Vec::new(), @@ -426,7 +443,9 @@ fn lowers_every_stmt_variant_smoke() { stmt(StmtKind::FunctionDecl { name: "f".to_string(), params: vec![("x".to_string(), Some(TypeExpr::Int), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -451,7 +470,7 @@ fn lowers_every_stmt_variant_smoke() { methods: vec![method.clone(), static_method.clone()], constants: vec![class_const.clone()], }), - stmt(StmtKind::EnumDecl { name: "E".to_string(), backing_type: Some(TypeExpr::Int), cases: vec![EnumCaseDecl { name: "A".to_string(), value: Some(int(1)), span: sp(), attributes: Vec::new() }], implements: Vec::new(), methods: Vec::new(), constants: Vec::new() }), + stmt(StmtKind::EnumDecl { name: "E".to_string(), backing_type: Some(TypeExpr::Int), cases: vec![EnumCaseDecl { name: "A".to_string(), value: Some(int(1)), span: sp(), attributes: Vec::new() }], implements: Vec::new(), trait_uses: Vec::new(), methods: Vec::new(), constants: Vec::new() }), stmt(StmtKind::PackedClassDecl { name: "P".to_string(), fields: vec![PackedField { name: "x".to_string(), type_expr: TypeExpr::Int, span: sp() }] }), stmt(StmtKind::InterfaceDecl { name: "I".to_string(), extends: Vec::new(), properties: Vec::new(), methods: Vec::new(), constants: Vec::new() }), stmt(StmtKind::TraitDecl { name: "T".to_string(), trait_uses: Vec::new(), properties: Vec::new(), methods: vec![method], constants: vec![class_const] }), @@ -482,7 +501,9 @@ fn class_method(name: &str, is_static: bool) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, diff --git a/src/ir_lower/tests/mod.rs b/src/ir_lower/tests/mod.rs index bc4202ef04..77551ba2c8 100644 --- a/src/ir_lower/tests/mod.rs +++ b/src/ir_lower/tests/mod.rs @@ -105,6 +105,38 @@ echo $counter->value(); assert!(text.contains("flags(method)"), "missing method flag: {text}"); } +/// Verifies a native program without Reflection references does not lower the synthetic surface. +#[test] +fn plain_program_omits_unreferenced_builtin_reflection_methods() { + let module = lower_source("getName(); +"#, + ); + let method_names = module + .class_methods + .iter() + .map(|function| function.name.as_str()) + .collect::>(); + assert!(method_names.contains("ReflectionClass::__construct")); + assert!(method_names.contains("ReflectionClass::getName")); +} + /// Verifies mixed float/integer comparisons coerce both operands before `fcmp`. #[test] fn float_comparison_coerces_integer_operand() { diff --git a/src/ir_passes/const_fold.rs b/src/ir_passes/const_fold.rs index e7fba8b56a..e1930a45ff 100644 --- a/src/ir_passes/const_fold.rs +++ b/src/ir_passes/const_fold.rs @@ -127,6 +127,31 @@ impl IrPass for ConstFold { return false; } + // Values returned by a heap-typed function must keep their boxed + // representation: narrowing a directly-returned Heap(Mixed) result to + // a raw scalar constant would break the return ABI (e.g. the fixed + // Heap(Mixed) contract of internal eval AOT functions). + if matches!(function.return_type, IrType::Heap(_)) { + let returned: std::collections::HashSet = function + .blocks + .iter() + .filter_map(|block| match &block.terminator { + Some(crate::ir::Terminator::Return { value }) => *value, + _ => None, + }) + .collect(); + folds.retain(|(inst_id, _, narrowing)| { + *narrowing == TypeNarrowing::None + || function + .instruction(*inst_id) + .and_then(|inst| inst.result) + .is_none_or(|result| !returned.contains(&result)) + }); + if folds.is_empty() { + return false; + } + } + // Phase 2 (mutate): rewrite each folded instruction in place to a constant. // When the fold narrows the result type (e.g. ICheckedAdd → ConstI64), // also update the instruction's result_type/result_php_type/ownership and diff --git a/src/ir_passes/dead_store.rs b/src/ir_passes/dead_store.rs index 99da80cb2d..cc7150d61b 100644 --- a/src/ir_passes/dead_store.rs +++ b/src/ir_passes/dead_store.rs @@ -42,8 +42,8 @@ impl IrPass for DeadStore { /// Neutralizes dead scalar `store_local` instructions and reports whether any /// store changed. The literal pool is unused because the pass never /// materializes new constants. - fn run(&self, function: &mut Function, _data: &mut DataPool) -> bool { - let eligible = eligible_slots(function); + fn run(&self, function: &mut Function, data: &mut DataPool) -> bool { + let eligible = eligible_slots(function, data); if eligible.is_empty() { return false; } @@ -70,7 +70,7 @@ impl IrPass for DeadStore { /// `unset_local`, static-local or global access, list unpack, …) makes the slot /// ineligible because it could read or alias the slot in a way this pass does not /// model. -fn eligible_slots(function: &Function) -> HashSet { +fn eligible_slots(function: &Function, data: &DataPool) -> HashSet { let mut eligible: HashSet = function .locals .iter() @@ -95,9 +95,50 @@ fn eligible_slots(function: &Function) -> HashSet { } exclude_address_escaping_slots(function, &mut eligible); + exclude_eval_literal_read_slots(function, data, &mut eligible); eligible } +/// Drops slots read implicitly by literal `eval` calls from dead-store eligibility. +/// +/// Direct eval AOT read-param lowering materializes those slot loads in codegen +/// rather than as explicit EIR `LoadLocal` instructions, so the liveness scan +/// cannot see them. Excluding the known read names keeps stores that feed the +/// eval fragment alive while leaving ordinary scalar locals eligible. +fn exclude_eval_literal_read_slots( + function: &Function, + data: &DataPool, + eligible: &mut HashSet, +) { + let slots_by_name: HashMap = function + .locals + .iter() + .filter(|local| eligible.contains(&local.id)) + .filter_map(|local| local.name.as_ref().map(|name| (name.clone(), local.id))) + .collect(); + if slots_by_name.is_empty() { + return; + } + + for inst in &function.instructions { + if inst.op != Op::EvalLiteralCall { + continue; + } + let Some(Immediate::Data(data_id)) = inst.immediate else { + continue; + }; + let Some(fragment) = data.strings.get(data_id.as_raw() as usize) else { + continue; + }; + let plan = crate::eval_aot::plan_literal_fragment_with_static_calls(fragment, |_, _| false); + for name in plan.reads() { + if let Some(slot) = slots_by_name.get(name) { + eligible.remove(slot); + } + } + } +} + /// Drops slots whose loaded value can be reinterpreted as the slot's address. /// /// A by-reference call argument or closure capture aliases the underlying slot: @@ -188,7 +229,11 @@ fn immediate_slots(immediate: Option<&Immediate>) -> Vec { } /// Returns the eligible local slot loaded or stored by an instruction, if any. -fn instruction_slot(function: &Function, inst_id: InstId, eligible: &HashSet) -> Option<(Op, LocalSlotId)> { +fn instruction_slot( + function: &Function, + inst_id: InstId, + eligible: &HashSet, +) -> Option<(Op, LocalSlotId)> { let inst = function.instruction(inst_id)?; let Some(Immediate::LocalSlot(slot)) = inst.immediate else { return None; diff --git a/src/ir_passes/peephole/load_store.rs b/src/ir_passes/peephole/load_store.rs index d66f685a60..7c4ebea310 100644 --- a/src/ir_passes/peephole/load_store.rs +++ b/src/ir_passes/peephole/load_store.rs @@ -217,6 +217,7 @@ fn consumes_operands_by_value(op: Op) -> bool { | StoreGlobal | StoreStaticLocal | StoreStaticProperty + | StoreReflectionStaticProperty | InitStaticLocal | ThrowException | GeneratorYield diff --git a/src/lexer/literals/identifiers.rs b/src/lexer/literals/identifiers.rs index f08fefe891..0d522a6006 100644 --- a/src/lexer/literals/identifiers.rs +++ b/src/lexer/literals/identifiers.rs @@ -184,6 +184,7 @@ pub(in crate::lexer) fn scan_keyword(cursor: &mut Cursor) -> Result Ok(Token::Class), "enum" => Ok(Token::Enum), "new" => Ok(Token::New), + "clone" => Ok(Token::Clone), "public" => Ok(Token::Public), "protected" => Ok(Token::Protected), "private" => Ok(Token::Private), diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 6b651724b9..7c5b3e248b 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -108,6 +108,7 @@ pub enum Token { Class, // class Enum, // enum New, // new + Clone, // clone Public, // public Protected, // protected Private, // private diff --git a/src/lib.rs b/src/lib.rs index 4630ece030..1963a36daa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ //! - Public module boundaries here are part of the crate-facing compiler API. pub mod autoload; +/// Builtin catalog and signature metadata snapshots. +pub mod builtin_metadata; /// Single-source builtin registry: catalog, signatures, type-check, and lowering dispatch. pub mod builtins; /// Canonical EIR-consuming assembly backend and public codegen helpers. @@ -20,8 +22,11 @@ pub mod codegen_support; pub mod conditional; /// Error and warning reporting. pub mod errors; +mod eval_aot; /// `#[Export]` attribute scan for cdylib emission. pub mod exports; +/// Image (GD/Exif/Imagick/Gmagick/Cairo) standard-library prelude injection. +pub mod image_prelude; /// Intrinsic call handling. pub mod intrinsics; /// Intermediate representation used by the EIR backend track. @@ -36,18 +41,16 @@ pub mod lexer; pub mod list_id_prelude; /// Magic constant substitution. pub mod magic_constants; -/// Name resolution and mangling. -pub mod names; /// Namespace and use resolution. pub mod name_resolver; +/// Name resolution and mangling. +pub mod names; /// Optimizer passes. pub mod optimize; /// Parser for PHP syntax. pub mod parser; /// PDO (SQLite) standard-library prelude injection. pub mod pdo_prelude; -/// Image (GD/Exif/Imagick/Gmagick/Cairo) standard-library prelude injection. -pub mod image_prelude; /// Resolution of includes. pub mod resolver; /// Source span tracking. diff --git a/src/linker.rs b/src/linker.rs index 939538a379..ca339b4e3f 100644 --- a/src/linker.rs +++ b/src/linker.rs @@ -134,6 +134,15 @@ const BRIDGES: &[BridgeStaticlib] = &[ // Rust runtime/unwinder symbols, like the other bridges. needs_libdl: true, }, + BridgeStaticlib { + lib_name: "elephc_magician", + env_var: "ELEPHC_MAGICIAN_LIB_DIR", + crate_name: "elephc-magician", + flag_name: "eval", + whole_archive: false, + macos_frameworks: &[], + needs_libdl: true, + }, ]; /// Resolves a `--with-` crate flag to its bridge `lib_name`, or `None` @@ -720,6 +729,10 @@ fn macos_sdk_version() -> String { #[cfg(test)] mod tests { use super::*; + use std::sync::{Mutex, OnceLock}; + + /// Serializes tests that temporarily modify process environment variables. + static ENV_LOCK: OnceLock> = OnceLock::new(); /// Verifies an empty or whitespace-only SDK path (xcrun missing or misconfigured) /// yields an actionable Xcode Command Line Tools hint instead of being silently @@ -779,6 +792,19 @@ mod tests { assert!(!entry.whole_archive, "tz bridge must not force-load (no link-time side effects)"); } + /// Verifies the optional eval bridge is registered for programs that use `eval()`. + #[test] + fn bridges_includes_elephc_magician() { + let entry = BRIDGES + .iter() + .find(|b| b.lib_name == "elephc_magician") + .expect("elephc_magician must be a registered bridge"); + assert_eq!(entry.crate_name, "elephc-magician"); + assert_eq!(entry.env_var, "ELEPHC_MAGICIAN_LIB_DIR"); + assert_eq!(entry.archive_filename(), "libelephc_magician.a"); + assert!(!entry.whole_archive, "eval bridge must not force-load"); + } + /// Verifies every bridge exposes a non-empty `--with-` name and that /// `bridge_lib_for_flag` maps each one back to its `lib_name`, so the CLI's /// `--with-` validation stays in lockstep with the `BRIDGES` table. @@ -807,4 +833,28 @@ mod tests { assert!(crate_flag_names().contains(&"pdo")); assert_eq!(crate_flag_names().len(), BRIDGES.len()); } + + /// Verifies the eval bridge honors `ELEPHC_MAGICIAN_LIB_DIR` before filesystem discovery. + #[test] + fn eval_bridge_lib_dir_uses_env_override() { + let _guard = ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("env lock should not be poisoned"); + let previous = std::env::var_os("ELEPHC_MAGICIAN_LIB_DIR"); + let override_dir = "/tmp/elephc-magician-lib-dir-override"; + std::env::set_var("ELEPHC_MAGICIAN_LIB_DIR", override_dir); + let entry = BRIDGES + .iter() + .find(|b| b.lib_name == "elephc_magician") + .expect("elephc_magician must be a registered bridge"); + + let resolved = entry.lib_dir(); + + match previous { + Some(value) => std::env::set_var("ELEPHC_MAGICIAN_LIB_DIR", value), + None => std::env::remove_var("ELEPHC_MAGICIAN_LIB_DIR"), + } + assert_eq!(resolved.as_deref(), Some(override_dir)); + } } diff --git a/src/list_id_prelude/detect.rs b/src/list_id_prelude/detect.rs index 20175e71da..8b03391fb0 100644 --- a/src/list_id_prelude/detect.rs +++ b/src/list_id_prelude/detect.rs @@ -36,6 +36,16 @@ fn name_is_listid_fn(name: &Name) -> bool { .is_some_and(|segment| segment.eq_ignore_ascii_case("timezone_identifiers_list")) } +/// Returns whether a function name is PHP's `eval` construct. +/// +/// Runtime eval can call `timezone_identifiers_list()` from a string that this +/// prelude detector cannot inspect statically, so an `eval(...)` call is treated +/// as a potential identifier-listing use. +fn name_is_eval_fn(name: &Name) -> bool { + name.last_segment() + .is_some_and(|segment| segment.eq_ignore_ascii_case("eval")) +} + /// Returns whether a method name is `listIdentifiers`, compared case-insensitively /// as PHP method names are. fn method_is_listid(method: &str) -> bool { @@ -128,7 +138,7 @@ fn expr_refs_listid(expr: &Expr) -> bool { | ExprKind::MagicConstant(_) => false, ExprKind::FunctionCall { name, args } => { - name_is_listid_fn(name) || args.iter().any(expr_refs_listid) + name_is_eval_fn(name) || name_is_listid_fn(name) || args.iter().any(expr_refs_listid) } ExprKind::MethodCall { object, @@ -140,6 +150,15 @@ fn expr_refs_listid(expr: &Expr) -> bool { method, args, } => method_is_listid(method) || expr_refs_listid(object) || args.iter().any(expr_refs_listid), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_refs_listid(object) + || expr_refs_listid(method) + || args.iter().any(expr_refs_listid) + } ExprKind::StaticMethodCall { method, args, .. } => { method_is_listid(method) || args.iter().any(expr_refs_listid) } @@ -155,6 +174,7 @@ fn expr_refs_listid(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -460,6 +480,15 @@ mod tests { ))); } + /// A runtime `eval(...)` call is detected because the eval fragment can call + /// `timezone_identifiers_list()` dynamically. + #[test] + fn detects_eval_call() { + assert!(program_uses_list_identifiers(&parse( + r#"(expr: Expr, pass: &mut P) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(walk_expr(*inner, pass))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(walk_expr(*inner, pass))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(walk_expr(*inner, pass))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(walk_expr(*inner, pass))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(walk_expr(*inner, pass))), ExprKind::Print(inner) => ExprKind::Print(Box::new(walk_expr(*inner, pass))), ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { @@ -129,6 +130,7 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -150,6 +152,7 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { ExprKind::Closure { params: new_params, variadic, + variadic_by_ref, variadic_type, return_type, body: new_body, @@ -241,6 +244,15 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { method, args: args.into_iter().map(|a| walk_expr(a, pass)).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(walk_expr(*object, pass)), + method: Box::new(walk_expr(*method, pass)), + args: args.into_iter().map(|a| walk_expr(a, pass)).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index ef600df401..2b7cafa755 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -241,7 +241,9 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -259,7 +261,9 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { by_ref_return, name, params: new_params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: new_body, @@ -349,6 +353,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } => { @@ -372,6 +377,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } diff --git a/src/main.rs b/src/main.rs index 556394e8b1..95731f4ddc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,9 @@ mod codegen; mod codegen_support; mod conditional; mod errors; +mod eval_aot; mod exports; +mod image_prelude; mod intrinsics; #[allow(dead_code, unused_imports)] mod ir; @@ -23,12 +25,11 @@ mod ir; mod ir_lower; #[allow(dead_code, unused_imports)] mod ir_passes; -mod linker; mod lexer; +mod linker; mod list_id_prelude; mod magic_constants; mod name_resolver; -mod image_prelude; mod names; mod optimize; mod parser; diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index a8b2d881aa..4791332f54 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -40,7 +40,9 @@ pub(super) fn resolve_decl_stmt( by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -51,7 +53,12 @@ pub(super) fn resolve_decl_stmt( by_ref_return: *by_ref_return, name: canonical_name_for_decl(namespace, name), params: resolve_params(params, namespace, imports, symbols), + param_attributes: param_attributes + .iter() + .map(|groups| resolve_attribute_groups(groups, namespace, imports, symbols)) + .collect(), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type .as_ref() @@ -108,9 +115,14 @@ pub(super) fn resolve_decl_stmt( backing_type, cases, implements, + trait_uses, methods, constants, } => { + let trait_uses = trait_uses + .iter() + .map(|trait_use| resolve_trait_use(trait_use, namespace, imports, symbols)) + .collect::, CompileError>>()?; let resolved_cases = cases .iter() .map(|case| crate::parser::ast::EnumCaseDecl { @@ -140,6 +152,7 @@ pub(super) fn resolve_decl_stmt( resolved_name(resolved_class_name(name, namespace, imports, symbols)) }) .collect(), + trait_uses, methods: resolved_methods, constants: resolve_class_consts(constants, namespace, imports, symbols), }, @@ -276,10 +289,7 @@ fn resolve_attribute_groups( .iter() .map(|attr| Attribute { name: resolved_name(resolved_class_name( - &attr.name, - namespace, - imports, - symbols, + &attr.name, namespace, imports, symbols, )), args: attr .args @@ -308,6 +318,11 @@ fn resolve_methods( let body = resolve_stmt_list(&method.body, namespace, imports, symbols)?; Ok(ClassMethod { params: resolve_params(&method.params, namespace, imports, symbols), + param_attributes: method + .param_attributes + .iter() + .map(|groups| resolve_attribute_groups(groups, namespace, imports, symbols)) + .collect(), return_type: method .return_type .as_ref() @@ -337,12 +352,7 @@ fn resolve_class_consts( .iter() .map(|constant| ClassConst { value: resolve_expr(&constant.value, namespace, imports, symbols), - attributes: resolve_attribute_groups( - &constant.attributes, - namespace, - imports, - symbols, - ), + attributes: resolve_attribute_groups(&constant.attributes, namespace, imports, symbols), ..constant.clone() }) .collect() @@ -367,19 +377,16 @@ fn resolve_properties( .default .as_ref() .map(|expr| resolve_expr(expr, namespace, imports, symbols)), - attributes: resolve_attribute_groups( - &property.attributes, - namespace, - imports, - symbols, - ), + attributes: resolve_attribute_groups(&property.attributes, namespace, imports, symbols), ..property.clone() }) .collect() } -/// Resolves a trait use statement by rewriting its trait names and adaptations -/// (aliases and instead-of rules) through `resolved_class_name` and `php_symbol_key`. +/// Resolves a trait use statement by rewriting trait names and method selectors. +/// +/// Alias display names keep their declared spelling because Reflection exposes +/// them, while later method lookup still normalizes through `php_symbol_key`. pub(super) fn resolve_trait_use( trait_use: &TraitUse, current_namespace: Option<&str>, @@ -409,18 +416,16 @@ pub(super) fn resolve_trait_use( alias, visibility, } => Ok(TraitAdaptation::Alias { - trait_name: trait_name - .as_ref() - .map(|name| { - resolved_name(resolved_class_name( - name, - current_namespace, - imports, - symbols, - )) - }), + trait_name: trait_name.as_ref().map(|name| { + resolved_name(resolved_class_name( + name, + current_namespace, + imports, + symbols, + )) + }), method: php_symbol_key(method), - alias: alias.as_ref().map(|alias| php_symbol_key(alias)), + alias: alias.clone(), visibility: visibility.clone(), }), TraitAdaptation::InsteadOf { @@ -428,16 +433,14 @@ pub(super) fn resolve_trait_use( method, instead_of, } => Ok(TraitAdaptation::InsteadOf { - trait_name: trait_name - .as_ref() - .map(|name| { - resolved_name(resolved_class_name( - name, - current_namespace, - imports, - symbols, - )) - }), + trait_name: trait_name.as_ref().map(|name| { + resolved_name(resolved_class_name( + name, + current_namespace, + imports, + symbols, + )) + }), method: php_symbol_key(method), instead_of: instead_of .iter() diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index 98729e303f..42dce9fdc4 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -165,6 +165,7 @@ pub(super) fn resolve_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -176,6 +177,7 @@ pub(super) fn resolve_expr( } => ExprKind::Closure { params: resolve_params(params, current_namespace, imports, symbols), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type .as_ref() @@ -299,6 +301,18 @@ pub(super) fn resolve_expr( .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) .collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(resolve_expr(object, current_namespace, imports, symbols)), + method: Box::new(resolve_expr(method, current_namespace, imports, symbols)), + args: args + .iter() + .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) + .collect(), + }, ExprKind::StaticMethodCall { receiver, method, @@ -310,7 +324,9 @@ pub(super) fn resolve_expr( )), _ => receiver.clone(), }; - let resolved_method = php_symbol_key(method); + // Keep the source spelling: dispatch lookups fold case at lookup + // time, and `__callStatic` must receive the as-written name. + let method_key = php_symbol_key(method); let resolved_args: Vec = args .iter() .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) @@ -321,7 +337,7 @@ pub(super) fn resolve_expr( // function's flow-inferred array return keeps its element type, // so in_array works on the filtered result, where the synthetic method // would yield scalar mixed and regress in_array. - if resolved_method == "listidentifiers" + if method_key == "listidentifiers" && resolved_args.len() <= 2 && matches!( &resolved_receiver, @@ -337,7 +353,7 @@ pub(super) fn resolve_expr( } else { ExprKind::StaticMethodCall { receiver: resolved_receiver, - method: resolved_method, + method: method.clone(), args: resolved_args, } } diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 5db00318ea..c5682fbff1 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -496,7 +496,9 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -505,7 +507,9 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: dce_block_with_guards(body, GuardState::default()), @@ -574,6 +578,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, } => vec![Stmt { @@ -582,6 +587,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index ef7c9c8924..53f6d50330 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -173,7 +173,9 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -181,7 +183,9 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { by_ref_return, name, params: fold_params(params), + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: fold_block(body), @@ -228,12 +232,14 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods: methods.into_iter().map(fold_method).collect(), constants, cases: cases.into_iter().map(fold_enum_case).collect(), diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index 6bb9af9289..e93cd07a7c 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -42,6 +42,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(prune_expr(*inner))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(prune_expr(*inner))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(prune_expr(*inner))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(prune_expr(*inner))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(prune_expr(*inner))), ExprKind::Print(inner) => ExprKind::Print(Box::new(prune_expr(*inner))), ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { @@ -123,6 +124,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -134,6 +136,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { } => ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body: prune_block(body), @@ -219,6 +222,15 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { method, args: args.into_iter().map(prune_expr).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(prune_expr(*object)), + method: Box::new(prune_expr(*method)), + args: args.into_iter().map(prune_expr).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index 64a108cced..3ed1892269 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -244,7 +244,9 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -253,7 +255,9 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: prune_block(body), @@ -317,6 +321,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, } => vec![Stmt { @@ -325,6 +330,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index 9fc6574987..327baa599c 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -227,6 +227,9 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { | ExprKind::PtrCast { expr: inner, .. } | ExprKind::Spread(inner) => expr_effect(inner), ExprKind::Print(inner) => expr_effect(inner).with_side_effects(), + ExprKind::Clone(inner) => expr_effect(inner) + .with_side_effects() + .with_may_throw(), ExprKind::BinaryOp { left, right, .. } => expr_effect(left).combine(expr_effect(right)), ExprKind::InstanceOf { value, target } => { expr_effect(value).combine(instanceof_target_effect(target)) @@ -263,6 +266,15 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { ExprKind::ExprCall { callee, args } => expr_effect(callee) .combine(combine_effects(args.iter().map(expr_effect))) .combine(expr_call_effect(callee)), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_effect(object) + .combine(expr_effect(method)) + .combine(combine_effects(args.iter().map(expr_effect))) + .with_side_effects() + .with_may_throw(), ExprKind::NewObject { args, .. } => combine_effects(args.iter().map(expr_effect)) .with_side_effects() .with_may_throw(), diff --git a/src/optimize/effects/builtins.rs b/src/optimize/effects/builtins.rs index 4aa8ee0c23..b3cae89802 100644 --- a/src/optimize/effects/builtins.rs +++ b/src/optimize/effects/builtins.rs @@ -37,15 +37,20 @@ pub(in crate::optimize) fn is_pure_non_throwing_builtin(name: &str) -> bool { | "intval" | "floatval" | "boolval" + | "strval" | "gettype" | "is_array" | "is_object" | "is_scalar" | "is_bool" + | "is_double" | "is_float" | "is_int" + | "is_integer" + | "is_long" | "is_null" | "is_numeric" + | "is_real" | "is_string" | "is_resource" | "get_resource_type" diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index 99b17399c6..72d7704326 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -8,24 +8,32 @@ //! Key details: //! - Folding must respect PHP coercions, truthiness, numeric edge cases, and runtime error boundaries. -use super::super::{fold_block, try_prune_match_expr}; use super::super::*; +use super::super::{fold_block, try_prune_match_expr}; use super::casts::try_fold_cast; use super::ops::{ - try_fold_array_access, try_fold_binary_op, try_fold_bit_not, try_fold_negate, - try_fold_not, try_fold_null_coalesce, try_fold_short_ternary, try_fold_ternary, + try_fold_array_access, try_fold_binary_op, try_fold_bit_not, try_fold_negate, try_fold_not, + try_fold_null_coalesce, try_fold_short_ternary, try_fold_ternary, }; /// Folds default expressions in function parameters. /// Returns a new parameter list with each parameter's default expression folded. pub(in crate::optimize) fn fold_params( - params: Vec<(String, Option, Option, bool)>, -) -> Vec<(String, Option, Option, bool)> { + params: Vec<( + String, + Option, + Option, + bool, + )>, +) -> Vec<( + String, + Option, + Option, + bool, +)> { params .into_iter() - .map(|(name, type_expr, default, is_ref)| { - (name, type_expr, default.map(fold_expr), is_ref) - }) + .map(|(name, type_expr, default, is_ref)| (name, type_expr, default.map(fold_expr), is_ref)) .collect() } @@ -42,6 +50,7 @@ pub(in crate::optimize) fn fold_property(property: ClassProperty) -> ClassProper is_static: property.is_static, is_abstract: property.is_abstract, by_ref: property.by_ref, + is_promoted: property.is_promoted, default: property.default.map(fold_expr), span: property.span, attributes: property.attributes, @@ -58,7 +67,9 @@ pub(in crate::optimize) fn fold_method(method: ClassMethod) -> ClassMethod { is_final: method.is_final, has_body: method.has_body, params: fold_params(method.params), + param_attributes: method.param_attributes, variadic: method.variadic, + variadic_by_ref: method.variadic_by_ref, variadic_type: method.variadic_type, return_type: method.return_type, by_ref_return: method.by_ref_return, @@ -91,9 +102,9 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { let kind = match expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } ExprKind::StringLiteral(value) => ExprKind::StringLiteral(value), ExprKind::IntLiteral(value) => ExprKind::IntLiteral(value), ExprKind::FloatLiteral(value) => ExprKind::FloatLiteral(value), @@ -126,6 +137,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { try_fold_bit_not(&inner).unwrap_or_else(|| ExprKind::BitNot(Box::new(inner))) } ExprKind::Throw(inner) => ExprKind::Throw(Box::new(fold_expr(*inner))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(fold_expr(*inner))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(fold_expr(*inner))), ExprKind::Print(inner) => ExprKind::Print(Box::new(fold_expr(*inner))), ExprKind::NullCoalesce { value, default } => { @@ -243,6 +255,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -254,6 +267,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { } => ExprKind::Closure { params: fold_params(params), variadic, + variadic_by_ref, variadic_type, return_type, body: fold_block(body), @@ -300,18 +314,14 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { object: Box::new(fold_expr(*object)), property, }, - ExprKind::DynamicPropertyAccess { object, property } => { - ExprKind::DynamicPropertyAccess { - object: Box::new(fold_expr(*object)), - property: Box::new(fold_expr(*property)), - } - } - ExprKind::NullsafePropertyAccess { object, property } => { - ExprKind::NullsafePropertyAccess { - object: Box::new(fold_expr(*object)), - property, - } - } + ExprKind::DynamicPropertyAccess { object, property } => ExprKind::DynamicPropertyAccess { + object: Box::new(fold_expr(*object)), + property: Box::new(fold_expr(*property)), + }, + ExprKind::NullsafePropertyAccess { object, property } => ExprKind::NullsafePropertyAccess { + object: Box::new(fold_expr(*object)), + property, + }, ExprKind::NullsafeDynamicPropertyAccess { object, property } => { ExprKind::NullsafeDynamicPropertyAccess { object: Box::new(fold_expr(*object)), @@ -339,6 +349,15 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { method, args: args.into_iter().map(fold_expr).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(fold_expr(*object)), + method: Box::new(fold_expr(*method)), + args: args.into_iter().map(fold_expr).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/fold/inline_closure.rs b/src/optimize/fold/inline_closure.rs index 1ca249870a..bf0d7d03a9 100644 --- a/src/optimize/fold/inline_closure.rs +++ b/src/optimize/fold/inline_closure.rs @@ -149,6 +149,7 @@ fn expr_contains_call(expr: &Expr) -> bool { | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } | ExprKind::NewObject { .. } | ExprKind::NewScopedObject { .. } => true, diff --git a/src/optimize/fold/pipes.rs b/src/optimize/fold/pipes.rs index e88231e602..3aa150b030 100644 --- a/src/optimize/fold/pipes.rs +++ b/src/optimize/fold/pipes.rs @@ -84,8 +84,8 @@ pub(super) fn try_fold_pure_pipe(value: &Expr, callable: &Expr) -> Option Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::IntLiteral(_)))), - ("is_float", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::FloatLiteral(_)))), + ("is_int" | "is_integer" | "is_long", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::IntLiteral(_)))), + ("is_float" | "is_double" | "is_real", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::FloatLiteral(_)))), ("is_string", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::StringLiteral(_)))), ("is_bool", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::BoolLiteral(_)))), ("is_null", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::Null))), diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index fd3c1e287b..ddfaf17acd 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -88,6 +88,10 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(propagate_expr(*inner, env))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(propagate_expr(*inner, env))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(propagate_expr(*inner, env))), + ExprKind::Clone(inner) => { + let empty_env = HashMap::new(); + ExprKind::Clone(Box::new(propagate_expr(*inner, &empty_env))) + } ExprKind::ErrorSuppress(inner) => { ExprKind::ErrorSuppress(Box::new(propagate_expr(*inner, env))) } @@ -193,6 +197,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -210,6 +215,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Closure { params: propagate_params(params), variadic, + variadic_by_ref, variadic_type, return_type, body: super::stmt::with_function_scope(|| { @@ -315,6 +321,19 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { args: propagate_args(args, None, None), } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + let object = propagate_expr(*object, env); + let method = propagate_expr(*method, env); + ExprKind::NullsafeDynamicMethodCall { + object: Box::new(object), + method: Box::new(method), + args: propagate_args(args, None, None), + } + } ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index 7d3732f866..f645fc4ed6 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -16,9 +16,11 @@ //! - By-ref arguments to user-defined callees are also marked volatile: the //! callee may retain the reference (e.g. in a by-ref closure capture) and //! write it during any later call. Builtins never retain their arguments. -//! - `Invalidation::All` remains for genuinely unknowable writes: `include`, -//! `yield`, spreads into by-ref callees, and global-writing (or unknown) -//! callees invoked from top-level scope. +//! - `Invalidation::All` remains for genuinely unknowable writes: `eval`, +//! `include`, `yield`, spreads into by-ref callees, and global-writing (or +//! unknown) callees invoked from top-level scope. + +use crate::names::php_symbol_key; use super::*; @@ -116,6 +118,12 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { | ExprKind::Cast { expr: inner, .. } | ExprKind::BufferNew { len: inner, .. } | ExprKind::NamedArg { value: inner, .. } => expr_invalidation(inner), + ExprKind::Clone(inner) => expr_invalidation(inner).union(top_level_globals_guard( + Effect::PURE + .with_side_effects() + .with_may_throw() + .with_writes_globals(), + )), ExprKind::BinaryOp { left, right, .. } => { expr_invalidation(left).union(expr_invalidation(right)) } @@ -206,6 +214,11 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { acc.union(unset_target_invalidation(arg)) }) } + ExprKind::FunctionCall { name, .. } + if php_symbol_key(name.as_str().trim_start_matches('\\')) == "eval" => + { + Invalidation::All + } // `ptr($x)` (elephc pointer extension) takes the address of a local: // from this point on, `ptr_set`/`ptr_write*` through any alias of the // pointer rewrites the variable outside the PHP reference model, so @@ -255,6 +268,20 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { object, method, ))) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_invalidation(object) + .union(expr_invalidation(method)) + .union(args_invalidation(args)) + .union(call_args_invalidation(None, args, true)) + .union(top_level_globals_guard( + Effect::PURE + .with_side_effects() + .with_may_throw() + .with_writes_globals(), + )), ExprKind::StaticMethodCall { receiver, method, @@ -376,7 +403,7 @@ fn call_args_invalidation( if spread_seen && has_by_ref { return Invalidation::All; } - if sig.get(position).is_some_and(|(_, is_ref)| *is_ref) { + if positional_param_is_by_ref(sig, position) { expose_argument_root(arg, retain, &mut inv); } position += 1; @@ -386,6 +413,14 @@ fn call_args_invalidation( inv } +/// Returns whether a positional argument lands on a by-ref parameter. +fn positional_param_is_by_ref(sig: &[(String, bool)], position: usize) -> bool { + if let Some((_, is_ref)) = sig.get(position) { + return *is_ref; + } + sig.last().is_some_and(|(_, is_ref)| *is_ref) +} + /// Records an argument's lvalue root as writable (and volatile when the callee /// can retain the reference). Arguments without a local root — literals, /// property accesses, call results — expose no caller local. diff --git a/src/optimize/propagate/signatures.rs b/src/optimize/propagate/signatures.rs index 862d69e8ae..1f10ab5f13 100644 --- a/src/optimize/propagate/signatures.rs +++ b/src/optimize/propagate/signatures.rs @@ -127,18 +127,28 @@ fn union_params(union: &mut Vec<(String, bool)>, params: &[(String, bool)]) { } /// Converts an AST parameter tuple list into `(param name, is_by_ref)` pairs. -fn params_signature( +fn params_signature_with_variadic( params: &[(String, Option, Option, bool)], + variadic: Option<&str>, + variadic_by_ref: bool, ) -> Vec<(String, bool)> { - params + let mut signature: Vec<_> = params .iter() .map(|(name, _, _, is_ref)| (name.clone(), *is_ref)) - .collect() + .collect(); + if let Some(variadic) = variadic { + signature.push((variadic.to_string(), variadic_by_ref)); + } + signature } /// Records one method declaration into the by-name union and the ctor flag. fn collect_method(method: &ClassMethod, sigs: &mut ByRefSignatures) { - let signature = params_signature(&method.params); + let signature = params_signature_with_variadic( + &method.params, + method.variadic.as_deref(), + method.variadic_by_ref, + ); if method.name.eq_ignore_ascii_case("__construct") && signature.iter().any(|(_, by_ref)| *by_ref) { @@ -173,9 +183,17 @@ fn collect_properties(properties: &[ClassProperty], sigs: &mut ByRefSignatures) fn collect_from_block(body: &[Stmt], sigs: &mut ByRefSignatures) { for stmt in body { match &stmt.kind { - StmtKind::FunctionDecl { name, params, .. } => { - sigs.functions - .insert(name.clone(), params_signature(params)); + StmtKind::FunctionDecl { + name, + params, + variadic, + variadic_by_ref, + .. + } => { + sigs.functions.insert( + name.clone(), + params_signature_with_variadic(params, variadic.as_deref(), *variadic_by_ref), + ); } StmtKind::ClassDecl { properties, diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index 72ce565da3..614c286e8e 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -287,7 +287,9 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -297,7 +299,9 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv by_ref_return, name, params: propagate_params(params), + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: with_function_scope(|| propagate_block(body, HashMap::new()).0), @@ -378,6 +382,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv backing_type, cases, implements, + trait_uses, methods, constants, } => ( @@ -386,6 +391,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv name, backing_type, implements, + trait_uses, methods: methods.into_iter().map(propagate_method).collect(), constants, cases: cases.into_iter().map(propagate_enum_case).collect(), diff --git a/src/optimize/propagate/stmt/declarations.rs b/src/optimize/propagate/stmt/declarations.rs index 1407c3a887..5c12e00dd4 100644 --- a/src/optimize/propagate/stmt/declarations.rs +++ b/src/optimize/propagate/stmt/declarations.rs @@ -48,6 +48,7 @@ pub(super) fn propagate_property(property: ClassProperty) -> ClassProperty { is_static: property.is_static, is_abstract: property.is_abstract, by_ref: property.by_ref, + is_promoted: property.is_promoted, default: property .default .map(|expr| propagate_expr(expr, &HashMap::new())), diff --git a/src/optimize/tests/dce/basics.rs b/src/optimize/tests/dce/basics.rs index 0b75e78070..c08f53d3e1 100644 --- a/src/optimize/tests/dce/basics.rs +++ b/src/optimize/tests/dce/basics.rs @@ -28,7 +28,9 @@ fn test_eliminate_dead_code_invalidates_outer_strict_bool_guard_after_local_writ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs index 6e0dd299bc..595a10d302 100644 --- a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs +++ b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs @@ -26,7 +26,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_demorgan_equivalent_gua StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -74,7 +76,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_loose_comparison_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -121,7 +125,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_relational_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -168,7 +174,9 @@ fn test_eliminate_dead_code_prunes_nested_elseif_from_composite_guard_refinement StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -231,7 +239,9 @@ fn test_eliminate_dead_code_prunes_nested_subexpr_from_composite_guard_refinemen StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs index b02e104f37..a264d83a5a 100644 --- a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs +++ b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs @@ -41,7 +41,9 @@ fn test_eliminate_dead_code_rebuilds_empty_elseif_tail_as_needed_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -99,7 +101,9 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_cumulative_fal StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -165,7 +169,9 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_negated_compos StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -239,7 +245,9 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_demorgan_equiv StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/excluded_guards.rs b/src/optimize/tests/dce/guards/excluded_guards.rs index 236aa381a8..fa1fa1b5c7 100644 --- a/src/optimize/tests/dce/guards/excluded_guards.rs +++ b/src/optimize/tests/dce/guards/excluded_guards.rs @@ -19,7 +19,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_zero_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -68,7 +70,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_null_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -122,7 +126,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_empty_string_g StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -179,7 +185,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_string_zero_gu StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -236,7 +244,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_float_guard() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs index 22101406db..5d598ae4f7 100644 --- a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs @@ -35,7 +35,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_strict_bool_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -86,7 +88,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_and_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -134,7 +138,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_negated_and_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -190,7 +196,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_or_false_branch() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs index 18a052f308..f65bd4ae41 100644 --- a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs @@ -20,7 +20,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -69,7 +71,9 @@ fn test_eliminate_dead_code_invalidates_outer_guard_after_local_write() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -120,7 +124,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_null_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -175,7 +181,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -222,7 +230,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_empty_string_guar StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -273,7 +283,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_string_zero_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -324,7 +336,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_float_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/basics.rs b/src/optimize/tests/dce/switches/basics.rs index b01703e602..291f2af81e 100644 --- a/src/optimize/tests/dce/switches/basics.rs +++ b/src/optimize/tests/dce/switches/basics.rs @@ -31,7 +31,9 @@ fn test_eliminate_dead_code_drops_empty_switch_shell_created_by_branch_dce() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/case_shadowing.rs b/src/optimize/tests/dce/switches/case_shadowing.rs index 0215c7f2f8..c4010ddcf7 100644 --- a/src/optimize/tests/dce/switches/case_shadowing.rs +++ b/src/optimize/tests/dce/switches/case_shadowing.rs @@ -17,7 +17,9 @@ fn test_eliminate_dead_code_drops_switch_case_shadowed_by_terminating_duplicate_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -75,7 +77,9 @@ fn test_eliminate_dead_code_merges_fallthrough_body_from_fully_shadowed_switch_c StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -128,7 +132,9 @@ fn test_eliminate_dead_code_prunes_dead_label_inside_live_mixed_switch_case() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs index 492865e52f..fe73f489b7 100644 --- a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs +++ b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs @@ -25,7 +25,9 @@ fn test_eliminate_dead_code_prunes_negated_strict_switch_true_case() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -80,7 +82,9 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_and_switch_true_default() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -126,7 +130,9 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_or_switch_true_default() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -174,7 +180,9 @@ fn test_eliminate_dead_code_prunes_switch_true_suffix_after_exhaustive_multi_pat StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -221,7 +229,9 @@ fn test_eliminate_dead_code_prunes_scalar_switch_suffix_after_exhaustive_multi_p StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs index aecbcc2761..f8aa6bd98f 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs @@ -23,7 +23,9 @@ fn test_eliminate_dead_code_prunes_exhaustive_switch_true_default_from_cumulativ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -90,7 +92,9 @@ fn test_eliminate_dead_code_uses_cumulative_switch_true_guards_inside_case_body( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs index 7dd4ceef38..25a9e5f38f 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs @@ -33,7 +33,9 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_switch_bool_guard_case( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -85,7 +87,9 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_exact_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -136,7 +140,9 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_excluded_gu StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -210,7 +216,9 @@ fn test_eliminate_dead_code_drops_impossible_switch_true_cases_from_outer_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -267,7 +275,9 @@ fn test_eliminate_dead_code_invalidates_switch_bool_guard_after_local_write() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs index 898215b306..01c87014c9 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs @@ -17,7 +17,9 @@ fn test_eliminate_dead_code_prunes_truthy_switch_cases_and_default() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -82,7 +84,9 @@ fn test_eliminate_dead_code_prunes_falsy_scalar_labels_from_truthy_switch_subjec StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -145,7 +149,9 @@ fn test_eliminate_dead_code_combines_exclusion_and_truthy_switch_guards() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/tail_paths.rs b/src/optimize/tests/dce/switches/tail_paths.rs index 9ca980c978..cbf71eab77 100644 --- a/src/optimize/tests/dce/switches/tail_paths.rs +++ b/src/optimize/tests/dce/switches/tail_paths.rs @@ -31,7 +31,9 @@ fn test_eliminate_dead_code_drops_trailing_empty_switch_cases() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -90,7 +92,9 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_exit_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -140,7 +144,9 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_break_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tail_sinking.rs b/src/optimize/tests/dce/tail_sinking.rs index e3c2dbeaf5..3159a3b1f7 100644 --- a/src/optimize/tests/dce/tail_sinking.rs +++ b/src/optimize/tests/dce/tail_sinking.rs @@ -42,7 +42,9 @@ fn test_eliminate_dead_code_reduces_empty_if_chain_to_needed_condition_checks() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -90,7 +92,9 @@ fn test_eliminate_dead_code_sinks_tail_into_if_fallthrough_branch() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -144,7 +148,9 @@ fn test_eliminate_dead_code_sinks_tail_into_implicit_else_path() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -199,7 +205,9 @@ fn test_eliminate_dead_code_sinks_tail_into_ifdef_fallthrough_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -265,7 +273,9 @@ fn test_eliminate_dead_code_reduces_empty_if_to_effectful_condition_eval() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/catch_pruning.rs b/src/optimize/tests/dce/tries/catch_pruning.rs index 69971fce08..f2741af94e 100644 --- a/src/optimize/tests/dce/tries/catch_pruning.rs +++ b/src/optimize/tests/dce/tries/catch_pruning.rs @@ -20,7 +20,9 @@ fn test_eliminate_dead_code_drops_unreachable_catches_after_non_throwing_try() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -59,7 +61,9 @@ fn test_eliminate_dead_code_drops_unreachable_catches_before_finally() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -98,7 +102,9 @@ fn test_eliminate_dead_code_drops_catches_shadowed_by_throwable() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -153,7 +159,9 @@ fn test_eliminate_dead_code_drops_duplicate_shadowed_catch_types() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -210,7 +218,9 @@ fn test_eliminate_dead_code_merges_identical_catches_exposed_by_shadow_drop() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/finally_paths.rs b/src/optimize/tests/dce/tries/finally_paths.rs index b1ac912ccb..63e6fd515d 100644 --- a/src/optimize/tests/dce/tries/finally_paths.rs +++ b/src/optimize/tests/dce/tries/finally_paths.rs @@ -22,7 +22,9 @@ fn test_eliminate_dead_code_drops_statements_after_try_finally_exit() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -70,7 +72,9 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_finally_when_only_other_lo StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -129,7 +133,9 @@ fn test_eliminate_dead_code_sinks_tail_into_safe_finally_path() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/tail_paths.rs b/src/optimize/tests/dce/tries/tail_paths.rs index f1de297d8a..72ae706335 100644 --- a/src/optimize/tests/dce/tries/tail_paths.rs +++ b/src/optimize/tests/dce/tries/tail_paths.rs @@ -19,7 +19,9 @@ fn test_eliminate_dead_code_keeps_statements_after_fallthrough_try() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -71,7 +73,9 @@ fn test_eliminate_dead_code_sinks_tail_into_try_fallthrough_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/try_pruning.rs b/src/optimize/tests/dce/tries/try_pruning.rs index 1a9be765e6..f7c2a0e016 100644 --- a/src/optimize/tests/dce/tries/try_pruning.rs +++ b/src/optimize/tests/dce/tries/try_pruning.rs @@ -18,7 +18,9 @@ fn test_eliminate_dead_code_drops_statements_after_exhaustive_try_catch() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -82,7 +84,9 @@ fn test_eliminate_dead_code_drops_empty_try_shell_created_by_branch_dce() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -119,7 +123,9 @@ fn test_eliminate_dead_code_keeps_unknown_truthy_switch_entry_before_matching_ca StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -185,7 +191,9 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -267,7 +275,9 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body_from_switc StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -354,7 +364,9 @@ fn test_eliminate_dead_code_ignores_unreachable_switch_throw_path_writes_before_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -445,7 +457,9 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_catch_when_only_non_throw_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/basic_calls.rs b/src/optimize/tests/effects/basic_calls.rs index 33e62e44c6..aae2cfe4aa 100644 --- a/src/optimize/tests/effects/basic_calls.rs +++ b/src/optimize/tests/effects/basic_calls.rs @@ -27,6 +27,22 @@ fn test_effect_analysis_recognizes_pure_builtin_calls() { assert!(!expr_is_observable(&expr)); } +/// Verifies that `eval` is modeled as an observable, throwing dynamic barrier. +#[test] +fn test_effect_analysis_treats_eval_as_dynamic_barrier() { + let expr = Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = 5;")], + }, + Span::dummy(), + ); + + assert!(expr_has_side_effects(&expr)); + assert!(expr_effect(&expr).may_throw); + assert!(expr_is_observable(&expr)); +} + /// Verifies that property accesses (`.`) are pure (no side effects) but may /// throw (uninitialized typed property guard), while array accesses (`[]`) /// are observable and may throw (e.g., undefined index). @@ -62,7 +78,9 @@ fn test_program_function_effects_recognize_pure_user_functions() { StmtKind::FunctionDecl { name: "len3".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -95,7 +113,9 @@ fn test_program_function_effects_propagate_throwing_calls() { StmtKind::FunctionDecl { name: "boom".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -116,7 +136,9 @@ fn test_program_function_effects_propagate_throwing_calls() { StmtKind::FunctionDecl { name: "wrapper".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/callable_aliases/path_merges.rs b/src/optimize/tests/effects/callable_aliases/path_merges.rs index a053121702..73e60341cf 100644 --- a/src/optimize/tests/effects/callable_aliases/path_merges.rs +++ b/src/optimize/tests/effects/callable_aliases/path_merges.rs @@ -21,6 +21,7 @@ fn test_effect_analysis_tracks_pure_iife_expr_calls() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -81,7 +82,9 @@ fn test_program_function_effects_merge_callable_aliases_across_if_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -149,7 +152,9 @@ fn test_program_function_effects_merge_callable_aliases_across_try_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -223,7 +228,9 @@ fn test_program_function_effects_merge_callable_aliases_across_switch_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/callable_aliases/tracking.rs b/src/optimize/tests/effects/callable_aliases/tracking.rs index 91bbb25cc8..066115beb5 100644 --- a/src/optimize/tests/effects/callable_aliases/tracking.rs +++ b/src/optimize/tests/effects/callable_aliases/tracking.rs @@ -19,7 +19,9 @@ fn test_program_function_effects_track_closure_alias_locals() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -31,6 +33,7 @@ fn test_program_function_effects_track_closure_alias_locals() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -86,7 +89,9 @@ fn test_program_function_effects_track_callable_alias_through_ternary() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -147,7 +152,9 @@ fn test_program_function_effects_track_callable_alias_through_match() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -211,7 +218,9 @@ fn test_program_function_effects_track_callable_alias_through_null_coalesce() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -271,7 +280,9 @@ fn test_program_function_effects_track_callable_alias_locals() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/globals.rs b/src/optimize/tests/effects/globals.rs index 7236806fcf..c4cc6f0929 100644 --- a/src/optimize/tests/effects/globals.rs +++ b/src/optimize/tests/effects/globals.rs @@ -18,7 +18,9 @@ fn function_decl(name: &str, body: Vec) -> Stmt { StmtKind::FunctionDecl { name: name.to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/methods.rs b/src/optimize/tests/effects/methods.rs index 54e872155b..7f74f70daa 100644 --- a/src/optimize/tests/effects/methods.rs +++ b/src/optimize/tests/effects/methods.rs @@ -32,7 +32,9 @@ fn test_program_static_method_effects_recognize_pure_static_methods() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -85,7 +87,9 @@ fn test_program_static_method_effects_resolve_self_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -110,7 +114,9 @@ fn test_program_static_method_effects_resolve_self_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -166,7 +172,9 @@ fn test_program_static_method_effects_resolve_parent_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -205,7 +213,9 @@ fn test_program_static_method_effects_resolve_parent_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -259,7 +269,9 @@ fn test_program_private_instance_method_effects_recognize_private_methods() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/fold.rs b/src/optimize/tests/fold.rs index 3ad4538d13..bdbdaf2e64 100644 --- a/src/optimize/tests/fold.rs +++ b/src/optimize/tests/fold.rs @@ -91,6 +91,7 @@ fn test_fold_string_concat_and_property_default() { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::BinaryOp { left: Box::new(Expr::string_lit("hello ")), diff --git a/src/optimize/tests/propagate/collections.rs b/src/optimize/tests/propagate/collections.rs index 7c724b4a42..1774c39949 100644 --- a/src/optimize/tests/propagate/collections.rs +++ b/src/optimize/tests/propagate/collections.rs @@ -320,7 +320,9 @@ fn test_array_fact_survives_by_value_user_call() { StmtKind::FunctionDecl { name: "reader".to_string(), params: vec![("arr".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/invalidation.rs b/src/optimize/tests/propagate/invalidation.rs index 09b2ab0439..e6e802bfb1 100644 --- a/src/optimize/tests/propagate/invalidation.rs +++ b/src/optimize/tests/propagate/invalidation.rs @@ -99,7 +99,9 @@ fn test_user_by_ref_param_invalidates_and_retains() { ("p".to_string(), None, None, true), ("q".to_string(), None, None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -146,7 +148,9 @@ fn test_top_level_globals_guard() { StmtKind::FunctionDecl { name: "gw".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/signatures.rs b/src/optimize/tests/propagate/signatures.rs index 63c11bf9fa..b4a7609bbe 100644 --- a/src/optimize/tests/propagate/signatures.rs +++ b/src/optimize/tests/propagate/signatures.rs @@ -25,7 +25,9 @@ fn function_with_params(name: &str, params: Vec<(&str, bool)>) -> Stmt { .into_iter() .map(|(param, is_ref)| (param.to_string(), None, None, is_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -48,7 +50,9 @@ fn method_with_params(name: &str, params: Vec<(&str, bool)>) -> ClassMethod { .into_iter() .map(|(param, is_ref)| (param.to_string(), None, None, is_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -90,6 +94,7 @@ fn by_ref_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: true, + is_promoted: false, default: None, span: Span::dummy(), attributes: Vec::new(), @@ -113,6 +118,30 @@ fn test_user_function_by_ref_params_collected() { }); } +/// A user function's by-ref variadic parameter is exposed as the trailing +/// by-ref slot used by targeted call invalidation. +#[test] +fn test_user_function_by_ref_variadic_param_collected() { + let mut function = function_with_params("f", Vec::new()); + if let StmtKind::FunctionDecl { + variadic, + variadic_by_ref, + .. + } = &mut function.kind + { + *variadic = Some("items".to_string()); + *variadic_by_ref = true; + } + let sigs = collect_by_ref_signatures(&[function]); + + with_by_ref_signatures(sigs, || { + assert_eq!( + function_by_ref_params("f"), + Some(vec![("items".to_string(), true)]) + ); + }); +} + /// Same-named methods union their by-ref positions across classes and traits /// (dynamic dispatch cannot tell them apart). #[test] diff --git a/src/optimize/tests/propagate/straight_line.rs b/src/optimize/tests/propagate/straight_line.rs index 362262794c..fd120a9364 100644 --- a/src/optimize/tests/propagate/straight_line.rs +++ b/src/optimize/tests/propagate/straight_line.rs @@ -89,6 +89,123 @@ fn test_propagate_constants_invalidates_non_scalar_reassignment() { ); } +/// Tests that `eval` invalidates all prior scalar facts because it can read, +/// write, create, or unset visible variables in the caller scope. +#[test] +fn test_propagate_constants_stops_at_eval_barrier() { + let program = vec![ + Stmt::assign("x", Expr::int_lit(2)), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = 5;")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::echo(Expr::binop(Expr::var("x"), BinOp::Add, Expr::int_lit(1))), + ]; + + let propagated = propagate_constants(program); + + assert_eq!( + propagated[2], + Stmt::echo(Expr::binop(Expr::var("x"), BinOp::Add, Expr::int_lit(1))) + ); +} + +/// Tests that `eval` invalidates by-value closure capture facts inside the +/// closure body because the evaluated fragment can update the closure's local copy. +#[test] +fn test_propagate_constants_stops_at_eval_barrier_in_closure_capture() { + let closure = Expr::new( + ExprKind::Closure { + params: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: None, + body: vec![ + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = $x + 4;")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::new(StmtKind::Return(Some(Expr::var("x"))), Span::dummy()), + ], + is_arrow: false, + is_static: false, + by_ref_return: false, + captures: vec!["x".to_string()], + capture_refs: Vec::new(), + }, + Span::dummy(), + ); + let program = vec![Stmt::assign("x", Expr::int_lit(1)), Stmt::assign("fn", closure)]; + + let propagated = propagate_constants(program); + + let StmtKind::Assign { value, .. } = &propagated[1].kind else { + panic!("expected propagated closure assignment"); + }; + let ExprKind::Closure { body, .. } = &value.kind else { + panic!("expected propagated closure expression"); + }; + assert_eq!( + body[1], + Stmt::new(StmtKind::Return(Some(Expr::var("x"))), Span::dummy()) + ); +} + +/// Tests that a call to a user function with `&...$items` invalidates every +/// positional local passed into the variadic tail. +#[test] +fn test_propagate_constants_invalidates_by_ref_variadic_function_args() { + let program = vec![ + Stmt::new( + StmtKind::FunctionDecl { + name: "f".to_string(), + params: Vec::new(), + param_attributes: Vec::new(), + variadic: Some("items".to_string()), + variadic_by_ref: true, + variadic_type: None, + return_type: None, + by_ref_return: false, + body: Vec::new(), + }, + Span::dummy(), + ), + Stmt::assign("a", Expr::int_lit(1)), + Stmt::assign("b", Expr::int_lit(2)), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("f"), + args: vec![Expr::var("a"), Expr::var("b")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::echo(Expr::binop(Expr::var("a"), BinOp::Add, Expr::var("b"))), + ]; + + let propagated = propagate_constants(program); + + assert_eq!( + propagated[4], + Stmt::echo(Expr::binop(Expr::var("a"), BinOp::Add, Expr::var("b"))) + ); +} + /// Tests that when both branches of a ternary expression are the same constant, /// the resulting assignment is treated as a uniform constant. `base = flag ? 2 : 2` /// always yields `2`, so `base ** 3` folds to `8.0`. @@ -186,7 +303,9 @@ fn test_pure_user_call_keeps_constants() { StmtKind::FunctionDecl { name: "pf".to_string(), params: vec![("a".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -234,7 +353,9 @@ fn test_global_writing_user_call_clears_constants_at_top_level() { StmtKind::FunctionDecl { name: "gw".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/targeted_invalidation.rs b/src/optimize/tests/propagate/targeted_invalidation.rs index 6fed0f0ae1..4bba07e3de 100644 --- a/src/optimize/tests/propagate/targeted_invalidation.rs +++ b/src/optimize/tests/propagate/targeted_invalidation.rs @@ -33,7 +33,9 @@ fn noisy_function(name: &str) -> Stmt { StmtKind::FunctionDecl { name: name.to_string(), params: vec![("p".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -242,6 +244,7 @@ fn test_closure_by_ref_capture_kills_existing_fact() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, body: Vec::new(), diff --git a/src/optimize/tests/propagate/volatility.rs b/src/optimize/tests/propagate/volatility.rs index 71dd94048e..4e610a5a62 100644 --- a/src/optimize/tests/propagate/volatility.rs +++ b/src/optimize/tests/propagate/volatility.rs @@ -21,6 +21,7 @@ fn closure_with_captures(captures: Vec, capture_refs: Vec) -> Ex ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, body: Vec::new(), diff --git a/src/optimize/tests/prune.rs b/src/optimize/tests/prune.rs index f3f0c6c07c..8b1134b106 100644 --- a/src/optimize/tests/prune.rs +++ b/src/optimize/tests/prune.rs @@ -127,7 +127,9 @@ fn test_prune_block_drops_statements_after_return() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -156,7 +158,9 @@ fn test_prune_drops_pure_expr_stmt() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -230,7 +234,9 @@ fn test_prune_block_drops_statements_after_exhaustive_if() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -275,7 +281,9 @@ fn test_prune_block_drops_statements_after_exhaustive_switch() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index d8237a80aa..127c5e5449 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -99,6 +99,8 @@ pub enum ExprKind { Closure { params: Vec<(String, Option, Option, bool)>, variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. variadic_type: Option, return_type: Option, @@ -156,6 +158,7 @@ pub enum ExprKind { required_parent: Name, args: Vec, }, + Clone(Box), PropertyAccess { object: Box, property: String, @@ -186,6 +189,11 @@ pub enum ExprKind { method: String, args: Vec, }, + NullsafeDynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, StaticMethodCall { receiver: StaticReceiver, method: String, diff --git a/src/parser/ast/oop.rs b/src/parser/ast/oop.rs index 548c44a95e..bc88e95fee 100644 --- a/src/parser/ast/oop.rs +++ b/src/parser/ast/oop.rs @@ -62,8 +62,7 @@ pub struct EnumCaseDecl { impl PartialEq for EnumCaseDecl { /// Compares two enum cases by name, value, and attributes; span is not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.value == other.value - && self.attributes == other.attributes + self.name == other.name && self.value == other.value && self.attributes == other.attributes } } @@ -149,6 +148,7 @@ pub struct ClassProperty { pub is_static: bool, pub is_abstract: bool, pub by_ref: bool, + pub is_promoted: bool, pub default: Option, #[allow(dead_code)] // Used for error reporting in future phases pub span: Span, @@ -159,7 +159,8 @@ impl PartialEq for ClassProperty { /// Compares class properties by name, visibility, type, hooks, modifiers, /// by-ref flag, default value, and attributes; span is not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.visibility == other.visibility + self.name == other.name + && self.visibility == other.visibility && self.set_visibility == other.set_visibility && self.type_expr == other.type_expr && self.hooks == other.hooks @@ -168,6 +169,7 @@ impl PartialEq for ClassProperty { && self.is_static == other.is_static && self.is_abstract == other.is_abstract && self.by_ref == other.by_ref + && self.is_promoted == other.is_promoted && self.default == other.default && self.attributes == other.attributes } @@ -211,7 +213,12 @@ pub struct ClassMethod { pub is_final: bool, pub has_body: bool, pub params: Vec<(String, Option, Option, bool)>, + /// Attribute groups declared on each source-order parameter. + /// This vector is parallel to `params`, plus one trailing entry when `variadic` is present. + pub param_attributes: Vec>, pub variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + pub variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each argument /// collected into the variadic is checked against this type. pub variadic_type: Option, @@ -250,7 +257,8 @@ impl PartialEq for ClassMethod { /// Compares class methods by name, visibility, static/abstract/final flags, /// has_body, and attributes; span, params, return_type, and body are not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.visibility == other.visibility + self.name == other.name + && self.visibility == other.visibility && self.is_static == other.is_static && self.is_abstract == other.is_abstract && self.is_final == other.is_final diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index debcf12e45..7686ed501d 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -175,7 +175,12 @@ pub enum StmtKind { FunctionDecl { name: String, params: Vec<(String, Option, Option, bool)>, + /// PHP 8 attribute groups attached to each function parameter, aligned with `params` + /// plus the variadic parameter when present. + param_attributes: Vec>, variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each /// argument collected into the variadic is checked against this type. variadic_type: Option, @@ -227,6 +232,8 @@ pub enum StmtKind { cases: Vec, /// Interfaces the enum implements (`enum E implements Foo, Bar`). implements: Vec, + /// Trait-use declarations flattened into enum method metadata by the checker. + trait_uses: Vec, /// User-declared enum methods (instance and static). Enums dispatch instance methods on /// the case singleton, like a class. methods: Vec, diff --git a/src/parser/expr/assignment_targets.rs b/src/parser/expr/assignment_targets.rs index b067301187..5dbd1c6521 100644 --- a/src/parser/expr/assignment_targets.rs +++ b/src/parser/expr/assignment_targets.rs @@ -316,6 +316,17 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe | ExprKind::NullsafeMethodCall { object, .. } => { collect_assignment_target_dependencies(object, dependencies); } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_assignment_target_dependencies(object, dependencies); + collect_assignment_target_dependencies(method, dependencies); + for arg in args { + collect_assignment_target_dependencies(arg, dependencies); + } + } ExprKind::DynamicPropertyAccess { object, property } | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { collect_assignment_target_dependencies(object, dependencies); @@ -333,6 +344,7 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } @@ -465,6 +477,7 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> boo | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } @@ -532,6 +545,18 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> boo || expr_may_write_dependency(arg, dependencies) }) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_may_write_dependency(object, dependencies) + || expr_may_write_dependency(method, dependencies) + || args.iter().any(|arg| { + expr_contains_dependency(arg, dependencies) + || expr_may_write_dependency(arg, dependencies) + }) + } ExprKind::NewObject { args, .. } | ExprKind::NewScopedObject { args, .. } => { args.iter().any(|arg| { expr_contains_dependency(arg, dependencies) @@ -663,6 +688,7 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool { | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } @@ -756,6 +782,15 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool { expr_contains_equivalent(object, needle) || args.iter().any(|arg| expr_contains_equivalent(arg, needle)) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_equivalent(object, needle) + || expr_contains_equivalent(method, needle) + || args.iter().any(|arg| expr_contains_equivalent(arg, needle)) + } ExprKind::BufferNew { len, .. } => expr_contains_equivalent(len, needle), ExprKind::Yield { key, value } => { key.as_deref() diff --git a/src/parser/expr/pratt.rs b/src/parser/expr/pratt.rs index 447720940f..da7b143a3d 100644 --- a/src/parser/expr/pratt.rs +++ b/src/parser/expr/pratt.rs @@ -151,19 +151,24 @@ fn parse_expr_bp_inner( ObjectMember::Named(member_name) => member_name, ObjectMember::Dynamic(property) => { if *pos < tokens.len() && tokens[*pos].0 == Token::LParen { - if nullsafe { - return Err(CompileError::new( - arrow_span, - "Nullsafe dynamic method calls are not supported yet", - )); - } - // `$obj->$method(args)` reuses the runtime dynamic-dispatch path by - // desugaring to `call_user_func([$obj, $method], ...args)`. *pos += 1; // consume '(' let dynamic_args = crate::parser::expr::parse_args(tokens, pos, arrow_span)?; let arrow_span = crate::parser::expr::span_through_prev_token(tokens, *pos, arrow_span); reject_named_args_in_dynamic_call(&dynamic_args, arrow_span)?; + if nullsafe { + lhs = Expr::new( + ExprKind::NullsafeDynamicMethodCall { + object: Box::new(lhs), + method: Box::new(property), + args: dynamic_args, + }, + arrow_span, + ); + continue; + } + // `$obj->$method(args)` reuses the runtime dynamic-dispatch path by + // desugaring to `call_user_func([$obj, $method], ...args)`. let mut call_args = vec![Expr::new( ExprKind::ArrayLiteral(vec![lhs, property]), arrow_span, diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index 311d764012..1552df5b7d 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -44,6 +44,7 @@ pub(super) fn parse_prefix( Token::At => parse_unary(tokens, pos, span, ExprKind::ErrorSuppress, 35), Token::Print => parse_unary(tokens, pos, span, ExprKind::Print, 7), Token::Throw => parse_unary(tokens, pos, span, ExprKind::Throw, 0), + Token::Clone => parse_unary(tokens, pos, span, ExprKind::Clone, 35), Token::True => parse_simple(tokens, pos, span, ExprKind::BoolLiteral(true)), Token::False => parse_simple(tokens, pos, span, ExprKind::BoolLiteral(false)), Token::Null => parse_simple(tokens, pos, span, ExprKind::Null), @@ -460,6 +461,26 @@ fn parse_array_literal( tokens: &[(Token, Span)], pos: &mut usize, span: Span, +) -> Result { + parse_array_literal_with_terminator(tokens, pos, span, &Token::RBracket, "']'") +} + +/// Parses the legacy `array(...)` literal form after its opening parenthesis. +pub(super) fn parse_legacy_array_literal( + tokens: &[(Token, Span)], + pos: &mut usize, + span: Span, +) -> Result { + parse_array_literal_with_terminator(tokens, pos, span, &Token::RParen, "')'") +} + +/// Parses an array literal body up to `closing`, starting at the opening token. +fn parse_array_literal_with_terminator( + tokens: &[(Token, Span)], + pos: &mut usize, + span: Span, + closing: &Token, + closing_desc: &str, ) -> Result { *pos += 1; let mut elems = Vec::new(); @@ -467,7 +488,8 @@ fn parse_array_literal( let mut is_assoc = false; let mut first = true; let mut next_auto_key = 0i64; - while *pos < tokens.len() && tokens[*pos].0 != Token::RBracket { + let mut auto_key_initialized = false; + while *pos < tokens.len() && tokens[*pos].0 != *closing { if !first { if tokens[*pos].0 != Token::Comma { return Err(CompileError::new( @@ -476,7 +498,7 @@ fn parse_array_literal( )); } *pos += 1; - if *pos < tokens.len() && tokens[*pos].0 == Token::RBracket { + if *pos < tokens.len() && tokens[*pos].0 == *closing { break; } } @@ -498,20 +520,29 @@ fn parse_array_literal( is_assoc = true; *pos += 1; let value = parse_expr(tokens, pos)?; - update_next_auto_key_from_explicit_key(&expr, &mut next_auto_key); + update_next_auto_key_from_explicit_key( + &expr, + &mut next_auto_key, + &mut auto_key_initialized, + ); assoc_elems.push((expr, value)); } else if is_assoc { let key = Expr::new(ExprKind::IntLiteral(next_auto_key), expr.span); assoc_elems.push((key, expr)); next_auto_key += 1; + auto_key_initialized = true; } else { elems.push(expr); next_auto_key += 1; + auto_key_initialized = true; } first = false; } - if *pos >= tokens.len() || tokens[*pos].0 != Token::RBracket { - return Err(CompileError::new(span, "Expected ']'")); + if *pos >= tokens.len() || tokens[*pos].0 != *closing { + return Err(CompileError::new( + span, + &format!("Expected {closing_desc}"), + )); } *pos += 1; if is_assoc { @@ -538,10 +569,65 @@ fn promote_indexed_array_items_to_assoc( } /// Advances the automatic integer key cursor after a statically known integer key. -fn update_next_auto_key_from_explicit_key(key: &Expr, next_auto_key: &mut i64) { - if let ExprKind::IntLiteral(value) = &key.kind { - if *value >= *next_auto_key { - *next_auto_key = *value + 1; +/// +/// The first integer-like key seeds the cursor unconditionally so a leading +/// negative key continues from there (PHP 8.3 semantics); later keys only +/// raise it. +fn update_next_auto_key_from_explicit_key( + key: &Expr, + next_auto_key: &mut i64, + auto_key_initialized: &mut bool, +) { + if let Some(value) = explicit_integer_array_key(key) { + let candidate = value.saturating_add(1); + if !*auto_key_initialized || candidate > *next_auto_key { + *next_auto_key = candidate; } + *auto_key_initialized = true; + } +} + +/// Returns the integer key PHP assigns to an explicit array key literal, +/// covering the int-normalizing forms: bools, integral floats, canonical +/// numeric strings, and negated numeric literals. +fn explicit_integer_array_key(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::FloatLiteral(value) => integral_float_array_key(*value), + ExprKind::StringLiteral(value) => php_integer_string_array_key(value), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg(), + ExprKind::FloatLiteral(value) => integral_float_array_key(-*value), + _ => None, + }, + _ => None, + } +} + +/// Returns the integer key for a float literal PHP casts without truncation. +fn integral_float_array_key(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + if value < i64::MIN as f64 || value >= i64::MAX as f64 { + return None; + } + Some(value as i64) +} + +/// Returns the integer key for a canonical PHP integer string ("0", no +/// leading zeros, no "-0"); other strings stay string keys. +fn php_integer_string_array_key(value: &str) -> Option { + if value == "0" { + return value.parse().ok(); + } + let digits = value.strip_prefix('-').unwrap_or(value); + if digits.is_empty() + || digits.starts_with('0') + || !digits.bytes().all(|byte| byte.is_ascii_digit()) + { + return None; } + value.parse().ok() } diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index d79c3db462..587010846d 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -163,7 +163,8 @@ pub(super) fn parse_closure( return Err(CompileError::new(span, "Unexpected token: Function")); } *pos = after_fn + 1; - let (params, variadic, variadic_type) = parse_closure_params(tokens, pos, span)?; + let (params, variadic, variadic_by_ref, variadic_type) = + parse_closure_params(tokens, pos, span)?; let mut captures = Vec::new(); let mut capture_refs = Vec::new(); if *pos < tokens.len() && tokens[*pos].0 == Token::Use { @@ -218,6 +219,7 @@ pub(super) fn parse_closure( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -247,7 +249,8 @@ pub(super) fn parse_arrow_closure( return Err(CompileError::new(span, "Expected '(' after 'fn'")); } *pos += 1; - let (params, variadic, variadic_type) = parse_closure_params(tokens, pos, span)?; + let (params, variadic, variadic_by_ref, variadic_type) = + parse_closure_params(tokens, pos, span)?; let return_type = parse_optional_closure_return_type(tokens, pos, span)?; if *pos >= tokens.len() || tokens[*pos].0 != Token::DoubleArrow { return Err(CompileError::new( @@ -263,6 +266,7 @@ pub(super) fn parse_arrow_closure( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -341,6 +345,7 @@ fn collect_arrow_expr_captures( | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -459,6 +464,17 @@ fn collect_arrow_expr_captures( collect_arrow_expr_captures(arg, bound, seen, captures); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_arrow_expr_captures(object, bound, seen, captures); + collect_arrow_expr_captures(method, bound, seen, captures); + for arg in args { + collect_arrow_expr_captures(arg, bound, seen, captures); + } + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { collect_arrow_expr_captures(object, bound, seen, captures); } @@ -514,12 +530,14 @@ fn parse_closure_params( ( Vec<(String, Option, Option, bool)>, Option, + bool, Option, ), CompileError, > { let mut params = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; while *pos < tokens.len() && tokens[*pos].0 != Token::RParen { if !params.is_empty() || variadic.is_some() { @@ -561,6 +579,7 @@ fn parse_closure_params( match tokens.get(*pos).map(|(token, _)| token) { Some(Token::Variable(name)) => { variadic = Some(name.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; *pos += 1; } @@ -587,7 +606,7 @@ fn parse_closure_params( return Err(CompileError::new(span, "Expected ')' after parameters")); } *pos += 1; - Ok((params, variadic, variadic_type)) + Ok((params, variadic, variadic_by_ref, variadic_type)) } /// Parses a named expression that could be a constant reference, function call, buffer_new, ptr_cast, or static/class method access. @@ -599,6 +618,15 @@ pub(super) fn parse_named_expr( span: Span, ) -> Result { let name = parse_name(tokens, pos, span, "Expected name")?; + // PHP's legacy `array(...)` construct is an array literal, not a call: + // its elements may use `key => value` pairs that call arguments reject. + if name.parts.len() == 1 + && name.parts[0].eq_ignore_ascii_case("array") + && *pos < tokens.len() + && tokens[*pos].0 == Token::LParen + { + return super::prefix::parse_legacy_array_literal(tokens, pos, span); + } if name.parts.len() == 1 && name.parts[0] == "buffer_new" && *pos < tokens.len() diff --git a/src/parser/keyword_name.rs b/src/parser/keyword_name.rs index 5d95d0351f..12d64d0724 100644 --- a/src/parser/keyword_name.rs +++ b/src/parser/keyword_name.rs @@ -74,6 +74,7 @@ pub(crate) fn bareword_name_from_token(token: &Token) -> Option { Token::Class => Some("class".to_string()), Token::Enum => Some("enum".to_string()), Token::New => Some("new".to_string()), + Token::Clone => Some("clone".to_string()), Token::Public => Some("public".to_string()), Token::Protected => Some("protected".to_string()), Token::Private => Some("private".to_string()), diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 6befd9cda1..952f8ccca8 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -152,6 +152,7 @@ fn parse_stmt_dispatch( | Token::Parent | Token::Backslash | Token::Question + | Token::New | Token::LParen | Token::Match => { if matches!(&tokens[*pos].0, Token::Identifier(name) if name.eq_ignore_ascii_case("list")) diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index caa280b3da..530cf16ed5 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -12,8 +12,8 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::names::{property_hook_get_method, property_hook_set_method}; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, EnumCaseDecl, PropertyHooks, Stmt, StmtKind, TraitUse, - TypeExpr, Visibility, + ClassConst, ClassMethod, ClassProperty, EnumCaseDecl, Expr, ExprKind, PropertyHooks, Stmt, + StmtKind, TraitUse, TypeExpr, Visibility, }; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -228,8 +228,8 @@ pub(in crate::parser::stmt) fn parse_class_like_body( )); } *pos += 1; // consume `const` - // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, - // which is reserved for the `Foo::class` name fetch. + // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, + // which is reserved for the `Foo::class` name fetch. let const_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Class) => { return Err(CompileError::new( @@ -324,7 +324,10 @@ pub(in crate::parser::stmt) fn parse_class_like_body( if modifiers.is_abstract && default.is_some() { return Err(CompileError::new( member_span, - &format!("Abstract property ${} cannot have a default value", prop_name), + &format!( + "Abstract property ${} cannot have a default value", + prop_name + ), )); } if modifiers.is_abstract && !hooks.any() { @@ -394,6 +397,7 @@ pub(in crate::parser::stmt) fn parse_class_like_body( is_static: modifiers.is_static, is_abstract: modifiers.is_abstract, by_ref: false, + is_promoted: false, default, span: member_span, attributes: member_attributes, @@ -421,7 +425,10 @@ fn append_promoted_properties( promoted_properties: Vec, ) -> Result<(), CompileError> { for promoted in promoted_properties { - if properties.iter().any(|property| property.name == promoted.name) { + if properties + .iter() + .any(|property| property.name == promoted.name) + { return Err(CompileError::new( promoted.span, &format!("Cannot redeclare promoted property ${}", promoted.name), @@ -583,8 +590,15 @@ fn parse_class_like_method( &Token::LParen, "Expected '(' after method name", )?; - let (params, variadic, variadic_type, promoted_properties, promoted_assignments) = - parse_method_params(tokens, pos, span, &method_name)?; + let ( + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + promoted_properties, + promoted_assignments, + ) = parse_method_params(tokens, pos, span, &method_name)?; expect_token(tokens, pos, &Token::RParen, "Expected ')'")?; // Parse optional return type: `: TypeExpr` let return_type = if *pos < tokens.len() && tokens[*pos].0 == Token::Colon { @@ -618,22 +632,27 @@ fn parse_class_like_method( } else { promoted_assignments.into_iter().chain(body).collect() }; - Ok((ClassMethod { - name: method_name, - visibility, - is_static, - is_abstract, - is_final, - has_body, - params, - variadic, - variadic_type, - return_type, - by_ref_return, - body, - span, - attributes: Vec::new(), - }, promoted_properties)) + Ok(( + ClassMethod { + name: method_name, + visibility, + is_static, + is_abstract, + is_final, + has_body, + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + return_type, + by_ref_return, + body, + span, + attributes: Vec::new(), + }, + promoted_properties, + )) } /// Parses the body of an `interface` declaration. @@ -663,8 +682,8 @@ fn parse_interface_body( } if tokens[*pos].0 == Token::Const { *pos += 1; // consume `const` - // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, - // which is reserved for the `Foo::class` name fetch. + // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, + // which is reserved for the `Foo::class` name fetch. let const_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Class) => { return Err(CompileError::new( @@ -730,7 +749,10 @@ fn parse_interface_body( } let prop_name = prop_name.clone(); *pos += 1; - if properties.iter().any(|property: &ClassProperty| property.name == prop_name) { + if properties + .iter() + .any(|property: &ClassProperty| property.name == prop_name) + { return Err(CompileError::new( member_span, &format!("Cannot redeclare interface property ${}", prop_name), @@ -779,6 +801,7 @@ fn parse_interface_body( is_static: false, is_abstract: true, by_ref: false, + is_promoted: false, default: None, span: member_span, attributes: member_attributes, @@ -855,12 +878,7 @@ fn parse_property_hooks( }; let hook_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Identifier(name)) => name.clone(), - _ => { - return Err(CompileError::new( - hook_span, - "Expected property hook name", - )) - } + _ => return Err(CompileError::new(hook_span, "Expected property hook name")), }; *pos += 1; let is_get = hook_name.eq_ignore_ascii_case("get"); @@ -913,10 +931,14 @@ fn parse_property_hooks( if is_get { Some(vec![Stmt::new(StmtKind::Return(Some(expr)), hook_span)]) } else { - return Err(CompileError::new( + Some(vec![Stmt::new( + StmtKind::PropertyAssign { + object: Box::new(Expr::new(ExprKind::This, hook_span)), + property: prop_name.to_string(), + value: expr, + }, hook_span, - "Short `set => expr` hooks require a backed property; use a block `set { ... }`", - )); + )]) } } Some(Token::LBrace) => Some(parse_block(tokens, pos)?), @@ -943,7 +965,9 @@ fn parse_property_hooks( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: prop_type.cloned(), by_ref_return: get_by_ref, @@ -972,7 +996,9 @@ fn parse_property_hooks( is_final: false, has_body: true, params: vec![(set_param, prop_type.cloned(), None, false)], + param_attributes: vec![Vec::new()], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), by_ref_return: false, @@ -991,7 +1017,10 @@ fn parse_property_hooks( "Expected '}' at end of property hook block", )?; if !hooks.any() { - return Err(CompileError::new(span, "Expected property hook declaration")); + return Err(CompileError::new( + span, + "Expected property hook declaration", + )); } Ok((hooks, accessors)) } diff --git a/src/parser/stmt/oop/declarations.rs b/src/parser/stmt/oop/declarations.rs index bb1be87f66..b43a176d61 100644 --- a/src/parser/stmt/oop/declarations.rs +++ b/src/parser/stmt/oop/declarations.rs @@ -225,12 +225,6 @@ pub(in crate::parser::stmt) fn parse_enum_decl( if !properties.is_empty() { return Err(CompileError::new(span, "Enums cannot declare properties")); } - if !trait_uses.is_empty() { - return Err(CompileError::new( - span, - "Enums using traits are not supported yet", - )); - } Ok(Stmt::new( StmtKind::EnumDecl { @@ -238,6 +232,7 @@ pub(in crate::parser::stmt) fn parse_enum_decl( backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/parser/stmt/oop/method_params.rs b/src/parser/stmt/oop/method_params.rs index 475c859121..34f07627d9 100644 --- a/src/parser/stmt/oop/method_params.rs +++ b/src/parser/stmt/oop/method_params.rs @@ -11,18 +11,21 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::parser::ast::{ - ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, Visibility, + AttributeGroup, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, + Visibility, }; use crate::parser::expr::parse_expr; use crate::span::Span; -use super::super::params::{looks_like_typed_param, parse_type_expr}; use super::super::expect_token; +use super::super::params::{looks_like_typed_param, parse_type_expr}; type MethodParam = (String, Option, Option, bool); type ParsedMethodParams = ( Vec, + Vec>, Option, + bool, Option, Vec, Vec, @@ -44,7 +47,9 @@ pub(super) fn parse_method_params( method_name: &str, ) -> Result { let mut params = Vec::new(); + let mut param_attributes = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; let mut promoted_properties = Vec::new(); let mut promoted_assignments = Vec::new(); @@ -64,7 +69,7 @@ pub(super) fn parse_method_params( } // PHP 8.0 parameter attributes — also covers attributes preceding a // promoted-property modifier such as `#[Inject] public Foo $f`. - crate::parser::consume_attribute_lists(tokens, pos)?; + let attributes = crate::parser::parse_attribute_lists(tokens, pos)?; if variadic.is_some() { return Err(CompileError::new( span, @@ -105,7 +110,9 @@ pub(super) fn parse_method_params( match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => { variadic = Some(n.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; + param_attributes.push(attributes); *pos += 1; } _ => return Err(CompileError::new(span, "Expected variable after '...'")), @@ -143,21 +150,31 @@ pub(super) fn parse_method_params( is_static: false, is_abstract: false, by_ref: is_ref, + is_promoted: true, // PHP keeps constructor-promotion defaults on the parameter, // not on the promoted property's default metadata. default: None, span: property_span, - attributes: Vec::new(), + attributes: attributes.clone(), }); promoted_assignments.push(promoted_property_assignment(&n, param_span)); } + param_attributes.push(attributes); params.push((n, type_ann, default, is_ref)); } _ => return Err(CompileError::new(span, "Expected parameter variable")), } } - Ok((params, variadic, variadic_type, promoted_properties, promoted_assignments)) + Ok(( + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + promoted_properties, + promoted_assignments, + )) } /// Scans the token stream for visibility modifiers (`public`/`protected`/`private`) @@ -176,7 +193,10 @@ fn parse_promoted_param_modifiers( match tokens.get(*pos).map(|(t, s)| (t, *s)) { Some((Token::Public, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Public); @@ -184,7 +204,10 @@ fn parse_promoted_param_modifiers( } Some((Token::Protected, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Protected); @@ -192,7 +215,10 @@ fn parse_promoted_param_modifiers( } Some((Token::Private, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Private); diff --git a/src/parser/stmt/params.rs b/src/parser/stmt/params.rs index 6dc4d27f1f..bcc32ade5c 100644 --- a/src/parser/stmt/params.rs +++ b/src/parser/stmt/params.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::names::Name; -use crate::parser::ast::{Expr, Stmt, StmtKind, TypeExpr}; +use crate::parser::ast::{AttributeGroup, Expr, Stmt, StmtKind, TypeExpr}; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -45,7 +45,8 @@ pub(super) fn parse_function_decl( &Token::LParen, "Expected '(' after function name", )?; - let (params, variadic, variadic_type) = parse_params(tokens, pos, span)?; + let (params, param_attributes, variadic, variadic_by_ref, variadic_type) = + parse_params(tokens, pos, span)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after parameters")?; // Parse optional return type: `: TypeExpr` @@ -62,7 +63,9 @@ pub(super) fn parse_function_decl( StmtKind::FunctionDecl { name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -346,13 +349,17 @@ pub(super) fn parse_params( ) -> Result< ( Vec<(String, Option, Option, bool)>, + Vec>, Option, + bool, Option, ), CompileError, > { let mut params = Vec::new(); + let mut param_attributes = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; while *pos < tokens.len() && tokens[*pos].0 != Token::RParen { if !params.is_empty() || variadic.is_some() { @@ -368,7 +375,7 @@ pub(super) fn parse_params( } } // PHP 8.0 parameter attributes (`function f(#[Sensitive] $s)`). - crate::parser::consume_attribute_lists(tokens, pos)?; + let attributes = crate::parser::parse_attribute_lists(tokens, pos)?; if variadic.is_some() { return Err(CompileError::new( span, @@ -394,7 +401,9 @@ pub(super) fn parse_params( match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => { variadic = Some(n.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; + param_attributes.push(attributes); *pos += 1; } _ => return Err(CompileError::new(span, "Expected variable after '...'")), @@ -411,12 +420,19 @@ pub(super) fn parse_params( } else { None }; + param_attributes.push(attributes); params.push((n, type_ann, default, is_ref)); } _ => return Err(CompileError::new(span, "Expected parameter variable")), } } - Ok((params, variadic, variadic_type)) + Ok(( + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + )) } /// Parses a comma-separated list of `Name`s until a token that does not start a name is diff --git a/src/pdo_prelude/detect.rs b/src/pdo_prelude/detect.rs index d4a141043b..3505ce0406 100644 --- a/src/pdo_prelude/detect.rs +++ b/src/pdo_prelude/detect.rs @@ -185,6 +185,7 @@ fn expr_refs_pdo(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -272,6 +273,11 @@ fn expr_refs_pdo(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_pdo(object) || args.iter().any(expr_refs_pdo) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_pdo(object) || expr_refs_pdo(method) || args.iter().any(expr_refs_pdo), ExprKind::StaticMethodCall { receiver, args, .. } => { receiver_refs_pdo(receiver) || args.iter().any(expr_refs_pdo) } diff --git a/src/pipeline.rs b/src/pipeline.rs index 0d9f77f251..75f8d7f5dd 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -258,7 +258,12 @@ pub(crate) fn compile(config: CliConfig) { if emit_ir { let phase_started = Instant::now(); - let mut module = match ir_lower::lower_program(&ast, &check_result, target) { + let mut module = match ir_lower::lower_program_with_source_path( + &ast, + &check_result, + target, + Path::new(filename), + ) { Ok(module) => module, Err(err) => { eprintln!("EIR lowering error: {}", err); @@ -282,7 +287,12 @@ pub(crate) fn compile(config: CliConfig) { } let phase_started = Instant::now(); - let mut ir_module = match ir_lower::lower_program(&ast, &check_result, target) { + let mut ir_module = match ir_lower::lower_program_with_source_path( + &ast, + &check_result, + target, + Path::new(filename), + ) { Ok(module) => module, Err(err) => { eprintln!("EIR lowering error: {}", err); diff --git a/src/resolver/contains.rs b/src/resolver/contains.rs index 64888047aa..fd1d3d8ce0 100644 --- a/src/resolver/contains.rs +++ b/src/resolver/contains.rs @@ -144,6 +144,7 @@ fn expr_has_includes(expr: &Expr) -> bool { | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Spread(value) @@ -224,6 +225,15 @@ fn expr_has_includes(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_includes(object) || args.iter().any(expr_has_includes) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_includes(object) + || expr_has_includes(method) + || args.iter().any(expr_has_includes) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_has_includes(object) } diff --git a/src/resolver/discovery/exprs.rs b/src/resolver/discovery/exprs.rs index 29f5895a8a..47d0c0916a 100644 --- a/src/resolver/discovery/exprs.rs +++ b/src/resolver/discovery/exprs.rs @@ -69,6 +69,7 @@ pub(super) fn discover_expr( | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Spread(value) @@ -188,6 +189,15 @@ pub(super) fn discover_expr( discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; discover_exprs(args, base_dir, loaded_paths, include_chain, state, output)?; } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; + discover_expr(method, base_dir, loaded_paths, include_chain, state, output)?; + discover_exprs(args, base_dir, loaded_paths, include_chain, state, output)?; + } ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. }) => { discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; } diff --git a/src/resolver/engine.rs b/src/resolver/engine.rs index 67670e1728..c3276b8491 100644 --- a/src/resolver/engine.rs +++ b/src/resolver/engine.rs @@ -398,7 +398,17 @@ pub(super) fn resolve_stmts( stmt.span, )); } - StmtKind::FunctionDecl { name, params, variadic, variadic_type, return_type, by_ref_return, body } => { + StmtKind::FunctionDecl { + name, + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + return_type, + by_ref_return, + body, + } => { let body = resolve_isolated( body.clone(), base_dir, @@ -411,7 +421,9 @@ pub(super) fn resolve_stmts( StmtKind::FunctionDecl { name: name.clone(), params: params.clone(), + param_attributes: param_attributes.clone(), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), by_ref_return: *by_ref_return, diff --git a/src/resolver/exprs.rs b/src/resolver/exprs.rs index 44eb7595b5..204177a209 100644 --- a/src/resolver/exprs.rs +++ b/src/resolver/exprs.rs @@ -223,6 +223,7 @@ pub(super) fn resolve_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -234,6 +235,7 @@ pub(super) fn resolve_expr( } => ExprKind::Closure { params: resolve_params(params, base_dir, declared_once, include_chain, state, function_variants)?, variadic, + variadic_by_ref, variadic_type, return_type, body: resolve_isolated( @@ -373,6 +375,29 @@ pub(super) fn resolve_expr( method, args: resolve_exprs(args, base_dir, declared_once, include_chain, state, function_variants)?, }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(resolve_expr( + *object, + base_dir, + declared_once, + include_chain, + state, + function_variants, + )?), + method: Box::new(resolve_expr( + *method, + base_dir, + declared_once, + include_chain, + state, + function_variants, + )?), + args: resolve_exprs(args, base_dir, declared_once, include_chain, state, function_variants)?, + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/resolver/include_path.rs b/src/resolver/include_path.rs index bab4f557f4..04a1bc393a 100644 --- a/src/resolver/include_path.rs +++ b/src/resolver/include_path.rs @@ -83,6 +83,9 @@ fn runtime_dynamic_include_path_detail(expr: &Expr) -> Option { ExprKind::MethodCall { method, .. } | ExprKind::NullsafeMethodCall { method, .. } => { Some(format!("method call `->{}` is resolved at runtime", method)) } + ExprKind::NullsafeDynamicMethodCall { .. } => { + Some("dynamic method call is resolved at runtime".to_string()) + } ExprKind::StaticMethodCall { method, .. } => { Some(format!("static method call `::{}` is resolved at runtime", method)) } diff --git a/src/resolver/stmt_exprs.rs b/src/resolver/stmt_exprs.rs index 6630028c9c..61cfd37921 100644 --- a/src/resolver/stmt_exprs.rs +++ b/src/resolver/stmt_exprs.rs @@ -372,7 +372,9 @@ pub(super) fn resolve_stmt_exprs( by_ref_return, name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -387,7 +389,9 @@ pub(super) fn resolve_stmt_exprs( state, function_variants, )?, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -488,12 +492,14 @@ pub(super) fn resolve_stmt_exprs( backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods: resolve_method_exprs( methods, base_dir, diff --git a/src/types/call_args/mod.rs b/src/types/call_args/mod.rs index b7f4243a9a..903b73ce80 100644 --- a/src/types/call_args/mod.rs +++ b/src/types/call_args/mod.rs @@ -18,5 +18,7 @@ pub(crate) use matching::{named_param_index, regular_param_count}; pub(crate) use plan::{ CallArgPlan, CallArgPlanError, PlannedRegularArg, PlannedSourceValue, SpreadBoundsCheck, }; -pub(crate) use planner::plan_call_args_with_regular_param_count_and_assoc_spreads; +pub(crate) use planner::{ + plan_call_args, plan_call_args_with_regular_param_count_and_assoc_spreads, +}; pub(crate) use static_spread::{expand_static_assoc_spread_args, has_named_args}; diff --git a/src/types/call_args/plan.rs b/src/types/call_args/plan.rs index d068f7e7b9..99cb91f111 100644 --- a/src/types/call_args/plan.rs +++ b/src/types/call_args/plan.rs @@ -352,6 +352,8 @@ mod tests { .collect(); FunctionSig { params, + param_type_exprs: vec![None; 3], + param_attributes: Vec::new(), defaults, return_type: PhpType::Int, declared_return: true, diff --git a/src/types/call_args/planner.rs b/src/types/call_args/planner.rs index 7db5da0532..f881a66c62 100644 --- a/src/types/call_args/planner.rs +++ b/src/types/call_args/planner.rs @@ -13,7 +13,6 @@ use crate::parser::ast::{Expr, ExprKind}; use crate::span::Span; use crate::types::FunctionSig; -#[cfg(test)] use super::matching::regular_param_count; use super::matching::{NamedParamMatch, NamedParamTracker}; use super::plan::{ @@ -28,7 +27,6 @@ use super::static_spread::{expand_static_assoc_spread_args_with_origins, Expande /// - `trim_trailing_defaults`: when `true`, elide trailing default-only slots from the plan. /// - `allow_unknown_named_variadic`: when `true`, unknown named args are allowed and routed /// to the variadic parameter if the signature is variadic. -#[cfg(test)] pub(crate) fn plan_call_args( sig: &FunctionSig, args: &[Expr], @@ -50,7 +48,6 @@ pub(crate) fn plan_call_args( /// supplied `regular_param_count` rather than inferring it from the signature. /// Use this when the caller knows the visible parameter count (e.g., internal /// signatures with hidden implementation parameters). -#[cfg(test)] pub(crate) fn plan_call_args_with_regular_param_count( sig: &FunctionSig, args: &[Expr], diff --git a/src/types/checker/builtin_enums.rs b/src/types/checker/builtin_enums.rs index 8103e24c01..1015bb9370 100644 --- a/src/types/checker/builtin_enums.rs +++ b/src/types/checker/builtin_enums.rs @@ -11,12 +11,13 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{Program, Stmt, StmtKind}; -use crate::types::EnumCaseInfo; +use crate::types::{EnumCaseInfo, EnumCaseValue, PhpType}; use super::schema::insert_enum_metadata; use super::Checker; const SORT_DIRECTION: &str = "SortDirection"; +const PROPERTY_HOOK_TYPE: &str = "PropertyHookType"; /// Injects all builtin enum declarations into the checker. /// @@ -35,15 +36,50 @@ pub(crate) fn inject_builtin_enums( EnumCaseInfo { name: "Ascending".to_string(), value: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), }, EnumCaseInfo { name: "Descending".to_string(), value: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), }, ], &[], &[], &[], + &[], + &[], + crate::span::Span::dummy(), + checker, + next_class_id, + )?; + + ensure_builtin_enum_name_available(program, checker, PROPERTY_HOOK_TYPE)?; + insert_enum_metadata( + PROPERTY_HOOK_TYPE, + Some(PhpType::Str), + vec![ + EnumCaseInfo { + name: "Get".to_string(), + value: Some(EnumCaseValue::Str("get".to_string())), + attribute_names: Vec::new(), + attribute_args: Vec::new(), + }, + EnumCaseInfo { + name: "Set".to_string(), + value: Some(EnumCaseValue::Str("set".to_string())), + attribute_names: Vec::new(), + attribute_args: Vec::new(), + }, + ], + &[], + &[], + &[], + &[], + &[], + crate::span::Span::dummy(), checker, next_class_id, ) diff --git a/src/types/checker/builtin_interfaces.rs b/src/types/checker/builtin_interfaces.rs index 4c38bfa961..ba6d4ca0a1 100644 --- a/src/types/checker/builtin_interfaces.rs +++ b/src/types/checker/builtin_interfaces.rs @@ -330,7 +330,9 @@ fn builtin_interface_method(name: &str, return_type: TypeExpr) -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -361,7 +363,9 @@ fn builtin_interface_method_with_params( is_final: false, has_body: false, params, + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, diff --git a/src/types/checker/builtin_iterators.rs b/src/types/checker/builtin_iterators.rs index 7c39046636..e94e1a1189 100644 --- a/src/types/checker/builtin_iterators.rs +++ b/src/types/checker/builtin_iterators.rs @@ -53,6 +53,7 @@ pub(crate) fn inject_builtin_iterators( "Generator".to_string(), FlattenedClass { name: "Generator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, @@ -72,6 +73,7 @@ pub(crate) fn inject_builtin_iterators( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -91,7 +93,9 @@ fn stub_method_returning_null(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -117,7 +121,9 @@ fn stub_method_returning_null_with_param(name: &str, param: &str) -> ClassMethod is_final: false, has_body: true, params: vec![(param.to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -141,7 +147,9 @@ fn stub_method_returning_false(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Bool), by_ref_return: false, @@ -168,7 +176,9 @@ fn stub_void_method(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), by_ref_return: false, diff --git a/src/types/checker/builtin_json.rs b/src/types/checker/builtin_json.rs index 9bdda15567..a576504241 100644 --- a/src/types/checker/builtin_json.rs +++ b/src/types/checker/builtin_json.rs @@ -71,7 +71,9 @@ fn json_serialize_method() -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, diff --git a/src/types/checker/builtin_spl_classes/append.rs b/src/types/checker/builtin_spl_classes/append.rs index c8dccbb5e8..0c13ba0258 100644 --- a/src/types/checker/builtin_spl_classes/append.rs +++ b/src/types/checker/builtin_spl_classes/append.rs @@ -25,6 +25,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "AppendIterator".to_string(), FlattenedClass { name: "AppendIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -35,6 +36,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); append_array_iterator::insert_class(class_map); diff --git a/src/types/checker/builtin_spl_classes/append_array_iterator.rs b/src/types/checker/builtin_spl_classes/append_array_iterator.rs index d10d432887..b97e2d14c4 100644 --- a/src/types/checker/builtin_spl_classes/append_array_iterator.rs +++ b/src/types/checker/builtin_spl_classes/append_array_iterator.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "__ElephcAppendIteratorArrayIterator".to_string(), FlattenedClass { name: "__ElephcAppendIteratorArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("ArrayIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -32,6 +33,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/caching.rs b/src/types/checker/builtin_spl_classes/caching.rs index eca7c0fa4f..857d3f2300 100644 --- a/src/types/checker/builtin_spl_classes/caching.rs +++ b/src/types/checker/builtin_spl_classes/caching.rs @@ -23,6 +23,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "CachingIterator".to_string(), FlattenedClass { name: "CachingIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: vec![ "ArrayAccess".to_string(), @@ -37,6 +38,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: caching_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/common.rs b/src/types/checker/builtin_spl_classes/common.rs index c0c16f06aa..3ba9138b42 100644 --- a/src/types/checker/builtin_spl_classes/common.rs +++ b/src/types/checker/builtin_spl_classes/common.rs @@ -78,7 +78,9 @@ pub(super) fn class_method_with_body( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -135,6 +137,7 @@ pub(super) fn storage_property_with_visibility( is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default, span: crate::span::Span::dummy(), attributes: Vec::new(), diff --git a/src/types/checker/builtin_spl_classes/containers.rs b/src/types/checker/builtin_spl_classes/containers.rs index e22e097d02..9ce2afc478 100644 --- a/src/types/checker/builtin_spl_classes/containers.rs +++ b/src/types/checker/builtin_spl_classes/containers.rs @@ -24,6 +24,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplDoublyLinkedList".to_string(), FlattenedClass { name: "SplDoublyLinkedList".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), @@ -38,6 +39,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_doubly_linked_list_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -45,6 +47,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplStack".to_string(), FlattenedClass { name: "SplStack".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplDoublyLinkedList".to_string()), implements: Vec::new(), is_abstract: false, @@ -55,6 +58,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -62,6 +66,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplQueue".to_string(), FlattenedClass { name: "SplQueue".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplDoublyLinkedList".to_string()), implements: Vec::new(), is_abstract: false, @@ -75,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -82,6 +88,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFixedArray".to_string(), FlattenedClass { name: "SplFixedArray".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "IteratorAggregate".to_string(), @@ -97,6 +104,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -104,6 +112,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "InternalIterator".to_string(), FlattenedClass { name: "InternalIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, @@ -114,6 +123,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/filesystem.rs b/src/types/checker/builtin_spl_classes/filesystem.rs index 17fefe2930..b241589875 100644 --- a/src/types/checker/builtin_spl_classes/filesystem.rs +++ b/src/types/checker/builtin_spl_classes/filesystem.rs @@ -42,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFileInfo".to_string(), FlattenedClass { name: "SplFileInfo".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Stringable".to_string()], is_abstract: false, @@ -52,6 +53,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -59,6 +61,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFileObject".to_string(), FlattenedClass { name: "SplFileObject".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: vec!["RecursiveIterator".to_string(), "SeekableIterator".to_string()], is_abstract: false, @@ -69,6 +72,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_file_object_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -76,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplTempFileObject".to_string(), FlattenedClass { name: "SplTempFileObject".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileObject".to_string()), implements: Vec::new(), is_abstract: false, @@ -86,6 +91,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_file_object_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -93,6 +99,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "DirectoryIterator".to_string(), FlattenedClass { name: "DirectoryIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: vec!["Iterator".to_string(), "SeekableIterator".to_string()], is_abstract: false, @@ -103,6 +110,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -110,6 +118,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "FilesystemIterator".to_string(), FlattenedClass { name: "FilesystemIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("DirectoryIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -120,6 +129,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -127,6 +137,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "GlobIterator".to_string(), FlattenedClass { name: "GlobIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilesystemIterator".to_string()), implements: vec!["Countable".to_string()], is_abstract: false, @@ -137,6 +148,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -144,6 +156,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveDirectoryIterator".to_string(), FlattenedClass { name: "RecursiveDirectoryIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilesystemIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -154,6 +167,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -161,6 +175,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveCachingIterator".to_string(), FlattenedClass { name: "RecursiveCachingIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("CachingIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -171,6 +186,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/filters.rs b/src/types/checker/builtin_spl_classes/filters.rs index 31b10fdc44..a0bdb2b76b 100644 --- a/src/types/checker/builtin_spl_classes/filters.rs +++ b/src/types/checker/builtin_spl_classes/filters.rs @@ -23,6 +23,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "FilterIterator".to_string(), FlattenedClass { name: "FilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: true, @@ -33,6 +34,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -40,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "CallbackFilterIterator".to_string(), FlattenedClass { name: "CallbackFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -50,6 +53,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/forwarding.rs b/src/types/checker/builtin_spl_classes/forwarding.rs index 36013e4036..d630888c6b 100644 --- a/src/types/checker/builtin_spl_classes/forwarding.rs +++ b/src/types/checker/builtin_spl_classes/forwarding.rs @@ -15,6 +15,7 @@ use crate::parser::ast::{BinOp, ClassMethod, ClassProperty, Expr, Stmt, TypeExpr use crate::types::traits::FlattenedClass; use super::common::*; +use super::recursive_array::assume_recursive_iterator_expr; /// Inserts classes into the supplied builtin metadata registry. pub(super) fn insert_classes(class_map: &mut HashMap) { @@ -22,6 +23,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "IteratorIterator".to_string(), FlattenedClass { name: "IteratorIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["OuterIterator".to_string()], is_abstract: false, @@ -32,6 +34,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -39,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "LimitIterator".to_string(), FlattenedClass { name: "LimitIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -49,6 +53,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -56,6 +61,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "NoRewindIterator".to_string(), FlattenedClass { name: "NoRewindIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -66,6 +72,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -73,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "InfiniteIterator".to_string(), FlattenedClass { name: "InfiniteIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -83,6 +91,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -210,7 +219,11 @@ pub(super) fn inner_void_body(method: &str) -> Vec { /// Builds the synthetic method body for recursive inner return. pub(super) fn recursive_inner_return_body(method: &str) -> Vec { - return_body(method_call(inner_expr(), method, Vec::new())) + return_body(method_call( + assume_recursive_iterator_expr(inner_expr()), + method, + Vec::new(), + )) } /// Builds the AST expression for limit position. diff --git a/src/types/checker/builtin_spl_classes/heaps.rs b/src/types/checker/builtin_spl_classes/heaps.rs index 0ef59eb74c..c52b7101ce 100644 --- a/src/types/checker/builtin_spl_classes/heaps.rs +++ b/src/types/checker/builtin_spl_classes/heaps.rs @@ -22,6 +22,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplHeap".to_string(), FlattenedClass { name: "SplHeap".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string(), "Countable".to_string()], is_abstract: true, @@ -32,6 +33,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -39,6 +41,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplMaxHeap".to_string(), FlattenedClass { name: "SplMaxHeap".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplHeap".to_string()), implements: Vec::new(), is_abstract: false, @@ -49,6 +52,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -56,6 +60,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplMinHeap".to_string(), FlattenedClass { name: "SplMinHeap".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplHeap".to_string()), implements: Vec::new(), is_abstract: false, @@ -66,6 +71,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -73,6 +79,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplPriorityQueue".to_string(), FlattenedClass { name: "SplPriorityQueue".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string(), "Countable".to_string()], is_abstract: false, @@ -83,6 +90,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_priority_queue_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/multiple.rs b/src/types/checker/builtin_spl_classes/multiple.rs index 30ec0963aa..0950fbe81a 100644 --- a/src/types/checker/builtin_spl_classes/multiple.rs +++ b/src/types/checker/builtin_spl_classes/multiple.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "MultipleIterator".to_string(), FlattenedClass { name: "MultipleIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, @@ -32,6 +33,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: multiple_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/object_storage.rs b/src/types/checker/builtin_spl_classes/object_storage.rs index 3008924cc5..d6e14c828c 100644 --- a/src/types/checker/builtin_spl_classes/object_storage.rs +++ b/src/types/checker/builtin_spl_classes/object_storage.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "SplObjectStorage".to_string(), FlattenedClass { name: "SplObjectStorage".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), @@ -36,6 +37,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/patch.rs b/src/types/checker/builtin_spl_classes/patch.rs index 08c86ae2a4..a10c4c82de 100644 --- a/src/types/checker/builtin_spl_classes/patch.rs +++ b/src/types/checker/builtin_spl_classes/patch.rs @@ -47,6 +47,7 @@ pub(super) fn patch_builtin_spl_storage_signatures(checker: &mut Checker) { "class".to_string(), PhpType::Union(vec![PhpType::Str, PhpType::Void]), )); + sig.param_type_exprs.push(None); sig.defaults.push(Some(null_expr())); sig.ref_params.push(false); sig.declared_params.push(true); @@ -97,9 +98,7 @@ pub(super) fn patch_builtin_spl_storage_signatures(checker: &mut Checker) { ] { if let Some(class_info) = checker.classes.get_mut(class_name) { for (name, ty) in &mut class_info.properties { - if name == "inner" { - *ty = PhpType::Object("RecursiveIterator".to_string()); - } else if name == "callback" { + if name == "callback" { *ty = PhpType::Callable; } else if name == "callbackEnv" { *ty = PhpType::Pointer(None); diff --git a/src/types/checker/builtin_spl_classes/phar.rs b/src/types/checker/builtin_spl_classes/phar.rs index 9e46c47cd4..29359a6bea 100644 --- a/src/types/checker/builtin_spl_classes/phar.rs +++ b/src/types/checker/builtin_spl_classes/phar.rs @@ -32,6 +32,7 @@ fn insert_phar_like_class(class_map: &mut HashMap, name: name.to_string(), FlattenedClass { name: name.to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "ArrayAccess".to_string(), @@ -46,6 +47,7 @@ fn insert_phar_like_class(class_map: &mut HashMap, name: attributes: Vec::new(), constants: phar_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -56,6 +58,7 @@ fn insert_phar_file_info_class(class_map: &mut HashMap) "PharFileInfo".to_string(), FlattenedClass { name: "PharFileInfo".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: Vec::new(), is_abstract: false, @@ -66,6 +69,7 @@ fn insert_phar_file_info_class(class_map: &mut HashMap) attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/recursive.rs b/src/types/checker/builtin_spl_classes/recursive.rs index 06735b1837..1b76375d8d 100644 --- a/src/types/checker/builtin_spl_classes/recursive.rs +++ b/src/types/checker/builtin_spl_classes/recursive.rs @@ -25,6 +25,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveFilterIterator".to_string(), FlattenedClass { name: "RecursiveFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: true, @@ -35,6 +36,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -42,6 +44,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveCallbackFilterIterator".to_string(), FlattenedClass { name: "RecursiveCallbackFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("CallbackFilterIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -52,6 +55,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -59,6 +63,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ParentIterator".to_string(), FlattenedClass { name: "ParentIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("RecursiveFilterIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -69,6 +74,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -154,7 +160,14 @@ fn spl_parent_iterator_methods() -> Vec { /// Builds the synthetic method body for recursive filter get children. fn recursive_filter_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), @@ -167,7 +180,14 @@ fn recursive_filter_get_children_body() -> Vec { /// Builds the synthetic method body for recursive callback filter get children. fn recursive_callback_filter_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), @@ -195,7 +215,14 @@ fn recursive_callback_filter_get_children_body() -> Vec { /// Builds the synthetic method body for parent iterator get children. fn parent_iterator_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), diff --git a/src/types/checker/builtin_spl_classes/recursive_array.rs b/src/types/checker/builtin_spl_classes/recursive_array.rs index 643837e07d..05bcc6bb25 100644 --- a/src/types/checker/builtin_spl_classes/recursive_array.rs +++ b/src/types/checker/builtin_spl_classes/recursive_array.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "RecursiveArrayIterator".to_string(), FlattenedClass { name: "RecursiveArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("ArrayIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -32,6 +33,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs index c3d81833c6..47847a2e8d 100644 --- a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs +++ b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs @@ -23,6 +23,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "RecursiveIteratorIterator".to_string(), FlattenedClass { name: "RecursiveIteratorIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["OuterIterator".to_string()], is_abstract: false, @@ -33,6 +34,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: recursive_iterator_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/regex.rs b/src/types/checker/builtin_spl_classes/regex.rs index a684ba1f27..3121a8b868 100644 --- a/src/types/checker/builtin_spl_classes/regex.rs +++ b/src/types/checker/builtin_spl_classes/regex.rs @@ -37,6 +37,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RegexIterator".to_string(), FlattenedClass { name: "RegexIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -47,6 +48,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: regex_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -54,6 +56,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveRegexIterator".to_string(), FlattenedClass { name: "RecursiveRegexIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("RegexIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -64,6 +67,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: regex_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -309,6 +313,7 @@ fn regex_capture_closure_expr( expr(ExprKind::Closure { params: vec![("matches".to_string(), None, None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), body, diff --git a/src/types/checker/builtin_spl_classes/storage.rs b/src/types/checker/builtin_spl_classes/storage.rs index 55fee7d98c..b3669daf23 100644 --- a/src/types/checker/builtin_spl_classes/storage.rs +++ b/src/types/checker/builtin_spl_classes/storage.rs @@ -22,6 +22,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "EmptyIterator".to_string(), FlattenedClass { name: "EmptyIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, @@ -32,6 +33,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -39,6 +41,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ArrayIterator".to_string(), FlattenedClass { name: "ArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), @@ -54,6 +57,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -61,6 +65,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ArrayObject".to_string(), FlattenedClass { name: "ArrayObject".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "IteratorAggregate".to_string(), @@ -75,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_exceptions.rs b/src/types/checker/builtin_spl_exceptions.rs index f62d973d2d..e1364dd3de 100644 --- a/src/types/checker/builtin_spl_exceptions.rs +++ b/src/types/checker/builtin_spl_exceptions.rs @@ -75,6 +75,7 @@ pub(crate) fn inject_builtin_spl_exceptions( (*name).to_string(), FlattenedClass { name: (*name).to_string(), + span: crate::span::Span::dummy(), extends: Some((*parent).to_string()), implements: Vec::new(), is_abstract: false, @@ -85,6 +86,7 @@ pub(crate) fn inject_builtin_spl_exceptions( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_stdclass.rs b/src/types/checker/builtin_stdclass.rs index 4c41d96e17..126febf1e7 100644 --- a/src/types/checker/builtin_stdclass.rs +++ b/src/types/checker/builtin_stdclass.rs @@ -42,6 +42,7 @@ pub(crate) fn inject_builtin_stdclass( "stdClass".to_string(), FlattenedClass { name: "stdClass".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -52,6 +53,7 @@ pub(crate) fn inject_builtin_stdclass( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_types/calendar.rs b/src/types/checker/builtin_types/calendar.rs index 82550bff70..2556f38304 100644 --- a/src/types/checker/builtin_types/calendar.rs +++ b/src/types/checker/builtin_types/calendar.rs @@ -42,7 +42,9 @@ fn cal_method( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(ret), by_ref_return: false, diff --git a/src/types/checker/builtin_types/date_period.rs b/src/types/checker/builtin_types/date_period.rs index eb19ceb46e..6ee4eb622d 100644 --- a/src/types/checker/builtin_types/date_period.rs +++ b/src/types/checker/builtin_types/date_period.rs @@ -180,7 +180,9 @@ fn method_vis( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -213,6 +215,7 @@ fn int_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(int_lit(0)), span: dummy(), attributes: Vec::new(), @@ -232,6 +235,7 @@ fn bool_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new(ExprKind::BoolLiteral(false), dummy())), span: dummy(), attributes: Vec::new(), @@ -612,7 +616,9 @@ fn date_period_create_from_iso8601_string() -> ClassMethod { param("specification", Some(TypeExpr::Str), None), param("options", Some(TypeExpr::Int), Some(int_lit(0))), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, // PHP 8.3+: returns a `DatePeriod` or throws (never `false`). return_type: Some(TypeExpr::Named(Name::unqualified("DatePeriod"))), @@ -671,6 +677,7 @@ pub(crate) fn inject_builtin_date_period(class_map: &mut HashMap ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(default), span: dummy(), attributes: Vec::new(), @@ -277,7 +280,9 @@ fn datetime_zone_list_identifiers() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -434,7 +439,9 @@ fn datetime_zone_list_abbreviations() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -1548,7 +1555,9 @@ fn datetime_create_from_format(class_name: &str) -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Union(vec![ TypeExpr::Named(Name::unqualified(class_name)), @@ -1586,7 +1595,9 @@ fn datetime_get_last_errors(class_name: &str) -> ClassMethod { is_final: false, has_body: true, params: vec![], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -1631,7 +1642,9 @@ fn datetime_create_from_object(method_name: &str, target_class: &str) -> ClassMe None, false, )], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(target_class))), by_ref_return: false, @@ -1674,7 +1687,9 @@ fn datetime_create_from_timestamp(class_name: &str) -> ClassMethod { None, false, )], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), by_ref_return: false, @@ -1724,7 +1739,9 @@ fn datetime_set_isodate(class_name: &str) -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), by_ref_return: false, @@ -1934,7 +1951,9 @@ fn datetime_date_parse_from_format() -> ClassMethod { ("format".to_string(), Some(TypeExpr::Str), None, false), ("datetime".to_string(), Some(TypeExpr::Str), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2019,7 +2038,9 @@ fn datetime_gettimeofday() -> ClassMethod { Some(Expr::new(ExprKind::BoolLiteral(false), dummy())), false, )], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2217,7 +2238,9 @@ fn datetime_extract_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -2285,7 +2308,9 @@ fn datetime_extract_modify_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -2308,7 +2333,9 @@ fn datetime_strip_modify_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2331,7 +2358,9 @@ fn datetime_strip_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2361,7 +2390,9 @@ fn datetime_strftime() -> ClassMethod { ("timestamp".to_string(), Some(TypeExpr::Int), None, false), ("utc".to_string(), Some(TypeExpr::Bool), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2593,7 +2624,9 @@ fn datetime_tz_name_from_abbr() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2755,7 +2788,9 @@ fn datetime_strptime() -> ClassMethod { ("timestamp".to_string(), Some(TypeExpr::Str), None, false), ("format".to_string(), Some(TypeExpr::Str), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2784,7 +2819,9 @@ fn datetime_sun_rs() -> ClassMethod { ("altit".to_string(), Some(TypeExpr::Float), None, false), ("limb".to_string(), Some(TypeExpr::Int), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2811,7 +2848,9 @@ fn datetime_sun_val() -> ClassMethod { ("rc".to_string(), Some(TypeExpr::Int), None, false), ("tsval".to_string(), Some(TypeExpr::Int), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2838,7 +2877,9 @@ fn datetime_sun_info() -> ClassMethod { ("latitude".to_string(), Some(TypeExpr::Float), None, false), ("longitude".to_string(), Some(TypeExpr::Float), None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2896,7 +2937,9 @@ fn datetime_sunfunc() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2921,7 +2964,9 @@ fn datetime_date_parse() -> ClassMethod { is_final: false, has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2969,7 +3014,9 @@ fn abstract_method( is_final: false, has_body: false, params, + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -3277,7 +3324,9 @@ fn date_interval_create_from_date_string() -> ClassMethod { is_final: false, has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("DateInterval"))), by_ref_return: false, @@ -3774,6 +3823,7 @@ pub(crate) fn inject_builtin_datetime( "DateInterval".to_string(), FlattenedClass { name: "DateInterval".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -3800,6 +3850,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3809,6 +3860,7 @@ pub(crate) fn inject_builtin_datetime( "DateTimeZone".to_string(), FlattenedClass { name: "DateTimeZone".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -3843,6 +3895,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_zone_group_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3852,6 +3905,7 @@ pub(crate) fn inject_builtin_datetime( "DateTimeImmutable".to_string(), FlattenedClass { name: "DateTimeImmutable".to_string(), + span: dummy(), extends: None, implements: vec!["DateTimeInterface".to_string()], is_abstract: false, @@ -3872,6 +3926,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_format_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3904,6 +3959,7 @@ pub(crate) fn inject_builtin_datetime( "DateTime".to_string(), FlattenedClass { name: "DateTime".to_string(), + span: dummy(), extends: None, implements: vec!["DateTimeInterface".to_string()], is_abstract: false, @@ -3914,6 +3970,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_format_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3929,6 +3986,7 @@ pub(crate) fn inject_builtin_datetime( fn date_exception_subclass(name: &str, parent: &str) -> FlattenedClass { FlattenedClass { name: name.to_string(), + span: dummy(), extends: Some(parent.to_string()), implements: Vec::new(), is_abstract: false, @@ -3939,6 +3997,7 @@ fn date_exception_subclass(name: &str, parent: &str) -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index ab09a37f66..dae06ef040 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -53,15 +53,15 @@ impl Clone for InterfaceDeclInfo { } } -/// Registers the nine builtin exception/error types (Throwable, Error, TypeError, -/// ValueError, Exception, RuntimeException, JsonException, Fiber, FiberError) in +/// Registers the builtin throwable hierarchy and Fiber declarations in /// `interface_map` and `class_map`. /// /// Checks for name collisions with user-declared types before inserting; returns /// `CompileError` if any builtin name is already present. Insertion order sets /// the inheritance chain: Error/Exception extend Throwable; TypeError/ValueError -/// extend Error; RuntimeException extends Exception; JsonException extends -/// RuntimeException; FiberError extends Error. Fiber is final with no parent. +/// extend Error; RuntimeException/ReflectionException extend Exception; +/// JsonException extends RuntimeException; FiberError extends Error. Fiber is +/// final with no parent. pub(crate) fn inject_builtin_throwables( interface_map: &mut HashMap, class_map: &mut HashMap, @@ -74,6 +74,7 @@ pub(crate) fn inject_builtin_throwables( "ArithmeticError", "Exception", "RuntimeException", + "ReflectionException", "JsonException", "Fiber", "FiberError", @@ -108,6 +109,7 @@ pub(crate) fn inject_builtin_throwables( "Error".to_string(), FlattenedClass { name: "Error".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Throwable".to_string()], is_abstract: false, @@ -131,12 +133,14 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( "Exception".to_string(), FlattenedClass { name: "Exception".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Throwable".to_string()], is_abstract: false, @@ -160,15 +164,35 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); - // RuntimeException and JsonException inherit the Throwable API from - // Exception via the standard inheritance machinery; they don't need to - // redeclare anything locally. + // RuntimeException, ReflectionException, and JsonException inherit the + // Throwable API from Exception via the standard inheritance machinery; they + // don't need to redeclare anything locally. class_map.insert( "RuntimeException".to_string(), FlattenedClass { name: "RuntimeException".to_string(), + span: crate::span::Span::dummy(), + extends: Some("Exception".to_string()), + implements: Vec::new(), + is_abstract: false, + is_final: false, + is_readonly_class: false, + properties: Vec::new(), + methods: Vec::new(), + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + trait_aliases: Vec::new(), + }, + ); + class_map.insert( + "ReflectionException".to_string(), + FlattenedClass { + name: "ReflectionException".to_string(), + span: crate::span::Span::dummy(), extends: Some("Exception".to_string()), implements: Vec::new(), is_abstract: false, @@ -179,12 +203,14 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( "JsonException".to_string(), FlattenedClass { name: "JsonException".to_string(), + span: crate::span::Span::dummy(), extends: Some("RuntimeException".to_string()), implements: Vec::new(), is_abstract: false, @@ -195,6 +221,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -202,6 +229,7 @@ pub(crate) fn inject_builtin_throwables( "TypeError".to_string(), FlattenedClass { name: "TypeError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -212,12 +240,14 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( "ValueError".to_string(), FlattenedClass { name: "ValueError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -228,12 +258,14 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( "ArithmeticError".to_string(), FlattenedClass { name: "ArithmeticError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -244,6 +276,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -256,6 +289,7 @@ pub(crate) fn inject_builtin_throwables( "Fiber".to_string(), FlattenedClass { name: "Fiber".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -266,6 +300,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -274,6 +309,7 @@ pub(crate) fn inject_builtin_throwables( "FiberError".to_string(), FlattenedClass { name: "FiberError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -284,6 +320,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index 33523e1c0b..b1ca606124 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -9,10 +9,9 @@ //! Key details: //! - Dummy AST members carry type contracts only; runtime behavior is implemented elsewhere. -use crate::names::{Name, php_symbol_key}; +use crate::names::{php_symbol_key, Name}; use crate::parser::ast::{ - ClassMethod, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, - Visibility, + ClassMethod, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, Visibility, }; use crate::types::PhpType; @@ -32,6 +31,7 @@ pub(super) fn builtin_exception_message_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::StringLiteral(String::new()), crate::span::Span::dummy(), @@ -71,7 +71,9 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -118,6 +120,7 @@ pub(super) fn builtin_exception_code_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::IntLiteral(0), crate::span::Span::dummy(), @@ -188,7 +191,10 @@ pub(super) fn builtin_exception_get_trace_method() -> ClassMethod { concrete_throwable_method( "getTrace", array_type(), - Expr::new(ExprKind::ArrayLiteral(Vec::new()), crate::span::Span::dummy()), + Expr::new( + ExprKind::ArrayLiteral(Vec::new()), + crate::span::Span::dummy(), + ), ) } @@ -256,7 +262,9 @@ fn concrete_throwable_method(name: &str, return_type: TypeExpr, value: Expr) -> is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -279,7 +287,9 @@ fn abstract_throwable_method(name: &str, return_type: TypeExpr) -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -301,12 +311,17 @@ fn nullable_throwable_type() -> TypeExpr { /// Patches the checker metadata for the Throwable interface and all builtin exception classes. /// Updates return types for getter methods and the `__construct` parameter types for Error, TypeError, -/// ValueError, Exception, RuntimeException, JsonException, and FiberError. +/// ValueError, Exception, RuntimeException, ReflectionException, JsonException, and FiberError. pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { - let nullable_throwable = - checker.normalize_union_type(vec![PhpType::Object("Throwable".to_string()), PhpType::Void]); + let nullable_throwable = checker.normalize_union_type(vec![ + PhpType::Object("Throwable".to_string()), + PhpType::Void, + ]); if let Some(interface_info) = checker.interfaces.get_mut("Throwable") { - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getMessage")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("getMessage")) + { sig.return_type = PhpType::Str; } if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getCode")) { @@ -327,10 +342,16 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { { sig.return_type = PhpType::Str; } - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getPrevious")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("getPrevious")) + { sig.return_type = nullable_throwable.clone(); } - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("__toString")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("__toString")) + { sig.return_type = PhpType::Str; } } @@ -341,6 +362,7 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { "ArithmeticError", "Exception", "RuntimeException", + "ReflectionException", "JsonException", "FiberError", ] { diff --git a/src/types/checker/builtin_types/fiber.rs b/src/types/checker/builtin_types/fiber.rs index 280c343b3d..550c9c3577 100644 --- a/src/types/checker/builtin_types/fiber.rs +++ b/src/types/checker/builtin_types/fiber.rs @@ -63,7 +63,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -82,7 +84,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("callback".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -102,7 +106,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -119,7 +125,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("value".to_string(), None, null_default(), false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -136,7 +144,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("exception".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -153,7 +163,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -175,7 +187,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("value".to_string(), None, null_default(), false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -192,7 +206,9 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/types/checker/builtin_types/magic_methods.rs b/src/types/checker/builtin_types/magic_methods.rs index b0b2d1667f..6495bd89d1 100644 --- a/src/types/checker/builtin_types/magic_methods.rs +++ b/src/types/checker/builtin_types/magic_methods.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; -use crate::parser::ast::Visibility; +use crate::parser::ast::{TypeExpr, Visibility}; use crate::types::PhpType; use super::super::Checker; @@ -23,6 +23,7 @@ use super::super::Checker; /// For `__set`: parameter 0 is `PhpType::Str`, parameter 1 is `PhpType::Mixed`. /// For `__call`/`__callStatic`: parameter 0 is `PhpType::Str`, parameter 1 is /// `PhpType::Array` of `PhpType::Never` (the forwarded argument list). +/// Declared `__isset`/`__unset` return types are validated separately. /// Does nothing for classes that do not declare these methods. pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { for class_info in checker.classes.values_mut() { @@ -41,6 +42,16 @@ pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { param.1 = PhpType::Mixed; } } + if let Some(sig) = class_info.methods.get_mut("__isset") { + if let Some(param) = sig.params.get_mut(0) { + param.1 = PhpType::Str; + } + } + if let Some(sig) = class_info.methods.get_mut("__unset") { + if let Some(param) = sig.params.get_mut(0) { + param.1 = PhpType::Str; + } + } if let Some(sig) = class_info.methods.get_mut("__call") { if let Some(param) = sig.params.get_mut(0) { param.1 = PhpType::Str; @@ -62,7 +73,8 @@ pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { } /// Validates that user-declared magic methods (`__toString`, `__get`, `__set`, -/// `__isset`, `__unset`, `__call`, `__callStatic`, `__invoke`, `__destruct`) +/// `__isset`, `__unset`, `__call`, `__callStatic`, `__invoke`, `__clone`, +/// `__destruct`) /// conform to PHP's static/non-static, visibility, arity, and return-type rules. /// /// Returns `Ok(())` if all declared magic methods are contract-compliant. @@ -159,6 +171,72 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } + "__isset" => { + if method.is_static { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be non-static: {}::__isset", class_name), + )); + continue; + } + if method.visibility != Visibility::Public { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be public: {}::__isset", class_name), + )); + continue; + } + if method.params.len() != 1 || method.variadic.is_some() { + errors.push(CompileError::new( + method.span, + &format!("Magic method must take 1 argument: {}::__isset", class_name), + )); + continue; + } + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Bool)) + { + errors.push(CompileError::new( + method.span, + &format!("Magic method must return bool: {}::__isset", class_name), + )); + } + } + "__unset" => { + if method.is_static { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be non-static: {}::__unset", class_name), + )); + continue; + } + if method.visibility != Visibility::Public { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be public: {}::__unset", class_name), + )); + continue; + } + if method.params.len() != 1 || method.variadic.is_some() { + errors.push(CompileError::new( + method.span, + &format!("Magic method must take 1 argument: {}::__unset", class_name), + )); + continue; + } + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Void)) + { + errors.push(CompileError::new( + method.span, + &format!("Magic method must return void: {}::__unset", class_name), + )); + } + } "__call" => { if method.is_static { errors.push(CompileError::new( @@ -196,52 +274,53 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } - "__destruct" => { - // PHP permits any visibility for __destruct (the engine calls - // it regardless), so only the non-static and zero-argument - // rules are enforced here. + "__clone" => { if method.is_static { errors.push(CompileError::new( method.span, - &format!( - "Magic method must be non-static: {}::__destruct", - class_name - ), + &format!("Magic method must be non-static: {}::__clone", class_name), )); continue; } if !method.params.is_empty() || method.variadic.is_some() { errors.push(CompileError::new( method.span, - &format!( - "Magic method must take 0 arguments: {}::__destruct", - class_name - ), + &format!("Magic method must take 0 arguments: {}::__clone", class_name), )); + continue; } - } - magic @ ("__isset" | "__unset") => { - // `isset($obj->prop)`/`unset($obj->prop)` on an undeclared - // property dispatch here; both take the property name only. - let pretty = if magic == "__isset" { "__isset" } else { "__unset" }; - if method.is_static { + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Void)) + { errors.push(CompileError::new( method.span, - &format!("Magic method must be non-static: {}::{}", class_name, pretty), + &format!("Magic method must return void: {}::__clone", class_name), )); - continue; } - if method.visibility != Visibility::Public { + } + "__destruct" => { + // PHP permits any visibility for __destruct (the engine calls + // it regardless), so only the non-static and zero-argument + // rules are enforced here. + if method.is_static { errors.push(CompileError::new( method.span, - &format!("Magic method must be public: {}::{}", class_name, pretty), + &format!( + "Magic method must be non-static: {}::__destruct", + class_name + ), )); continue; } - if method.params.len() != 1 || method.variadic.is_some() { + if !method.params.is_empty() || method.variadic.is_some() { errors.push(CompileError::new( method.span, - &format!("Magic method must take 1 argument: {}::{}", class_name, pretty), + &format!( + "Magic method must take 0 arguments: {}::__destruct", + class_name + ), )); } } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 1eddec371b..4b27e7e033 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -7,22 +7,29 @@ //! - `crate::types::checker::driver::init` (alongside `inject_builtin_throwables`). //! //! Key details: -//! - Property and method bodies are dummies or simple private-slot accessors; -//! runtime population is handled by codegen-only reflection constructors. +//! - Property and method bodies are dummies, private-slot accessors, or small +//! fallbacks; runtime population is handled by codegen-only reflection constructors. use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; +use crate::names::Name; use crate::parser::ast::{ - ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, + BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, InstanceOfTarget, Stmt, + StmtKind, TypeExpr, Visibility, }; use crate::types::traits::FlattenedClass; use crate::types::PhpType; use super::super::Checker; -/// Injects the four built-in reflection types into `class_map` after verifying +/// Returns a dummy source span for synthetic reflection AST nodes. +fn dummy() -> crate::span::Span { + crate::span::Span::dummy() +} + +/// Injects the built-in reflection types into `class_map` after verifying /// none are already declared. Each type is a dummy shell; runtime population /// happens in codegen. Returns an error if any reflection name is already in use. pub(crate) fn inject_builtin_reflection( @@ -33,11 +40,18 @@ pub(crate) fn inject_builtin_reflection( for builtin_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionFunction", "ReflectionMethod", "ReflectionProperty", - "ReflectionFunction", "ReflectionParameter", "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", ] { let builtin_key = php_symbol_key(builtin_name); if interface_map @@ -48,7 +62,10 @@ pub(crate) fn inject_builtin_reflection( { return Err(CompileError::new( crate::span::Span::dummy(), - &format!("Cannot redeclare built-in reflection type: {}", builtin_name), + &format!( + "Cannot redeclare built-in reflection type: {}", + builtin_name + ), )); } } @@ -57,38 +74,78 @@ pub(crate) fn inject_builtin_reflection( "ReflectionAttribute".to_string(), FlattenedClass { name: "ReflectionAttribute".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, is_final: true, is_readonly_class: false, properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__args", Visibility::Private, Some(array_type()), empty_array()), - builtin_property("__factory", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), + builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__args", + Visibility::Private, + Some(array_type()), + empty_array(), + ), + builtin_property( + "__factory", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), + builtin_property( + "__target", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), + builtin_property( + "__is_repeated", + Visibility::Private, + Some(TypeExpr::Bool), + false_bool(), + ), ], methods: vec![ builtin_reflection_attribute_constructor_method(), builtin_reflection_attribute_get_name_method(), builtin_reflection_attribute_get_arguments_method(), builtin_reflection_attribute_new_instance_method(), + builtin_reflection_class_int_method("getTarget", "__target"), + builtin_reflection_class_bool_method("isRepeated", "__is_repeated"), ], attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); + class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); class_map.insert( - "ReflectionClass".to_string(), - builtin_reflection_class(), + "ReflectionObject".to_string(), + builtin_reflection_object_class(), ); + class_map.insert("ReflectionEnum".to_string(), builtin_reflection_enum_class()); + class_map.insert("ReflectionFunction".to_string(), builtin_reflection_function()); class_map.insert( "ReflectionMethod".to_string(), builtin_reflection_owner_class( "ReflectionMethod", + true, vec![ ("class_name", Some(TypeExpr::Str), None, false), - ("method_name", Some(TypeExpr::Str), None, false), + ( + "method_name", + Some(TypeExpr::Nullable(Box::new(TypeExpr::Str))), + null_expr(), + false, + ), ], ), ); @@ -96,21 +153,43 @@ pub(crate) fn inject_builtin_reflection( "ReflectionProperty".to_string(), builtin_reflection_owner_class( "ReflectionProperty", + true, vec![ ("class_name", Some(TypeExpr::Str), None, false), ("property_name", Some(TypeExpr::Str), None, false), ], ), ); - class_map.insert("ReflectionFunction".to_string(), builtin_reflection_function()); class_map.insert( "ReflectionParameter".to_string(), builtin_reflection_parameter(), ); + class_map.insert("ReflectionNamedType".to_string(), builtin_reflection_named_type()); + class_map.insert( + "ReflectionUnionType".to_string(), + builtin_reflection_union_type(), + ); class_map.insert( - "ReflectionNamedType".to_string(), - builtin_reflection_named_type(), + "ReflectionIntersectionType".to_string(), + builtin_reflection_intersection_type(), ); + for class_name in [ + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", + ] { + class_map.insert( + class_name.to_string(), + builtin_reflection_owner_class( + class_name, + true, + vec![ + ("class_name", Some(TypeExpr::Str), None, false), + ("constant_name", Some(TypeExpr::Str), None, false), + ], + ), + ); + } Ok(()) } @@ -134,6 +213,7 @@ fn builtin_property( is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default, span: crate::span::Span::dummy(), attributes: Vec::new(), @@ -156,6 +236,22 @@ fn empty_array() -> Option { )) } +/// Returns a `BoolLiteral(false)` expression. +fn false_bool() -> Option { + Some(Expr::new( + ExprKind::BoolLiteral(false), + crate::span::Span::dummy(), + )) +} + +/// Returns a `BoolLiteral(true)` expression. +fn true_bool() -> Option { + Some(Expr::new( + ExprKind::BoolLiteral(true), + crate::span::Span::dummy(), + )) +} + /// Returns an `IntLiteral` expression with the given value. fn int_lit(value: i64) -> Option { Some(Expr::new( @@ -177,18 +273,98 @@ fn bool_lit(value: bool) -> Option { )) } +/// Returns a `null` expression for nullable synthetic property defaults. +fn null_expr() -> Option { + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())) +} + /// Returns a `TypeExpr` for the unqualified name `array`. fn array_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("array")) } +/// Returns a `TypeExpr` for an indexed array of strings. +fn string_array_type() -> TypeExpr { + TypeExpr::Array(Box::new(TypeExpr::Str)) +} + +/// Returns a `TypeExpr` for an indexed array of objects with the given class name. +fn object_array_type(class_name: &str) -> TypeExpr { + TypeExpr::Array(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) +} + +/// Returns `array` for name-keyed reflection maps. +fn reflection_class_object_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionClass".to_string())), + } +} + +/// Returns `array` for trait-alias reflection maps. +fn reflection_string_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + } +} + +/// Returns `array` for static-property value reflection maps. +fn reflection_static_properties_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + } +} + +/// Returns `array` for ReflectionAttribute argument maps. +fn reflection_attribute_args_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + } +} + +/// Returns `array` for property-hook reflection maps. +fn reflection_property_hook_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionMethod".to_string())), + } +} + +/// Returns a nullable object type expression for one synthetic reflection class. +fn nullable_object_type(class_name: &str) -> TypeExpr { + TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) +} + +/// Returns a `TypeExpr` for PHP's generic `object` type. +fn object_type() -> TypeExpr { + TypeExpr::Named(Name::unqualified("object")) +} + /// Returns a `TypeExpr` for the unqualified name `mixed`. fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) } +/// Returns a `TypeExpr` for PHP's builtin boolean type. +fn bool_type() -> TypeExpr { + TypeExpr::Bool +} + +/// Returns a `TypeExpr` for Reflection APIs whose PHP return is `string|false`. +fn string_or_bool_type() -> TypeExpr { + TypeExpr::Union(vec![TypeExpr::Str, TypeExpr::Bool]) +} + /// Returns a private parameterless `__construct` method for `ReflectionAttribute`. fn builtin_reflection_attribute_constructor_method() -> ClassMethod { + builtin_reflection_private_constructor_method() +} + +/// Returns a private parameterless `__construct` for internally materialized reflection objects. +fn builtin_reflection_private_constructor_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { name: "__construct".to_string(), @@ -198,7 +374,9 @@ fn builtin_reflection_attribute_constructor_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -220,7 +398,9 @@ fn builtin_reflection_attribute_get_name_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -251,7 +431,9 @@ fn builtin_reflection_attribute_get_arguments_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(crate::names::Name::unqualified("array"))), by_ref_return: false, @@ -282,7 +464,268 @@ fn builtin_reflection_attribute_new_instance_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public variadic `ReflectionClass::newInstance()` method. +/// +/// Direct calls are lowered specially so their source arguments become +/// constructor arguments for the reflected class. The no-argument body keeps +/// indirect calls and metadata emission coherent when no argument forwarding is +/// required. +fn builtin_reflection_class_new_instance_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstance".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: Some("args".to_string()), + variadic_by_ref: false, + variadic_type: None, + return_type: Some(object_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewDynamic { + name_expr: Box::new(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__name".to_string(), + }, + dummy_span, + )), + args: Vec::new(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public variadic `ReflectionMethod::invoke()` method shell. +/// +/// Direct AOT calls are lowered specially so the first argument becomes the +/// invocation receiver and the remaining source arguments are normalized +/// against the reflected method's own signature. +fn builtin_reflection_method_invoke_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invoke".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: Some("args".to_string()), + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionMethod::invokeArgs()` method shell. +/// +/// Direct AOT calls are lowered specially so the provided argument array becomes +/// the reflected method's source argument list. +fn builtin_reflection_method_invoke_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invokeArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("object".to_string(), Some(mixed_type()), None, false), + ("args".to_string(), Some(array_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public static `ReflectionMethod::createFromMethodName()` method shell. +fn builtin_reflection_method_create_from_method_name_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "createFromMethodName".to_string(), + visibility: Visibility::Public, + is_static: true, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("method".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionMethod"), + args: vec![ + Expr::new(ExprKind::StringLiteral(String::new()), dummy_span), + Expr::new(ExprKind::StringLiteral(String::new()), dummy_span), + ], + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `setAccessible(bool $accessible)` no-op method shell. +fn builtin_reflection_set_accessible_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "setAccessible".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("accessible".to_string(), Some(bool_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Void), + by_ref_return: false, + body: Vec::new(), + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public variadic `ReflectionFunction::invoke()` method shell. +/// +/// Direct generated/AOT calls are lowered specially so the variadic source +/// arguments are normalized against the reflected function's own signature. +fn builtin_reflection_function_invoke_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invoke".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: Some("args".to_string()), + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionFunction::invokeArgs()` method shell. +/// +/// Direct generated/AOT calls are lowered specially so the provided argument +/// array becomes the reflected function's source argument list. +fn builtin_reflection_function_invoke_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invokeArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("args".to_string(), Some(array_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass::newInstanceArgs()` method. +/// +/// Direct calls are lowered specially so the provided argument array becomes +/// constructor arguments for the reflected class. The placeholder body keeps +/// the synthetic class metadata coherent for non-special paths. +fn builtin_reflection_class_new_instance_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstanceArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + // `iterable` rather than bare `array`: the checker resolves `array` + // to a string-keyed map, which would reject indexed argument lists; + // Iterable accepts both array shapes. + params: vec![( + "args".to_string(), + Some(TypeExpr::Iterable), + empty_array(), + false, + )], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -312,7 +755,9 @@ fn builtin_reflection_slot_getter( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -343,8 +788,10 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + params: vec![("function".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -354,45 +801,23 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { } } -/// Builds the `ReflectionFunction` shell with private name/short-name and -/// parameter-count slots plus public accessors. The slots are populated at -/// codegen from the reflected function's signature. +/// Builds the `ReflectionFunction` shell with private name/short-name, +/// attribute, and parameter slots. Codegen populates these from the reflected +/// function's signature and attribute metadata. fn builtin_reflection_function() -> FlattenedClass { - FlattenedClass { - name: "ReflectionFunction".to_string(), - extends: None, - implements: Vec::new(), - is_abstract: false, - is_final: true, - is_readonly_class: false, - properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__short", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__num_params", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), - builtin_property( - "__num_required", - Visibility::Private, - Some(TypeExpr::Int), - int_lit(0), - ), - builtin_property("__params", Visibility::Private, Some(array_type()), empty_array()), - ], - methods: vec![ - builtin_reflection_function_constructor_method(), - builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), - builtin_reflection_slot_getter("getShortName", "__short", TypeExpr::Str), - builtin_reflection_slot_getter("getNumberOfParameters", "__num_params", TypeExpr::Int), - builtin_reflection_slot_getter( - "getNumberOfRequiredParameters", - "__num_required", - TypeExpr::Int, - ), - builtin_reflection_slot_getter("getParameters", "__params", array_type()), - ], - attributes: Vec::new(), - constants: Vec::new(), - used_traits: Vec::new(), + let mut class = builtin_reflection_owner_class( + "ReflectionFunction", + true, + vec![("function", Some(TypeExpr::Str), None, false)], + ); + if let Some(constructor) = class + .methods + .iter_mut() + .find(|method| method.name == "__construct") + { + *constructor = builtin_reflection_function_constructor_method(); } + class } /// Builds the `ReflectionParameter` shell with private name/position/optional/ @@ -401,6 +826,7 @@ fn builtin_reflection_function() -> FlattenedClass { fn builtin_reflection_parameter() -> FlattenedClass { FlattenedClass { name: "ReflectionParameter".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -408,6 +834,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { is_readonly_class: false, properties: vec![ builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), builtin_property("__position", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), builtin_property( "__optional", @@ -421,20 +848,441 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_passed_by_reference", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_promoted", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), builtin_property("__has_type", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false)), - builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), - ], - methods: vec![ - builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), - builtin_reflection_slot_getter("getPosition", "__position", TypeExpr::Int), - builtin_reflection_slot_getter("isOptional", "__optional", TypeExpr::Bool), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(true), + ), + builtin_property( + "__is_array_type", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_callable_type", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), + builtin_property("__class", Visibility::Private, Some(mixed_type()), null_lit()), + builtin_property( + "__has_default_value", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_default_value_constant", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__default_value_constant_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__default_value", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), + builtin_property( + "__default_value_object_class", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__declaring_class", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), + builtin_property( + "__declaring_function", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), + ], + methods: vec![ + builtin_reflection_owner_constructor_method(vec![ + ("function", Some(mixed_type()), None, false), + ("param", Some(mixed_type()), None, false), + ]), + builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), + builtin_reflection_slot_getter("getPosition", "__position", TypeExpr::Int), + builtin_reflection_slot_getter("isOptional", "__optional", TypeExpr::Bool), builtin_reflection_slot_getter("isVariadic", "__variadic", TypeExpr::Bool), + builtin_reflection_slot_getter( + "isPassedByReference", + "__is_passed_by_reference", + TypeExpr::Bool, + ), + builtin_reflection_parameter_can_be_passed_by_value_method(), + builtin_reflection_slot_getter("isPromoted", "__is_promoted", TypeExpr::Bool), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), + builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), + builtin_reflection_slot_getter("isArray", "__is_array_type", TypeExpr::Bool), + builtin_reflection_slot_getter("isCallable", "__is_callable_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), + builtin_reflection_slot_getter("getClass", "__class", mixed_type()), + builtin_reflection_slot_getter("__toString", "__name", TypeExpr::Str), + builtin_reflection_owner_get_attributes_method(), + builtin_reflection_slot_getter( + "isDefaultValueAvailable", + "__has_default_value", + TypeExpr::Bool, + ), + builtin_reflection_parameter_is_default_value_constant_method(), + builtin_reflection_parameter_get_default_value_constant_name_method(), + builtin_reflection_parameter_get_default_value_method(), + builtin_reflection_slot_getter("getDeclaringClass", "__declaring_class", mixed_type()), + builtin_reflection_slot_getter( + "getDeclaringFunction", + "__declaring_function", + mixed_type(), + ), ], attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Builds `ReflectionParameter::getDefaultValue()` over the retained default slot. +fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let object_default_body = reflection_parameter_get_default_object_body(dummy_span); + ClassMethod { + name: "getDefaultValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + reflection_parameter_throw_if_default_missing(dummy_span), + Stmt::new( + StmtKind::If { + condition: binary_expr( + reflection_this_property("__default_value_object_class", dummy_span), + BinOp::NotEq, + string_lit("", dummy_span), + dummy_span, + ), + then_body: object_default_body, + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__default_value", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds the object-default branch for `ReflectionParameter::getDefaultValue()`. +fn reflection_parameter_get_default_object_body(span: crate::span::Span) -> Vec { + let arg_count_var = "__default_value_arg_count"; + let mut body = vec![ + Stmt::new( + StmtKind::If { + condition: binary_expr( + reflection_this_property("__default_value", span), + BinOp::StrictEq, + null_value(span), + span, + ), + then_body: vec![reflection_parameter_return_dynamic_default_object( + Vec::new(), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ), + Stmt::new( + StmtKind::Assign { + name: arg_count_var.to_string(), + value: function_call( + "count", + vec![reflection_this_property("__default_value", span)], + span, + ), + }, + span, + ), + ]; + for arg_count in 1..=8 { + body.push(reflection_parameter_default_object_arg_count_branch( + arg_count, + arg_count_var, + span, + )); + } + body.push(throw_new_reflection_exception( + string_lit("Internal error: Failed to retrieve the default value", span), + span, + )); + body +} + +/// Builds one constructor-argument-count branch for object defaults. +fn reflection_parameter_default_object_arg_count_branch( + arg_count: usize, + arg_count_var: &str, + span: crate::span::Span, +) -> Stmt { + let args = (0..arg_count) + .map(|index| reflection_parameter_default_object_arg(index, span)) + .collect(); + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr(arg_count_var, span), + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(arg_count as i64), span), + span, + ), + then_body: vec![reflection_parameter_return_dynamic_default_object(args, span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a return statement that constructs the retained object default class. +fn reflection_parameter_return_dynamic_default_object( + args: Vec, + span: crate::span::Span, +) -> Stmt { + Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewDynamic { + name_expr: Box::new(reflection_this_property( + "__default_value_object_class", + span, + )), + args, + }, + span, + ))), + span, + ) +} + +/// Builds `$this->__default_value[$index]` for retained object-default args. +fn reflection_parameter_default_object_arg(index: usize, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__default_value", span)), + index: Box::new(Expr::new(ExprKind::IntLiteral(index as i64), span)), + }, + span, + ) +} + +/// Returns a public `ReflectionClass::newInstanceWithoutConstructor()` method. +/// +/// Eval dispatch supplies the real constructorless allocation. The body remains +/// a conservative placeholder so the built-in class metadata exposes the PHP +/// method without forcing ordinary method lowering to construct a class. +fn builtin_reflection_class_new_instance_without_constructor_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstanceWithoutConstructor".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `ReflectionParameter::isDefaultValueConstant()` over retained default metadata. +fn builtin_reflection_parameter_is_default_value_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isDefaultValueConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + reflection_parameter_throw_if_default_missing(dummy_span), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__is_default_value_constant", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `ReflectionParameter::getDefaultValueConstantName()` over retained default metadata. +fn builtin_reflection_parameter_get_default_value_constant_name_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getDefaultValueConstantName".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + reflection_parameter_throw_if_default_missing(dummy_span), + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_default_value_constant", + dummy_span, + ))), + dummy_span, + ), + then_body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__default_value_constant_name", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds the PHP-compatible default-metadata guard shared by ReflectionParameter methods. +fn reflection_parameter_throw_if_default_missing(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__has_default_value", + span, + ))), + span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit("Internal error: Failed to retrieve the default value", span), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds `ReflectionParameter::canBePassedByValue()` from the retained by-ref flag. +fn builtin_reflection_parameter_can_be_passed_by_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "canBePassedByValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_passed_by_reference", + dummy_span, + ))), + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), } } @@ -444,6 +1292,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { fn builtin_reflection_named_type() -> FlattenedClass { FlattenedClass { name: "ReflectionNamedType".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -451,6 +1300,7 @@ fn builtin_reflection_named_type() -> FlattenedClass { is_readonly_class: false, properties: vec![ builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), builtin_property( "__allows_null", Visibility::Private, @@ -458,80 +1308,2923 @@ fn builtin_reflection_named_type() -> FlattenedClass { bool_lit(false), ), builtin_property( - "__builtin", + "__is_builtin", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false), ), ], methods: vec![ + builtin_reflection_private_constructor_method(), builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), + builtin_reflection_named_type_to_string_method(), builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), - builtin_reflection_slot_getter("isBuiltin", "__builtin", TypeExpr::Bool), + builtin_reflection_slot_getter("isBuiltin", "__is_builtin", TypeExpr::Bool), + ], + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Builds the `ReflectionUnionType` shell returned for supported union hints. +fn builtin_reflection_union_type() -> FlattenedClass { + FlattenedClass { + name: "ReflectionUnionType".to_string(), + span: dummy(), + extends: None, + implements: Vec::new(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: vec![ + builtin_property( + "__types", + Visibility::Private, + Some(object_array_type("ReflectionNamedType")), + empty_array(), + ), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_builtin", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + ], + methods: vec![ + builtin_reflection_private_constructor_method(), + builtin_reflection_class_array_method( + "getTypes", + "__types", + object_array_type("ReflectionNamedType"), + ), + builtin_reflection_composite_type_string_method("__toString", "|", true), + builtin_reflection_composite_type_string_method("getName", "|", true), + builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + builtin_reflection_class_bool_method("isBuiltin", "__is_builtin"), + ], + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Builds the `ReflectionIntersectionType` shell returned for supported intersection hints. +fn builtin_reflection_intersection_type() -> FlattenedClass { + FlattenedClass { + name: "ReflectionIntersectionType".to_string(), + span: dummy(), + extends: None, + implements: Vec::new(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: vec![ + builtin_property( + "__types", + Visibility::Private, + Some(object_array_type("ReflectionNamedType")), + empty_array(), + ), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_builtin", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + ], + methods: vec![ + builtin_reflection_private_constructor_method(), + builtin_reflection_class_array_method( + "getTypes", + "__types", + object_array_type("ReflectionNamedType"), + ), + builtin_reflection_composite_type_string_method("__toString", "&", false), + builtin_reflection_composite_type_string_method("getName", "&", false), + builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + builtin_reflection_class_bool_method("isBuiltin", "__is_builtin"), ], attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Builds `ReflectionNamedType::__toString()` from retained name/nullability slots. +fn builtin_reflection_named_type_to_string_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = reflection_this_property("__name", dummy_span); + let nullable_named_type = binary_expr( + reflection_this_property("__allows_null", dummy_span), + BinOp::And, + binary_expr( + name.clone(), + BinOp::StrictNotEq, + string_lit("mixed", dummy_span), + dummy_span, + ), + dummy_span, + ); + ClassMethod { + name: "__toString".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Str), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: nullable_named_type, + then_body: vec![Stmt::new( + StmtKind::Return(Some(concat_expr( + string_lit("?", dummy_span), + name.clone(), + dummy_span, + ))), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(Some(name)), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds a string-rendering method for `ReflectionUnionType` and `ReflectionIntersectionType`. +fn builtin_reflection_composite_type_string_method( + method_name: &str, + separator: &'static str, + append_null: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let mut body = vec![ + Stmt::new( + StmtKind::TypedAssign { + type_expr: TypeExpr::Str, + name: "result".to_string(), + value: string_lit("", dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__types", dummy_span), + key_var: None, + value_var: "type".to_string(), + value_by_ref: false, + body: reflection_composite_type_append_body( + method_call_expr( + variable_expr("type", dummy_span), + "getName", + Vec::new(), + dummy_span, + ), + separator, + dummy_span, + ), + }, + dummy_span, + ), + ]; + if append_null { + body.push(Stmt::new( + StmtKind::If { + condition: reflection_this_property("__allows_null", dummy_span), + then_body: reflection_composite_type_append_body( + string_lit("null", dummy_span), + separator, + dummy_span, + ), + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )); + } + body.push(Stmt::new( + StmtKind::Return(Some(variable_expr("result", dummy_span))), + dummy_span, + )); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Str), + by_ref_return: false, + body, + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds the statements that append one rendered type segment to `$result`. +fn reflection_composite_type_append_body( + value: Expr, + separator: &'static str, + span: crate::span::Span, +) -> Vec { + vec![ + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr("result", span), + BinOp::StrictNotEq, + string_lit("", span), + span, + ), + then_body: vec![Stmt::new( + StmtKind::Assign { + name: "result".to_string(), + value: concat_expr( + variable_expr("result", span), + string_lit(separator, span), + span, + ), + }, + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ), + Stmt::new( + StmtKind::Assign { + name: "result".to_string(), + value: concat_expr(variable_expr("result", span), value, span), + }, + span, + ), + ] +} + +/// Builds the `ReflectionClass` shell with retained eval metadata accessors. +fn builtin_reflection_class() -> FlattenedClass { + FlattenedClass { + name: "ReflectionClass".to_string(), + span: dummy(), + extends: None, + implements: Vec::new(), + is_abstract: false, + is_final: false, + is_readonly_class: false, + properties: vec![ + builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__attrs", + Visibility::Private, + Some(array_type()), + empty_array(), + ), + builtin_property( + "__is_final", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_abstract", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_interface", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_trait", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_enum", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_readonly", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_anonymous", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_instantiable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_cloneable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_iterable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_internal", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_user_defined", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), + builtin_property( + "__short_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__namespace_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__in_namespace", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__interface_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__interfaces", + Visibility::Private, + Some(object_array_type("ReflectionClass")), + empty_array(), + ), + builtin_property( + "__trait_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__traits", + Visibility::Private, + Some(object_array_type("ReflectionClass")), + empty_array(), + ), + builtin_property( + "__trait_aliases", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__parent_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__method_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__property_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__constant_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__constants", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), + builtin_property( + "__default_properties", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), + builtin_property( + "__static_properties", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), + builtin_property( + "__reflection_constants", + Visibility::Private, + Some(object_array_type("ReflectionClassConstant")), + empty_array(), + ), + builtin_property( + "__methods", + Visibility::Private, + Some(object_array_type("ReflectionMethod")), + empty_array(), + ), + builtin_property( + "__constructor", + Visibility::Private, + Some(nullable_object_type("ReflectionMethod")), + null_expr(), + ), + builtin_property( + "__parent_class", + Visibility::Private, + Some(mixed_type()), + false_bool(), + ), + builtin_property( + "__properties", + Visibility::Private, + Some(object_array_type("ReflectionProperty")), + empty_array(), + ), + ], + methods: vec![ + builtin_reflection_owner_constructor_method(vec![( + "class_name", + Some(mixed_type()), + None, + false, + )]), + builtin_reflection_class_string_method("getName", "__name"), + builtin_reflection_class_string_method("__toString", "__string"), + builtin_reflection_constant_false_union_method("getDocComment"), + builtin_reflection_constant_false_union_method("getExtensionName"), + builtin_reflection_constant_null_mixed_method("getExtension"), + builtin_reflection_class_string_method("getShortName", "__short_name"), + builtin_reflection_class_string_method("getNamespaceName", "__namespace_name"), + builtin_reflection_class_bool_method("inNamespace", "__in_namespace"), + builtin_reflection_class_array_method( + "getInterfaceNames", + "__interface_names", + string_array_type(), + ), + builtin_reflection_class_array_method( + "getInterfaces", + "__interfaces", + object_array_type("ReflectionClass"), + ), + builtin_reflection_class_array_method( + "getTraitNames", + "__trait_names", + string_array_type(), + ), + builtin_reflection_class_array_method( + "getTraits", + "__traits", + object_array_type("ReflectionClass"), + ), + builtin_reflection_class_array_method( + "getTraitAliases", + "__trait_aliases", + string_array_type(), + ), + builtin_reflection_class_bool_method("isFinal", "__is_final"), + builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), + builtin_reflection_class_bool_method("isInterface", "__is_interface"), + builtin_reflection_class_bool_method("isTrait", "__is_trait"), + builtin_reflection_class_bool_method("isEnum", "__is_enum"), + builtin_reflection_class_bool_method("isReadOnly", "__is_readonly"), + builtin_reflection_class_bool_method("isAnonymous", "__is_anonymous"), + builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), + builtin_reflection_class_bool_method("isCloneable", "__is_cloneable"), + builtin_reflection_class_bool_method("isIterable", "__is_iterable"), + builtin_reflection_class_bool_method("isIterateable", "__is_iterable"), + builtin_reflection_class_bool_method("isInternal", "__is_internal"), + builtin_reflection_class_bool_method("isUserDefined", "__is_user_defined"), + builtin_reflection_class_int_method("getModifiers", "__modifiers"), + builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), + builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), + builtin_reflection_class_has_name_method("hasConstant", "__constant_names", false), + builtin_reflection_class_get_constant_method(), + builtin_reflection_class_mixed_method("getConstants", "__constants"), + builtin_reflection_class_mixed_method("getDefaultProperties", "__default_properties"), + builtin_reflection_class_mixed_method("getStaticProperties", "__static_properties"), + builtin_reflection_class_get_static_property_value_method(), + builtin_reflection_class_set_static_property_value_method(), + builtin_reflection_class_array_method( + "getReflectionConstants", + "__reflection_constants", + object_array_type("ReflectionClassConstant"), + ), + builtin_reflection_class_get_reflection_constant_method(), + builtin_reflection_class_implements_interface_method(), + builtin_reflection_class_is_subclass_of_method(), + builtin_reflection_class_is_instance_method(), + builtin_reflection_class_get_member_method( + "getMethod", + "__methods", + "ReflectionMethod", + true, + ), + builtin_reflection_class_nullable_object_method( + "getConstructor", + "__constructor", + "ReflectionMethod", + ), + builtin_reflection_class_mixed_method("getParentClass", "__parent_class"), + builtin_reflection_class_filtered_array_method( + "getMethods", + "__methods", + object_array_type("ReflectionMethod"), + ), + builtin_reflection_class_filtered_array_method( + "getProperties", + "__properties", + object_array_type("ReflectionProperty"), + ), + builtin_reflection_class_get_member_method( + "getProperty", + "__properties", + "ReflectionProperty", + false, + ), + builtin_reflection_class_new_instance_method(), + builtin_reflection_class_new_instance_args_method(), + builtin_reflection_class_new_instance_without_constructor_method(), + builtin_reflection_owner_get_attributes_method(), + ], + attributes: Vec::new(), + constants: reflection_class_constants(), + used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Builds the synthetic `ReflectionObject` class with ReflectionClass metadata slots. +fn builtin_reflection_object_class() -> FlattenedClass { + let mut class = builtin_reflection_class(); + class.name = "ReflectionObject".to_string(); + class.extends = Some("ReflectionClass".to_string()); + class.properties.push(builtin_property( + "__object", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + if let Some(constructor) = class + .methods + .iter_mut() + .find(|method| method.name == "__construct") + { + *constructor = builtin_reflection_owner_constructor_method(vec![( + "object", + Some(object_type()), + None, + false, + )]); + } + class +} + +/// Builds the synthetic `ReflectionEnum` class with flattened ReflectionClass members. +fn builtin_reflection_enum_class() -> FlattenedClass { + let mut class = builtin_reflection_class(); + class.name = "ReflectionEnum".to_string(); + class + .methods + .retain(|method| reflection_enum_inherited_method_is_supported(&method.name)); + class.properties.extend([ + builtin_property( + "__case_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__cases", + Visibility::Private, + Some(array_type()), + empty_array(), + ), + builtin_property( + "__is_backed", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__backing_type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + ), + ]); + class.methods.extend([ + builtin_reflection_class_bool_method("isBacked", "__is_backed"), + builtin_reflection_class_mixed_method("getBackingType", "__backing_type"), + ]); + class +} + +/// Returns whether a flattened ReflectionClass method is safe on ReflectionEnum. +fn reflection_enum_inherited_method_is_supported(method_name: &str) -> bool { + matches!( + method_name.to_ascii_lowercase().as_str(), + "__construct" + | "__tostring" + | "getname" + | "getshortname" + | "getnamespacename" + | "innamespace" + | "isfinal" + | "isabstract" + | "isinterface" + | "istrait" + | "isenum" + | "isreadonly" + | "isanonymous" + | "isinstantiable" + | "iscloneable" + | "isiterable" + | "isiterateable" + | "isinternal" + | "isuserdefined" + | "getmodifiers" + | "getattributes" + | "getdoccomment" + | "getextensionname" + | "getextension" + | "getfilename" + | "getstartline" + | "getendline" + ) +} + +/// Returns the public modifier constants exposed by PHP's `ReflectionClass`. +fn reflection_class_constants() -> Vec { + vec![ + builtin_class_const("IS_IMPLICIT_ABSTRACT", 16), + builtin_class_const("IS_FINAL", 32), + builtin_class_const("IS_EXPLICIT_ABSTRACT", 64), + builtin_class_const("IS_READONLY", 65_536), + ] +} + +/// Builds a public integer class constant for a synthetic reflection type. +fn builtin_class_const(name: &str, value: i64) -> ClassConst { + ClassConst { + name: name.to_string(), + visibility: Visibility::Public, + is_final: false, + value: Expr::new(ExprKind::IntLiteral(value), crate::span::Span::dummy()), + span: crate::span::Span::dummy(), + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass` string method backed by one private slot. +fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Str), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass` integer method backed by one private slot. +fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Int), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass` membership probe backed by a private string array. +fn builtin_reflection_class_has_name_method( + method_name: &str, + property: &str, + case_insensitive: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name_arg = Expr::new(ExprKind::Variable("name".to_string()), dummy_span); + let needle = if case_insensitive { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("strtolower"), + args: vec![name_arg], + }, + dummy_span, + ) + } else { + name_arg + }; + let haystack = Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ); + let contains = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("in_array"), + args: vec![needle, haystack], + }, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Int), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(Some(contains)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::getConstant()` backed by the private constant-value map. +fn builtin_reflection_class_get_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name_arg = Expr::new(ExprKind::Variable("name".to_string()), dummy_span); + let value = Expr::new(ExprKind::Variable("value".to_string()), dummy_span); + let value_read = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__constants", dummy_span)), + index: Box::new(name_arg), + }, + dummy_span, + ); + let value_is_present = Expr::new( + ExprKind::BinaryOp { + left: Box::new(value.clone()), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::Null, dummy_span)), + }, + dummy_span, + ); + ClassMethod { + name: "getConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::Assign { + name: "value".to_string(), + value: value_read, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: value_is_present, + then_body: vec![Stmt::new(StmtKind::Return(Some(value)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::BoolLiteral(false), dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::getStaticPropertyValue()` backed by the static-property map. +fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let default = variable_expr("default", dummy_span); + let value_read = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__static_properties", dummy_span)), + index: Box::new(name.clone()), + }, + dummy_span, + ); + let key_exists = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("array_key_exists"), + args: vec![ + name, + reflection_this_property("__static_properties", dummy_span), + ], + }, + dummy_span, + ); + let value_or_default = Expr::new( + ExprKind::Ternary { + condition: Box::new(key_exists), + then_expr: Box::new(value_read), + else_expr: Box::new(default), + }, + dummy_span, + ); + ClassMethod { + name: "getStaticPropertyValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("name".to_string(), Some(TypeExpr::Str), None, false), + ( + "default".to_string(), + Some(mixed_type()), + null_expr(), + false, + ), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(value_or_default)), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns the public `ReflectionClass::setStaticPropertyValue()` signature. +fn builtin_reflection_class_set_static_property_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "setStaticPropertyValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("name".to_string(), Some(TypeExpr::Str), None, false), + ("value".to_string(), Some(mixed_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Void), + by_ref_return: false, + body: Vec::new(), + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::getReflectionConstant()` backed by reflected constant objects. +fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let member = variable_expr("member", dummy_span); + let exists = Expr::new( + ExprKind::BinaryOp { + left: Box::new(method_call_expr( + member.clone(), + "getName", + Vec::new(), + dummy_span, + )), + op: BinOp::StrictEq, + right: Box::new(name.clone()), + }, + dummy_span, + ); + ClassMethod { + name: "getReflectionConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__reflection_constants", dummy_span), + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: exists, + then_body: vec![Stmt::new(StmtKind::Return(Some(member)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::implementsInterface()` backed by interface-name metadata. +fn builtin_reflection_class_implements_interface_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let interface_var = Expr::new(ExprKind::Variable("interface".to_string()), dummy_span); + let candidate_var = Expr::new(ExprKind::Variable("interfaceName".to_string()), dummy_span); + let missing_interface_check = Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(function_call( + "interface_exists", + vec![interface_var.clone()], + dummy_span, + ))), + dummy_span, + ), + then_body: vec![ + throw_if_class_like_exists( + "class_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_if_class_like_exists( + "trait_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_if_class_like_exists( + "enum_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_new_reflection_exception( + concat_expr( + concat_expr( + string_lit("Interface \"", dummy_span), + interface_var.clone(), + dummy_span, + ), + string_lit("\" does not exist", dummy_span), + dummy_span, + ), + dummy_span, + ), + ], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + let lowered_interface = strtolower_call(interface_var.clone(), dummy_span); + let lowered_candidate = strtolower_call(candidate_var, dummy_span); + let candidate_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(lowered_candidate), + op: BinOp::Eq, + right: Box::new(lowered_interface.clone()), + }, + dummy_span, + ); + let reflected_name_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(strtolower_call( + reflection_this_property("__name", dummy_span), + dummy_span, + )), + op: BinOp::Eq, + right: Box::new(lowered_interface), + }, + dummy_span, + ); + let interface_self_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(reflection_this_property("__is_interface", dummy_span)), + op: BinOp::And, + right: Box::new(reflected_name_matches), + }, + dummy_span, + ); + ClassMethod { + name: "implementsInterface".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("interface".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + missing_interface_check, + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__interface_names", dummy_span), + key_var: None, + value_var: "interfaceName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: candidate_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: interface_self_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::isSubclassOf()` backed by parent and interface metadata. +fn builtin_reflection_class_is_subclass_of_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let class_var = variable_expr("class", dummy_span); + let target_var = variable_expr("target", dummy_span); + let parent_name_var = variable_expr("parentName", dummy_span); + let interface_name_var = variable_expr("interfaceName", dummy_span); + let target_missing = binary_expr( + binary_expr( + Expr::new( + ExprKind::Not(Box::new(function_call( + "class_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + BinOp::And, + Expr::new( + ExprKind::Not(Box::new(function_call( + "interface_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + dummy_span, + ), + BinOp::And, + binary_expr( + Expr::new( + ExprKind::Not(Box::new(function_call( + "trait_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + BinOp::And, + Expr::new( + ExprKind::Not(Box::new(function_call( + "enum_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + dummy_span, + ), + dummy_span, + ); + let missing_target_check = Stmt::new( + StmtKind::If { + condition: target_missing, + then_body: vec![throw_new_reflection_exception( + concat_expr( + concat_expr( + string_lit("Class \"", dummy_span), + class_var.clone(), + dummy_span, + ), + string_lit("\" does not exist", dummy_span), + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + let parent_matches = binary_expr( + strtolower_call(parent_name_var, dummy_span), + BinOp::Eq, + target_var.clone(), + dummy_span, + ); + let interface_matches = binary_expr( + strtolower_call(interface_name_var, dummy_span), + BinOp::Eq, + target_var.clone(), + dummy_span, + ); + ClassMethod { + name: "isSubclassOf".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("class".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + missing_target_check, + Stmt::new( + StmtKind::Assign { + name: "target".to_string(), + value: strtolower_call(class_var, dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__parent_names", dummy_span), + key_var: None, + value_var: "parentName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: parent_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__interface_names", dummy_span), + key_var: None, + value_var: "interfaceName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: interface_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionClass::isInstance()` backed by PHP's class relation predicate. +fn builtin_reflection_class_is_instance_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isInstance".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::InstanceOf { + value: Box::new(variable_expr("object", dummy_span)), + target: InstanceOfTarget::Expr(Box::new(reflection_this_property( + "__name", dummy_span, + ))), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `if (($interface)) throw new ReflectionException($message);`. +fn throw_if_class_like_exists( + predicate_name: &str, + interface_var: Expr, + message: Expr, + span: crate::span::Span, +) -> Stmt { + Stmt::new( + StmtKind::If { + condition: function_call(predicate_name, vec![interface_var], span), + then_body: vec![throw_new_reflection_exception(message, span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a normal function call expression for synthetic Reflection method bodies. +fn function_call(name: &str, args: Vec, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified(name), + args, + }, + span, + ) +} + +/// Builds a binary expression with the given operator and operands. +fn binary_expr(left: Expr, op: BinOp, right: Expr, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::BinaryOp { + left: Box::new(left), + op, + right: Box::new(right), + }, + span, + ) +} + +/// Builds a PHP string literal expression for synthetic method bodies. +fn string_lit(value: &str, span: crate::span::Span) -> Expr { + Expr::new(ExprKind::StringLiteral(value.to_string()), span) +} + +/// Builds a PHP string concatenation expression. +fn concat_expr(left: Expr, right: Expr, span: crate::span::Span) -> Expr { + binary_expr(left, BinOp::Concat, right, span) +} + +/// Builds `throw new ReflectionException($message)`. +fn throw_new_reflection_exception(message: Expr, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::Throw(Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionException"), + args: vec![message], + }, + span, + )), + span, + ) +} + +/// Builds a `null` expression for synthetic Reflection method bodies. +fn null_value(span: crate::span::Span) -> Expr { + Expr::new(ExprKind::Null, span) +} + +/// Builds `$this->{$property}` for synthetic ReflectionClass method bodies. +fn reflection_this_property(property: &str, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, span)), + property: property.to_string(), + }, + span, + ) +} + +/// Builds a `strtolower()` call around an expression for case-insensitive class names. +fn strtolower_call(expr: Expr, span: crate::span::Span) -> Expr { + function_call("strtolower", vec![expr], span) +} + +/// Builds a variable expression for synthetic Reflection method bodies. +fn variable_expr(name: &str, span: crate::span::Span) -> Expr { + Expr::new(ExprKind::Variable(name.to_string()), span) +} + +/// Builds a method call expression for synthetic Reflection method bodies. +fn method_call_expr(object: Expr, method: &str, args: Vec, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::MethodCall { + object: Box::new(object), + method: method.to_string(), + args, + }, + span, + ) +} + +/// Returns a public `ReflectionClass` array method backed by one private slot. +fn builtin_reflection_class_array_method( + method_name: &str, + property: &str, + return_type: TypeExpr, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(return_type.clone()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass` array method with an optional modifier filter. +fn builtin_reflection_class_filtered_array_method( + method_name: &str, + property: &str, + return_type: TypeExpr, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let filter = variable_expr("filter", dummy_span); + let member = variable_expr("member", dummy_span); + let source = reflection_this_property(property, dummy_span); + let filter_is_null = binary_expr( + filter.clone(), + BinOp::StrictEq, + Expr::new(ExprKind::Null, dummy_span), + dummy_span, + ); + let filter_is_zero = binary_expr( + filter.clone(), + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); + let empty_result = Expr::new(ExprKind::ArrayLiteral(Vec::new()), dummy_span); + let modifier_match = binary_expr( + binary_expr( + method_call_expr(member.clone(), "getModifiers", Vec::new(), dummy_span), + BinOp::BitAnd, + filter, + dummy_span, + ), + BinOp::StrictNotEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![( + "filter".to_string(), + Some(TypeExpr::Nullable(Box::new(TypeExpr::Int))), + null_expr(), + false, + )], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(return_type.clone()), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: filter_is_null, + then_body: vec![Stmt::new( + StmtKind::Return(Some(source.clone())), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: filter_is_zero, + then_body: vec![Stmt::new( + StmtKind::Return(Some(empty_result.clone())), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::TypedAssign { + type_expr: return_type.clone(), + name: "result".to_string(), + value: empty_result, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: source, + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: modifier_match, + then_body: vec![Stmt::new( + StmtKind::ArrayPush { + array: "result".to_string(), + value: member, + }, + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(variable_expr("result", dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass::getMethod()` or `getProperty()` lookup method. +fn builtin_reflection_class_get_member_method( + method_name: &str, + property: &str, + return_class: &str, + case_insensitive: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let member = variable_expr("member", dummy_span); + let member_name = method_call_expr(member.clone(), "getName", Vec::new(), dummy_span); + let left = if case_insensitive { + strtolower_call(member_name, dummy_span) + } else { + member_name + }; + let right = if case_insensitive { + strtolower_call(name.clone(), dummy_span) + } else { + name.clone() + }; + let exists = Expr::new( + ExprKind::BinaryOp { + left: Box::new(left), + op: if case_insensitive { + BinOp::Eq + } else { + BinOp::StrictEq + }, + right: Box::new(right), + }, + dummy_span, + ); + let message = if return_class == "ReflectionMethod" { + concat_expr( + concat_expr( + concat_expr( + reflection_this_property("__name", dummy_span), + string_lit("::", dummy_span), + dummy_span, + ), + name.clone(), + dummy_span, + ), + string_lit("() does not exist", dummy_span), + dummy_span, + ) + } else { + concat_expr( + concat_expr( + concat_expr( + reflection_this_property("__name", dummy_span), + string_lit("::$", dummy_span), + dummy_span, + ), + name.clone(), + dummy_span, + ), + string_lit(" does not exist", dummy_span), + dummy_span, + ) + }; + let message = concat_expr( + string_lit( + if return_class == "ReflectionMethod" { + "Method " + } else { + "Property " + }, + dummy_span, + ), + message, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified(return_class))), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property(property, dummy_span), + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: exists, + then_body: vec![Stmt::new(StmtKind::Return(Some(member)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + throw_new_reflection_exception(message, dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public nullable object `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_nullable_object_method( + method_name: &str, + property: &str, + class_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(nullable_object_type(class_name)), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public object-valued `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_object_method( + method_name: &str, + property: &str, + class_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public mixed `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionClass` boolean method backed by one private slot. +fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionMethod::getPrototype()` backed by a retained prototype reflector. +fn builtin_reflection_method_get_prototype_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getPrototype".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__has_prototype", + dummy_span, + ))), + dummy_span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit("Method does not have a prototype", dummy_span), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__prototype", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::isDefault()` backed by the dynamic-property slot. +fn builtin_reflection_property_is_default_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isDefault".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_dynamic", + dummy_span, + ))), + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public Reflection method that always reports PHP `false` as `string|false`. +fn builtin_reflection_constant_false_union_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(string_or_bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public Reflection predicate that always reports PHP `false`. +fn builtin_reflection_constant_false_bool_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public Reflection method that always reports an empty array. +fn builtin_reflection_constant_empty_array_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(array_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(empty_array()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public Reflection method that always reports PHP `null` as mixed. +fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(null_expr()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a `ReflectionMethod` predicate derived from its case-insensitive method name. +fn builtin_reflection_method_name_predicate_method( + method_name: &str, + expected_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let lower_name = strtolower_call(reflection_this_property("__name", dummy_span), dummy_span); + let comparison = binary_expr( + lower_name, + BinOp::StrictEq, + string_lit(expected_name, dummy_span), + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::hasType()` backed by a nullable private `__type` slot. +fn builtin_reflection_property_has_type_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "hasType".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__type".to_string(), + }, + dummy_span, + )), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::Null, dummy_span)), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public ReflectionProperty predicate over one `__modifiers` bit. +fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let masked_modifiers = Expr::new( + ExprKind::BinaryOp { + left: Box::new(reflection_this_property("__modifiers", dummy_span)), + op: BinOp::BitAnd, + right: Box::new(Expr::new(ExprKind::IntLiteral(mask), dummy_span)), + }, + dummy_span, + ); + let comparison = Expr::new( + ExprKind::BinaryOp { + left: Box::new(masked_modifiers), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::IntLiteral(0), dummy_span)), + }, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::hasHook()` backed by the retained hook method map. +fn builtin_reflection_property_has_hook_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let hook_kind = reflection_property_hook_type_value(dummy_span); + let has_hook = function_call( + "array_key_exists", + vec![ + hook_kind, + reflection_this_property("__hooks", dummy_span), + ], + dummy_span, + ); + ClassMethod { + name: "hasHook".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("type".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(Some(has_hook)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::getHook()` backed by the retained hook method map. +fn builtin_reflection_property_get_hook_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let hook_kind = reflection_property_hook_type_value(dummy_span); + let hooks = reflection_this_property("__hooks", dummy_span); + let hook_method = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(hooks.clone()), + index: Box::new(hook_kind.clone()), + }, + dummy_span, + ); + let has_hook = function_call("array_key_exists", vec![hook_kind, hooks], dummy_span); + ClassMethod { + name: "getHook".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("type".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(nullable_object_type("ReflectionMethod")), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: has_hook, + then_body: vec![Stmt::new( + StmtKind::Return(Some(hook_method)), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(null_value(dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `$type->value` for `PropertyHookType` arguments accepted by hook APIs. +fn reflection_property_hook_type_value(span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::PropertyAccess { + object: Box::new(variable_expr("type", span)), + property: "value".to_string(), + }, + span, + ) +} + +/// Returns `ReflectionProperty::getValue()` for dynamic public instance reflectors. +fn builtin_reflection_property_get_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let object = variable_expr("object", dummy_span); + ClassMethod { + name: "getValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + reflection_property_static_get_value_return(dummy_span), + reflection_property_object_required_guard("getValue", dummy_span), + Stmt::new( + StmtKind::Return(Some(reflection_dynamic_object_property(object, dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::setValue()` for dynamic public instance reflectors. +fn builtin_reflection_property_set_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let object = variable_expr("object", dummy_span); + let value = variable_expr("value", dummy_span); + ClassMethod { + name: "setValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("object".to_string(), Some(mixed_type()), None, false), + ("value".to_string(), Some(mixed_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Void), + by_ref_return: false, + body: vec![ + reflection_property_static_value_guard("setValue", dummy_span), + reflection_property_object_required_guard("setValue", dummy_span), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::Assignment { + target: Box::new(reflection_dynamic_object_property(object, dummy_span)), + value: Box::new(value), + result_target: None, + prelude: Vec::new(), + conditional_value_temp: None, + }, + dummy_span, + )), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::isInitialized()` for supported materialized reflectors. +fn builtin_reflection_property_is_initialized_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let static_return = reflection_property_static_is_initialized_return(dummy_span); + let dynamic_return = Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_dynamic", dummy_span), + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + let defaulted_return = Stmt::new( + StmtKind::If { + condition: reflection_this_property("__has_default_value", dummy_span), + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + ClassMethod { + name: "isInitialized".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + static_return, + reflection_property_object_required_guard("isInitialized", dummy_span), + dynamic_return, + defaulted_return, + throw_new_reflection_exception( + string_lit( + "ReflectionProperty::isInitialized() for typed properties without defaults requires an inline known property", + dummy_span, + ), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a static-property `getValue()` branch backed by the declaring ReflectionClass snapshot. +fn reflection_property_static_get_value_return(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![Stmt::new( + StmtKind::Return(Some(method_call_expr( + reflection_this_property("__declaring_class", span), + "getStaticPropertyValue", + vec![reflection_this_property("__name", span)], + span, + ))), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Returns a static-property `isInitialized()` branch backed by materialized static values. +fn reflection_property_static_is_initialized_return(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![Stmt::new( + StmtKind::Return(Some(function_call( + "array_key_exists", + vec![ + reflection_this_property("__name", span), + method_call_expr( + reflection_this_property("__declaring_class", span), + "getStaticProperties", + Vec::new(), + span, + ), + ], + span, + ))), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a guard for static property value access that still needs inline lowering. +fn reflection_property_static_value_guard(method: &str, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![throw_new_reflection_exception( + string_lit( + &format!( + "ReflectionProperty::{}() for static properties requires an inline known static property", + method + ), + span, + ), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a guard requiring an object argument for instance property value access. +fn reflection_property_object_required_guard(method: &str, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr("object", span), + BinOp::StrictEq, + null_value(span), + span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit( + &format!( + "ReflectionProperty::{}() requires an object for instance properties", + method + ), + span, + ), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds `$object->{$this->__name}` for ReflectionProperty value access. +fn reflection_dynamic_object_property(object: Expr, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::DynamicPropertyAccess { + object: Box::new(object), + property: Box::new(reflection_this_property("__name", span)), + }, + span, + ) +} + +/// Returns `ReflectionProperty::isLazy()` for the non-lazy property model elephc supports. +fn builtin_reflection_property_is_lazy_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isLazy".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::skipLazyInitialization()` as a no-op for non-lazy properties. +fn builtin_reflection_property_skip_lazy_initialization_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let static_modifier = binary_expr( + binary_expr( + reflection_this_property("__modifiers", dummy_span), + BinOp::BitAnd, + Expr::new(ExprKind::IntLiteral(16), dummy_span), + dummy_span, + ), + BinOp::StrictNotEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); + ClassMethod { + name: "skipLazyInitialization".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(TypeExpr::Void), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: static_modifier, + then_body: vec![throw_new_reflection_exception( + string_lit( + "Can not use skipLazyInitialization on static property", + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_virtual", dummy_span), + then_body: vec![throw_new_reflection_exception( + string_lit( + "Can not use skipLazyInitialization on virtual property", + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), } } -/// Builds the `ReflectionClass` shell with a private resolved-name slot, -/// private attribute array slot, public constructor, `getName()`, and -/// `getAttributes()`. -fn builtin_reflection_class() -> FlattenedClass { +/// Builds a `FlattenedClass` for simple reflection owner classes +/// with a private `__attrs` array property and two methods: `__construct` +/// (public, accepting the supplied params) and `getAttributes` (public, +/// returning the `__attrs` array). +fn builtin_reflection_owner_class( + name: &str, + has_name: bool, + constructor_params: Vec<(&str, Option, Option, bool)>, +) -> FlattenedClass { + let mut properties = Vec::new(); + let mut methods = vec![builtin_reflection_owner_constructor_method( + constructor_params, + )]; + if has_name { + properties.push(builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_string_method("getName", "__name")); + } + if reflection_owner_has_doc_comment_method(name) { + methods.push(builtin_reflection_constant_false_union_method( + "getDocComment", + )); + } + if reflection_owner_has_extension_methods(name) { + methods.push(builtin_reflection_constant_false_union_method( + "getExtensionName", + )); + methods.push(builtin_reflection_constant_null_mixed_method( + "getExtension", + )); + } + add_reflection_function_method_origin_methods(name, &mut properties, &mut methods); + add_reflection_member_flag_methods(name, &mut properties, &mut methods); + if matches!( + name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__declaring_class", + Visibility::Private, + Some(mixed_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_object_method( + "getDeclaringClass", + "__declaring_class", + "ReflectionClass", + )); + } + if matches!( + name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "isDeprecated", + )); + methods.push(builtin_reflection_constant_false_bool_method("hasType")); + methods.push(builtin_reflection_constant_null_mixed_method("getType")); + } + if matches!(name, "ReflectionFunction" | "ReflectionMethod") { + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + properties.push(builtin_property( + "__parameters", + Visibility::Private, + Some(object_array_type("ReflectionParameter")), + empty_array(), + )); + properties.push(builtin_property( + "__is_deprecated", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_generator", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + properties.push(builtin_property( + "__has_return_type", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__required_parameter_count", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_array_method( + "getParameters", + "__parameters", + object_array_type("ReflectionParameter"), + )); + methods.push(builtin_reflection_parameter_count_method()); + methods.push(builtin_reflection_class_int_method( + "getNumberOfRequiredParameters", + "__required_parameter_count", + )); + methods.push(builtin_reflection_class_bool_method( + "hasReturnType", + "__has_return_type", + )); + methods.push(builtin_reflection_class_mixed_method("getReturnType", "__type")); + methods.push(builtin_reflection_constant_false_bool_method("isClosure")); + methods.push(builtin_reflection_class_bool_method( + "isDeprecated", + "__is_deprecated", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "returnsReference", + )); + methods.push(builtin_reflection_class_bool_method( + "isGenerator", + "__is_generator", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "hasTentativeReturnType", + )); + methods.push(builtin_reflection_constant_null_mixed_method( + "getTentativeReturnType", + )); + methods.push(builtin_reflection_function_method_is_variadic_method()); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); + } + if name == "ReflectionMethod" { + properties.push(builtin_property( + "__has_prototype", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__prototype", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + methods.push(builtin_reflection_class_bool_method( + "hasPrototype", + "__has_prototype", + )); + methods.push(builtin_reflection_method_get_prototype_method()); + methods.push(builtin_reflection_method_invoke_method()); + methods.push(builtin_reflection_method_invoke_args_method()); + methods.push(builtin_reflection_method_create_from_method_name_method()); + methods.push(builtin_reflection_set_accessible_method()); + } + if name == "ReflectionFunction" { + methods.push(builtin_reflection_function_invoke_method()); + methods.push(builtin_reflection_function_invoke_args_method()); + methods.push(builtin_reflection_constant_empty_array_method( + "getClosureUsedVariables", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "isDisabled", + )); + } + if name == "ReflectionProperty" { + methods.push(builtin_reflection_set_accessible_method()); + } + properties.push(builtin_property( + "__attrs", + Visibility::Private, + Some(array_type()), + empty_array(), + )); + methods.push(builtin_reflection_owner_get_attributes_method()); FlattenedClass { - name: "ReflectionClass".to_string(), + name: name.to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, is_final: true, is_readonly_class: false, - properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property( - "__attrs", - Visibility::Private, - Some(array_type()), - empty_array(), - ), - ], - methods: vec![ - builtin_reflection_owner_constructor_method(vec![( - "class_name", - Some(TypeExpr::Str), - None, - false, - )]), - builtin_reflection_class_get_name_method(), - builtin_reflection_owner_get_attributes_method(), - ], + properties, + methods, attributes: Vec::new(), - constants: Vec::new(), + constants: reflection_owner_constants(name), used_traits: Vec::new(), + trait_aliases: Vec::new(), + } +} + +/// Returns true when PHP exposes `getDocComment()` on this synthetic reflection owner. +fn reflection_owner_has_doc_comment_method(class_name: &str) -> bool { + matches!( + class_name, + "ReflectionFunction" + | "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) +} + +/// Returns true when PHP exposes extension-origin APIs on this reflection owner. +fn reflection_owner_has_extension_methods(class_name: &str) -> bool { + matches!(class_name, "ReflectionFunction" | "ReflectionMethod") +} + +/// Returns public class constants exposed by a synthetic reflection owner. +fn reflection_owner_constants(class_name: &str) -> Vec { + if class_name == "ReflectionMethod" { + return vec![ + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_STATIC", 16), + builtin_class_const("IS_FINAL", 32), + builtin_class_const("IS_ABSTRACT", 64), + ]; + } + if class_name == "ReflectionProperty" { + return vec![ + builtin_class_const("IS_STATIC", 16), + builtin_class_const("IS_READONLY", 128), + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_ABSTRACT", 64), + builtin_class_const("IS_PROTECTED_SET", 2048), + builtin_class_const("IS_PRIVATE_SET", 4096), + builtin_class_const("IS_VIRTUAL", 512), + builtin_class_const("IS_FINAL", 32), + ]; } + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + return vec![ + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_FINAL", 32), + ]; + } + Vec::new() } -/// Returns a public `ReflectionClass::getName()` method that returns the -/// resolved reflected class name from the private `__name` slot. -fn builtin_reflection_class_get_name_method() -> ClassMethod { +/// Builds `getNumberOfParameters()` over the retained parameter array. +fn builtin_reflection_parameter_count_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { - name: "getName".to_string(), + name: "getNumberOfParameters".to_string(), visibility: Visibility::Public, is_static: false, is_abstract: false, is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, - return_type: Some(TypeExpr::Str), + return_type: Some(TypeExpr::Int), by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( - ExprKind::PropertyAccess { - object: Box::new(Expr::new(ExprKind::This, dummy_span)), - property: "__name".to_string(), + ExprKind::FunctionCall { + name: Name::unqualified("count"), + args: vec![Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__parameters".to_string(), + }, + dummy_span, + )], }, dummy_span, ))), @@ -542,34 +4235,435 @@ fn builtin_reflection_class_get_name_method() -> ClassMethod { } } -/// Builds a `FlattenedClass` for `ReflectionMethod` or `ReflectionProperty` -/// with a private `__attrs` array property and two methods: `__construct` -/// (public, accepting the supplied params) and `getAttributes` (public, -/// returning the `__attrs` array). -fn builtin_reflection_owner_class( - name: &str, - constructor_params: Vec<(&str, Option, Option, bool)>, -) -> FlattenedClass { - FlattenedClass { - name: name.to_string(), - extends: None, - implements: Vec::new(), +/// Builds `ReflectionFunctionAbstract::isVariadic()` from the retained parameter list. +fn builtin_reflection_function_method_is_variadic_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let parameters = variable_expr("parameters", dummy_span); + let count = variable_expr("count", dummy_span); + let last_index = binary_expr( + count.clone(), + BinOp::Sub, + Expr::new(ExprKind::IntLiteral(1), dummy_span), + dummy_span, + ); + let last_parameter = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(parameters.clone()), + index: Box::new(last_index), + }, + dummy_span, + ); + ClassMethod { + name: "isVariadic".to_string(), + visibility: Visibility::Public, + is_static: false, is_abstract: false, - is_final: true, - is_readonly_class: false, - properties: vec![builtin_property( - "__attrs", + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::Assign { + name: "parameters".to_string(), + value: reflection_this_property("__parameters", dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Assign { + name: "count".to_string(), + value: function_call("count", vec![parameters], dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: binary_expr( + count.clone(), + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ), + then_body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(method_call_expr( + last_parameter, + "isVariadic", + Vec::new(), + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Adds namespace/name-origin accessors shared by ReflectionFunction and ReflectionMethod. +fn add_reflection_function_method_origin_methods( + class_name: &str, + properties: &mut Vec, + methods: &mut Vec, +) { + if !matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + return; + } + properties.push(builtin_property( + "__short_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + properties.push(builtin_property( + "__namespace_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + properties.push(builtin_property( + "__in_namespace", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_internal", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_user_defined", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_string_method( + "getShortName", + "__short_name", + )); + methods.push(builtin_reflection_class_string_method( + "getNamespaceName", + "__namespace_name", + )); + methods.push(builtin_reflection_class_bool_method( + "inNamespace", + "__in_namespace", + )); + methods.push(builtin_reflection_class_bool_method( + "isInternal", + "__is_internal", + )); + methods.push(builtin_reflection_class_bool_method( + "isUserDefined", + "__is_user_defined", + )); +} + +/// Adds member visibility/staticity predicates for method and property reflection owners. +fn add_reflection_member_flag_methods( + class_name: &str, + properties: &mut Vec, + methods: &mut Vec, +) { + let visibility_flags = [ + ("__is_public", "isPublic"), + ("__is_protected", "isProtected"), + ("__is_private", "isPrivate"), + ]; + if matches!( + class_name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + for (property, method) in visibility_flags { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } + } + if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { + properties.push(builtin_property( + "__is_static", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isStatic", + "__is_static", + )); + } + if class_name == "ReflectionMethod" { + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); + methods.push(builtin_reflection_method_name_predicate_method( + "isConstructor", + "__construct", + )); + methods.push(builtin_reflection_method_name_predicate_method( + "isDestructor", + "__destruct", + )); + } + if class_name == "ReflectionProperty" { + properties.push(builtin_property( + "__type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + properties.push(builtin_property( + "__settable_type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + properties.push(builtin_property( + "__has_default_value", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_promoted", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_virtual", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_dynamic", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__has_hooks", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__hooks", Visibility::Private, Some(array_type()), empty_array(), - )], - methods: vec![ - builtin_reflection_owner_constructor_method(constructor_params), - builtin_reflection_owner_get_attributes_method(), - ], - attributes: Vec::new(), - constants: Vec::new(), - used_traits: Vec::new(), + )); + properties.push(builtin_property( + "__default_value", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); + methods.push(builtin_reflection_property_has_type_method()); + methods.push(builtin_reflection_class_mixed_method("getType", "__type")); + methods.push(builtin_reflection_class_mixed_method( + "getSettableType", + "__settable_type", + )); + methods.push(builtin_reflection_class_bool_method( + "hasDefaultValue", + "__has_default_value", + )); + methods.push(builtin_reflection_class_bool_method( + "isPromoted", + "__is_promoted", + )); + methods.push(builtin_reflection_class_bool_method( + "isVirtual", + "__is_virtual", + )); + methods.push(builtin_reflection_class_bool_method( + "isDynamic", + "__is_dynamic", + )); + methods.push(builtin_reflection_class_bool_method( + "hasHooks", + "__has_hooks", + )); + methods.push(builtin_reflection_class_array_method( + "getHooks", + "__hooks", + array_type(), + )); + methods.push(builtin_reflection_property_has_hook_method()); + methods.push(builtin_reflection_property_get_hook_method()); + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); + methods.push(builtin_reflection_property_is_lazy_method()); + methods.push(builtin_reflection_property_skip_lazy_initialization_method()); + methods.push(builtin_reflection_property_get_value_method()); + methods.push(builtin_reflection_property_set_value_method()); + methods.push(builtin_reflection_property_is_initialized_method()); + methods.push(builtin_reflection_property_modifier_mask_method( + "isProtectedSet", + 2048, + )); + methods.push(builtin_reflection_property_modifier_mask_method( + "isPrivateSet", + 4096, + )); + methods.push(builtin_reflection_property_is_default_method()); + methods.push(builtin_reflection_class_mixed_method( + "getDefaultValue", + "__default_value", + )); + for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } + properties.push(builtin_property( + "__is_readonly", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isReadOnly", + "__is_readonly", + )); + } + if class_name == "ReflectionClassConstant" { + properties.push(builtin_property( + "__value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method("getValue", "__value")); + } + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__is_enum_case", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isEnumCase", + "__is_enum_case", + )); + properties.push(builtin_property( + "__is_final", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isFinal", + "__is_final", + )); + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); + } + if matches!( + class_name, + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__enum", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + methods.push(builtin_reflection_class_mixed_method("getEnum", "__enum")); + properties.push(builtin_property( + "__value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method("getValue", "__value")); + } + if class_name == "ReflectionEnumBackedCase" { + properties.push(builtin_property( + "__backing_value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method( + "getBackingValue", + "__backing_value", + )); + } + if class_name == "ReflectionMethod" { + for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } } } @@ -590,7 +4684,9 @@ fn builtin_reflection_owner_constructor_method( .into_iter() .map(|(name, ty, default, by_ref)| (name.to_string(), ty, default, by_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -612,7 +4708,9 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(array_type()), by_ref_return: false, @@ -631,14 +4729,29 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { } } +/// Marks a synthesized variadic method signature as callable with no variadic arguments. +fn make_reflection_variadic_optional(sig: &mut crate::types::FunctionSig) { + if sig.variadic.is_some() { + if let Some(default) = sig.defaults.last_mut() { + *default = empty_array(); + } + } +} + /// Overrides the return types on the synthesized reflection class methods inside /// `checker` to match PHP's actual signatures: /// - `__construct` → `void` /// - `getName` / `getArguments` → `string` / `array` /// - `newInstance` → `mixed` +/// - `getTarget` / `isRepeated` → `int` / `bool` /// - `getAttributes` → `array` pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(class_info) = checker.classes.get_mut("ReflectionAttribute") { + for (name, ty) in &mut class_info.properties { + if name == "__args" { + *ty = reflection_attribute_args_type(); + } + } if let Some(sig) = class_info.methods.get_mut("__construct") { sig.return_type = PhpType::Void; } @@ -646,31 +4759,495 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getArguments")) { - // Attribute arguments can be keyed (named arguments / associative - // arrays), so the result is an associative array of mixed values. - sig.return_type = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; + sig.return_type = reflection_attribute_args_type(); } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { sig.return_type = PhpType::Mixed; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTarget")) { + sig.return_type = PhpType::Int; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("isRepeated")) { + sig.return_type = PhpType::Bool; + } } - for class_name in ["ReflectionClass", "ReflectionMethod", "ReflectionProperty"] { + for class_name in [ + "ReflectionClass", + "ReflectionObject", + "ReflectionFunction", + "ReflectionMethod", + "ReflectionProperty", + "ReflectionParameter", + "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", + ] { if let Some(class_info) = checker.classes.get_mut(class_name) { if let Some(sig) = class_info.methods.get_mut("__construct") { sig.return_type = PhpType::Void; } - if class_name == "ReflectionClass" { + if matches!( + class_name, + "ReflectionClass" + | "ReflectionObject" + | "ReflectionFunction" + | "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionParameter" + | "ReflectionNamedType" + | "ReflectionUnionType" + | "ReflectionIntersectionType" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getName")) { sig.return_type = PhpType::Str; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getDocComment")) { + sig.return_type = PhpType::Union(vec![PhpType::Str, PhpType::Bool]); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getExtensionName")) + { + sig.return_type = PhpType::Union(vec![PhpType::Str, PhpType::Bool]); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getExtension")) { + sig.return_type = PhpType::Mixed; + } + if matches!( + class_name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getDeclaringClass")) + { + sig.return_type = PhpType::Object("ReflectionClass".to_string()); + } + } + if matches!(class_name, "ReflectionClass" | "ReflectionObject") { + for (property_name, property_type) in &mut class_info.properties { + match property_name.as_str() { + "__interfaces" | "__traits" => { + *property_type = reflection_class_object_map_type(); + } + "__trait_aliases" => { + *property_type = reflection_string_map_type(); + } + "__static_properties" => { + *property_type = reflection_static_properties_map_type(); + } + _ => {} + } + } + for method_name in [ + "isfinal", + "isabstract", + "isinterface", + "istrait", + "isenum", + "isreadonly", + "isanonymous", + "isinstantiable", + "iscloneable", + "isiterable", + "isiterateable", + "isinternal", + "isuserdefined", + "hasmethod", + "hasproperty", + "implementsinterface", + "issubclassof", + "isinstance", + ] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + for method_name in ["getinterfacenames", "gettraitnames"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Array(Box::new(PhpType::Str)); + } + } + for method_name in ["getinterfaces", "gettraits"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = reflection_class_object_map_type(); + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTraitAliases")) { + sig.return_type = reflection_string_map_type(); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethods")) { + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethod")) { + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getReflectionConstants")) + { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionClassConstant".to_string(), + ))); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getReflectionConstant")) + { + sig.return_type = PhpType::Mixed; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperties")) { + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionProperty".to_string()))); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperty")) { + sig.return_type = PhpType::Object("ReflectionProperty".to_string()); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getStaticProperties")) + { + sig.return_type = reflection_static_properties_map_type(); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getConstructor")) + { + sig.return_type = PhpType::Union(vec![ + PhpType::Object("ReflectionMethod".to_string()), + PhpType::Void, + ]); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getParentClass")) + { + sig.return_type = PhpType::Mixed; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { + sig.return_type = PhpType::Object(String::new()); + sig.variadic = Some("args".to_string()); + let variadic_default = Some(Expr::new( + ExprKind::ArrayLiteral(Vec::new()), + crate::span::Span::dummy(), + )); + if let Some(index) = sig.params.iter().position(|(name, _)| name == "args") { + while sig.defaults.len() <= index { + sig.defaults.push(None); + } + sig.defaults[index] = variadic_default; + } else { + sig.params + .push(("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); + sig.param_type_exprs.push(None); + sig.defaults.push(variadic_default); + sig.ref_params.push(false); + sig.declared_params.push(false); + } + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("newInstanceArgs")) + { + sig.return_type = PhpType::Mixed; + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("newInstanceWithoutConstructor")) + { + sig.return_type = PhpType::Mixed; + } + } + if matches!( + class_name, + "ReflectionClass" | "ReflectionObject" | "ReflectionEnum" + ) { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } + if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { + for method_name in ["isstatic", "ispublic", "isprotected", "isprivate"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("setAccessible")) { + sig.return_type = PhpType::Void; + } + } + if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + for method_name in ["getShortName", "getNamespaceName"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Str; + } + } + for method_name in [ + "inNamespace", + "isInternal", + "isUserDefined", + "hasReturnType", + "isVariadic", + "isClosure", + "isDeprecated", + "returnsReference", + "isGenerator", + "hasTentativeReturnType", + ] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Bool; + } + } + for method_name in ["getReturnType", "getTentativeReturnType"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } + } + } + if class_name == "ReflectionProperty" { + for (property_name, property_type) in &mut class_info.properties { + if property_name == "__hooks" { + *property_type = reflection_property_hook_map_type(); + } + } + for method_name in [ + "isfinal", + "isabstract", + "isreadonly", + "isdefault", + "ispromoted", + "isvirtual", + "isdynamic", + "hashooks", + "isinitialized", + "isprotectedset", + "isprivateset", + ] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("hasDefaultValue")) + { + sig.return_type = PhpType::Bool; + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getDefaultValue")) + { + sig.return_type = PhpType::Mixed; + } + for method_name in ["getType", "getSettableType"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getHooks")) { + sig.return_type = reflection_property_hook_map_type(); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("hasHook")) { + sig.params = vec![( + "type".to_string(), + PhpType::Object("PropertyHookType".to_string()), + )]; + sig.param_type_exprs = + vec![Some(TypeExpr::Named(Name::unqualified("PropertyHookType")))]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Bool; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getHook")) { + sig.params = vec![( + "type".to_string(), + PhpType::Object("PropertyHookType".to_string()), + )]; + sig.param_type_exprs = + vec![Some(TypeExpr::Named(Name::unqualified("PropertyHookType")))]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Union(vec![ + PhpType::Object("ReflectionMethod".to_string()), + PhpType::Void, + ]); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } + if class_name == "ReflectionMethod" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + for method_name in [ + "isfinal", + "isabstract", + "isconstructor", + "isdestructor", + "hasPrototype", + ] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPrototype")) { + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { + sig.return_type = PhpType::Mixed; + sig.variadic = Some("args".to_string()); + make_reflection_variadic_optional(sig); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { + // Keep the shell's (object, args) parameters: ReflectionMethod::invokeArgs + // takes the receiver first, and replacing the params with a + // lone args array breaks invoke dispatch. + sig.return_type = PhpType::Mixed; + } + if let Some(sig) = + class_info.methods.get_mut(&php_symbol_key("createFromMethodName")) + { + sig.params = vec![("method".to_string(), PhpType::Str)]; + sig.param_type_exprs = vec![Some(TypeExpr::Str)]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionParameter".to_string(), + ))); + } + for method_name in ["getNumberOfParameters", "getNumberOfRequiredParameters"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Int; + } + } + } + if class_name == "ReflectionFunction" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { + sig.return_type = PhpType::Mixed; + sig.variadic = Some("args".to_string()); + make_reflection_variadic_optional(sig); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { + sig.return_type = PhpType::Mixed; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionParameter".to_string(), + ))); + } + for method_name in ["getNumberOfParameters", "getNumberOfRequiredParameters"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Int; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("isDisabled")) { + sig.return_type = PhpType::Bool; + } + } + if class_name == "ReflectionParameter" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { + sig.return_type = PhpType::Int; + } + for method_name in [ + "isoptional", + "isvariadic", + "ispassedbyreference", + "canbepassedbyvalue", + "ispromoted", + "hastype", + "allowsnull", + "isarray", + "iscallable", + "isdefaultvalueavailable", + "isdefaultvalueconstant", + ] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + for method_name in ["getType", "getDefaultValue", "getDefaultValueConstantName"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } + } + } + if class_name == "ReflectionNamedType" { + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } + if class_name == "ReflectionUnionType" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionNamedType".to_string(), + ))); + } + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } + if class_name == "ReflectionIntersectionType" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionNamedType".to_string(), + ))); + } + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { - sig.return_type = PhpType::Array(Box::new(PhpType::Object( - "ReflectionAttribute".to_string(), - ))); + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); } } } @@ -686,9 +5263,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } if let Some(class_info) = checker.classes.get_mut("ReflectionParameter") { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getType")) { - // ?ReflectionNamedType — null for untyped parameters. + // ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null. sig.return_type = PhpType::Union(vec![ PhpType::Object("ReflectionNamedType".to_string()), + PhpType::Object("ReflectionUnionType".to_string()), + PhpType::Object("ReflectionIntersectionType".to_string()), PhpType::Void, ]); } diff --git a/src/types/checker/builtin_user_filter.rs b/src/types/checker/builtin_user_filter.rs index 748e7ca0ab..d3db6d3c32 100644 --- a/src/types/checker/builtin_user_filter.rs +++ b/src/types/checker/builtin_user_filter.rs @@ -37,6 +37,7 @@ pub(crate) fn inject_builtin_user_filter( "php_user_filter".to_string(), FlattenedClass { name: "php_user_filter".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -47,6 +48,7 @@ pub(crate) fn inject_builtin_user_filter( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -67,6 +69,7 @@ fn params_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new(ExprKind::Null, Span::dummy())), span: Span::dummy(), attributes: Vec::new(), diff --git a/src/types/checker/builtins/arrays.rs b/src/types/checker/builtins/arrays.rs index 0667b98407..2ca794e653 100644 --- a/src/types/checker/builtins/arrays.rs +++ b/src/types/checker/builtins/arrays.rs @@ -9,7 +9,7 @@ //! - Signatures, callable aliases, optimizer effects, and codegen builtin dispatch must remain in lockstep. use crate::errors::CompileError; -use crate::parser::ast::Expr; +use crate::parser::ast::{Expr, ExprKind}; use crate::types::{PhpType, TypeEnv}; use super::super::Checker; @@ -46,16 +46,7 @@ pub(super) fn check_builtin( return Err(CompileError::new(span, "isset() takes at least 1 argument")); } for arg in args { - // `isset($obj->prop)` on an undeclared property dispatches to - // `__isset`; the helper infers the receiver but skips the bare - // property access that would otherwise reject the property. - if checker - .isset_unset_property_magic_class(arg, "__isset", env)? - .is_some() - { - continue; - } - checker.infer_type(arg, env)?; + check_isset_arg(checker, arg, env)?; } Ok(Some(PhpType::Bool)) } @@ -63,6 +54,31 @@ pub(super) fn check_builtin( } } +/// Type-checks one `isset()` operand while preserving PHP's non-reading property semantics. +fn check_isset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + if let ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } = &arg.kind + { + let object_ty = checker.infer_type(object, env)?; + if isset_object_receiver_type(checker, &object_ty) { + return Ok(()); + } + } + checker.infer_type(arg, env).map(|_| ()) +} + +/// Returns true when `isset($object->property)` can be checked without reading the property. +fn isset_object_receiver_type(checker: &Checker, ty: &PhpType) -> bool { + match ty { + PhpType::Object(_) | PhpType::Mixed => true, + PhpType::Union(members) => { + checker.union_single_object_class(ty).is_some() + || members.iter().any(|member| matches!(member, PhpType::Mixed)) + } + _ => false, + } +} + /// Returns `true` if a `PhpType` is a countable array type for Union membership checks. /// /// Used by `crate::builtins::array::count` to test whether every branch of a Union type diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index c8f40a4d80..37b45854e9 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -647,6 +647,7 @@ fn callback_descriptor_env_ownership(callback: &Expr) -> CallbackDescriptorEnvOw | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } => CallbackDescriptorEnvOwnership::Owned, ExprKind::Ternary { then_expr, @@ -964,6 +965,16 @@ pub(crate) fn check_call_user_func_array( let ret_ty = checker.check_function_call(&cb_name, &spread_args, span, env)?; return Ok(ret_ty); } + let arg_array_ty = checker.infer_type(&args[1], env)?; + if checker.eval_barrier_active && !cb_name.contains("::") { + if !call_user_func_array_arg_container_is_supported(&arg_array_ty) { + return Err(CompileError::new( + args[1].span, + "call_user_func_array() second argument must be an array", + )); + } + return Ok(PhpType::Mixed); + } // A string-literal callback that matched no extern, builtin, user function, // or fn_decl is an undefined function. Reject plain function-name callbacks // here instead of falling through to the generic `Str` acceptance below diff --git a/src/types/checker/builtins/callables/preg_replace_callback.rs b/src/types/checker/builtins/callables/preg_replace_callback.rs index 400909efcd..242a5a1ae9 100644 --- a/src/types/checker/builtins/callables/preg_replace_callback.rs +++ b/src/types/checker/builtins/callables/preg_replace_callback.rs @@ -58,6 +58,7 @@ fn contextual_closure_sig( let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -78,6 +79,7 @@ fn contextual_closure_sig( let mut closure_env = env.clone(); let mut param_types = Vec::new(); + let mut param_type_exprs = Vec::new(); let mut defaults = Vec::new(); let mut ref_params = Vec::new(); let mut declared_params = Vec::new(); @@ -119,6 +121,7 @@ fn contextual_closure_sig( closure_env.insert(name.clone(), env_ty); param_types.push((name.clone(), sig_ty)); + param_type_exprs.push(type_ann.clone()); defaults.push(default.clone()); ref_params.push(*is_ref); declared_params.push(declared); @@ -127,8 +130,9 @@ fn contextual_closure_sig( if let Some(name) = variadic { closure_env.insert(name.clone(), PhpType::Array(Box::new(PhpType::Int))); param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + param_type_exprs.push(None); defaults.push(None); - ref_params.push(false); + ref_params.push(*variadic_by_ref); declared_params.push(false); } @@ -136,6 +140,8 @@ fn contextual_closure_sig( checker.resolve_closure_return_type(body, return_type, callback.span, &closure_env)?; Ok(Some(FunctionSig { params: param_types, + param_type_exprs, + param_attributes: Vec::new(), defaults, return_type, declared_return, diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index 37f08b92d1..de98768dc2 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -10,6 +10,8 @@ //! - `SUPPORTED_BUILTIN_FUNCTIONS` is the source of truth for PHP-visible builtin names. //! - `INTERNAL_BUILTIN_FUNCTIONS` is now an empty placeholder; internal builtins are //! registered via `internal: true` in `src/builtins/` and recognized through the registry. +//! - `LANGUAGE_CONSTRUCT_FUNCTIONS` participates in call resolution but stays +//! hidden from `function_exists()` and first-class callable surfaces. const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ "buffer_free", @@ -18,7 +20,14 @@ const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ "die", "empty", "exit", + "is_double", + "is_integer", + "is_long", + "is_real", "isset", + "method_exists", + "property_exists", + "strval", "unset", ]; @@ -28,10 +37,14 @@ const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ // `is_supported_builtin_function_exact` compiles unchanged. const INTERNAL_BUILTIN_FUNCTIONS: &[&str] = &[]; -/// Checks if the exact (lowercase) name is in the PHP-visible or internal builtin lists. +const LANGUAGE_CONSTRUCT_FUNCTIONS: &[&str] = &["eval"]; + +/// Checks if the exact (lowercase) name is in any callable-resolution builtin list. /// Does not perform case folding; use `is_supported_builtin_function` for case-insensitive lookup. fn is_supported_builtin_function_exact(name: &str) -> bool { - SUPPORTED_BUILTIN_FUNCTIONS.contains(&name) || INTERNAL_BUILTIN_FUNCTIONS.contains(&name) + SUPPORTED_BUILTIN_FUNCTIONS.contains(&name) + || INTERNAL_BUILTIN_FUNCTIONS.contains(&name) + || LANGUAGE_CONSTRUCT_FUNCTIONS.contains(&name) } /// Returns the union of PHP-visible supported builtin function names from the diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 2a25e0a38d..6249bc84ca 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -65,7 +65,8 @@ impl Checker { // undeclared property routed to `__isset`/`__unset`, which must not be // eagerly inferred by argument normalization. Their handlers inspect the // raw operands directly. - let is_lazy_construct = matches!(name, "isset" | "unset"); + let builtin_key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + let is_lazy_construct = matches!(builtin_key.as_str(), "isset" | "unset"); let normalized_args; let args = if let Some(sig) = (!is_lazy_construct).then(|| crate::types::builtin_call_sig(name)).flatten() @@ -82,6 +83,17 @@ impl Checker { args }; + if name == "eval" { + // eval is not registry-backed, and argument normalization tolerates + // zero-arg calls (trailing defaults are trimmed), so arity must be + // enforced here before the fast-path return. + if args.len() != 1 { + return Err(CompileError::new(span, "eval() takes exactly 1 argument")); + } + self.infer_type(&args[0], env)?; + return Ok(Some(PhpType::Mixed)); + } + // Registry-first: if the builtin is registered, use its spec to check arity // and derive the return type (or call the spec's check hook for refined types). // Falls through to the legacy per-area dispatch when the name is not registered. diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index 632c420d43..5bf8f2cfbb 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -24,6 +24,8 @@ type BuiltinResult = Result, CompileError>; /// arity mismatch. /// /// ## Supported builtins +/// - Legacy scalar aliases not yet migrated into `src/builtins/`: `strval`, +/// `is_double`, `is_real`, `is_integer`, `is_long` /// - Control: `exit`, `die`, `empty` /// - Unset: `unset` /// - Buffers: `buffer_len`, `buffer_free` @@ -58,6 +60,34 @@ pub(super) fn check_builtin( } Ok(Some(PhpType::Void)) } + "strval" => { + if args.len() != 1 { + return Err(CompileError::new(span, "strval() takes exactly 1 argument")); + } + checker.infer_type(&args[0], env)?; + Ok(Some(PhpType::Str)) + } + "is_double" | "is_real" | "is_integer" | "is_long" => { + if args.len() != 1 { + return Err(CompileError::new( + span, + &format!("{}() takes exactly 1 argument", name), + )); + } + checker.infer_type(&args[0], env)?; + Ok(Some(PhpType::Bool)) + } + "method_exists" | "property_exists" => { + if args.len() != 2 { + return Err(CompileError::new( + span, + &format!("{}() takes exactly 2 arguments", name), + )); + } + checker.infer_type(&args[0], env)?; + checker.infer_type(&args[1], env)?; + Ok(Some(PhpType::Bool)) + } "empty" => { if args.len() != 1 { return Err(CompileError::new(span, "empty() takes exactly 1 argument")); @@ -70,16 +100,7 @@ pub(super) fn check_builtin( return Err(CompileError::new(span, "unset() takes at least 1 argument")); } for arg in args { - // `unset($obj->prop)` on an undeclared property dispatches to - // `__unset`; the helper infers the receiver but skips the bare - // property access that would otherwise reject the property. - if checker - .isset_unset_property_magic_class(arg, "__unset", env)? - .is_some() - { - continue; - } - checker.infer_type(arg, env)?; + check_unset_arg(checker, arg, env)?; } Ok(Some(PhpType::Void)) } @@ -141,3 +162,77 @@ pub(super) fn check_builtin( _ => Ok(None), } } + +/// Type-checks one `unset()` operand while preserving PHP's non-reading property semantics. +fn check_unset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + if let ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } = &arg.kind + { + let object_ty = checker.infer_type(object, env)?; + if unset_object_property_probe_is_valid(checker, &object_ty, property, arg)? { + return Ok(()); + } + } + checker.infer_type(arg, env).map(|_| ()) +} + +/// Returns true when `unset($object->property)` can be checked without reading the property. +fn unset_object_property_probe_is_valid( + checker: &Checker, + object_ty: &PhpType, + property: &str, + arg: &Expr, +) -> Result { + match object_ty { + PhpType::Object(class_name) => { + unset_property_probe_is_valid_on_class(checker, class_name, property, arg) + } + PhpType::Mixed => Ok(true), + PhpType::Union(members) => { + if let Some(class_name) = checker.union_single_object_class(object_ty) { + unset_property_probe_is_valid_on_class(checker, &class_name, property, arg) + } else { + Ok(members.iter().any(|member| matches!(member, PhpType::Mixed))) + } + } + _ => Ok(false), + } +} + +/// Checks one known receiver class for PHP `unset($object->property)` magic/no-op legality. +fn unset_property_probe_is_valid_on_class( + checker: &Checker, + class_name: &str, + property: &str, + arg: &Expr, +) -> Result { + if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { + return Ok(true); + } + let Some(class_info) = checker.classes.get(class_name) else { + return Ok(false); + }; + if let Some(visibility) = class_info.property_visibilities.get(property) { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + if !checker.can_access_member(declaring_class, visibility) { + if class_info.methods.contains_key("__unset") { + return Ok(true); + } + return Err(CompileError::new( + arg.span, + &format!( + "Cannot access {} property: {}::{}", + Checker::visibility_label(visibility), + class_name, + property + ), + )); + } + return Ok(false); + } + Ok(true) +} diff --git a/src/types/checker/callables/closures.rs b/src/types/checker/callables/closures.rs index 6ddd323a24..3d27d1979b 100644 --- a/src/types/checker/callables/closures.rs +++ b/src/types/checker/callables/closures.rs @@ -22,6 +22,7 @@ use super::super::Checker; /// environment bindings, default value expressions, by-reference flags, and declared-param flags. pub(crate) struct ClosureSignatureContext { pub params: Vec<(String, PhpType)>, + pub param_type_exprs: Vec>, pub env: TypeEnv, pub defaults: Vec>, pub ref_params: Vec, @@ -38,12 +39,19 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, captures: &[String], span: Span, env: &TypeEnv, ) -> Result { self.prepare_closure_signature_context_with_param_hints( - params, variadic, captures, span, env, &[], + params, + variadic, + variadic_by_ref, + captures, + span, + env, + &[], ) } @@ -60,6 +68,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, captures: &[String], span: Span, env: &TypeEnv, @@ -76,6 +85,7 @@ impl Checker { let mut closure_env = env.clone(); let mut param_types = Vec::new(); + let mut param_type_exprs = Vec::new(); let mut defaults = Vec::new(); let mut ref_params = Vec::new(); let mut declared_params = Vec::new(); @@ -104,6 +114,7 @@ impl Checker { closure_env.insert(name.clone(), env_ty); param_types.push((name.clone(), sig_ty)); + param_type_exprs.push(type_ann.clone()); defaults.push(default.clone()); ref_params.push(*is_ref); declared_params.push(type_ann.is_some()); @@ -112,13 +123,15 @@ impl Checker { if let Some(name) = variadic { closure_env.insert(name.clone(), PhpType::Array(Box::new(PhpType::Int))); param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + param_type_exprs.push(None); defaults.push(None); - ref_params.push(false); + ref_params.push(variadic_by_ref); declared_params.push(false); } Ok(ClosureSignatureContext { params: param_types, + param_type_exprs, env: closure_env, defaults, ref_params, @@ -217,6 +230,7 @@ impl Checker { ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -227,6 +241,7 @@ impl Checker { let closure_sig = self.prepare_closure_signature_context( params, variadic, + *variadic_by_ref, captures, expr.span, env, @@ -239,6 +254,8 @@ impl Checker { )?; Ok(Some(FunctionSig { params: closure_sig.params, + param_type_exprs: closure_sig.param_type_exprs, + param_attributes: Vec::new(), defaults: closure_sig.defaults, return_type, declared_return, diff --git a/src/types/checker/callables/first_class.rs b/src/types/checker/callables/first_class.rs index 1e7a8b4513..ef7cb828d9 100644 --- a/src/types/checker/callables/first_class.rs +++ b/src/types/checker/callables/first_class.rs @@ -53,6 +53,8 @@ impl Checker { if let Some(sig) = self.extern_functions.get(function_name) { return Ok(FunctionSig { params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, diff --git a/src/types/checker/driver/externs.rs b/src/types/checker/driver/externs.rs index 070a957cde..50005b710f 100644 --- a/src/types/checker/driver/externs.rs +++ b/src/types/checker/driver/externs.rs @@ -112,6 +112,8 @@ impl Checker { } let sig = FunctionSig { params: php_params.clone(), + param_type_exprs: vec![None; php_params.len()], + param_attributes: Vec::new(), defaults: params.iter().map(|_| None).collect(), return_type: php_ret.clone(), declared_return: true, diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 954dd699c7..a03bdc6e99 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -53,7 +53,9 @@ impl Checker { if let StmtKind::FunctionDecl { name, params, + param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -83,15 +85,20 @@ impl Checker { params.iter().map(|(_, t, _, _)| t.clone()).collect(); let defaults: Vec> = params.iter().map(|(_, _, d, _)| d.clone()).collect(); - let ref_flags: Vec = params.iter().map(|(_, _, _, r)| *r).collect(); + let mut ref_flags: Vec = params.iter().map(|(_, _, _, r)| *r).collect(); + if variadic.is_some() { + ref_flags.push(*variadic_by_ref); + } self.fn_decls.insert( name.clone(), FnDecl { params: param_names, param_types: param_type_anns, + param_attributes: param_attributes.clone(), defaults, ref_params: ref_flags, variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), by_ref_return: *by_ref_return, @@ -270,6 +277,13 @@ impl Checker { let param_types = self.initial_function_param_types(first_variant, &decl)?; Ok(Some(FunctionSig { params: param_types, + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults, return_type: crate::types::PhpType::Int, declared_return: decl.return_type.is_some(), @@ -279,7 +293,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic, deprecation: None, diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 857768be73..f8eb8f2b82 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -15,8 +15,8 @@ use crate::types::array_constants::ARRAY_INT_CONSTANTS; use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::ent_constants::ENT_INT_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; -use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; +use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::PhpType; use super::super::Checker; @@ -91,11 +91,15 @@ impl Checker { callable_captures: HashMap::new(), callable_array_targets: HashMap::new(), first_class_callable_targets: HashMap::new(), + reflection_class_targets: HashMap::new(), interfaces: HashMap::new(), classes: HashMap::new(), declared_classes: HashSet::new(), enums: HashMap::new(), declared_interfaces: HashSet::new(), + declared_traits: HashSet::new(), + declared_trait_methods: HashMap::new(), + declared_trait_constants: HashMap::new(), current_class: None, current_method: None, current_method_is_static: false, @@ -111,6 +115,7 @@ impl Checker { active_globals: HashSet::new(), active_statics: HashSet::new(), foreach_key_locals: HashSet::new(), + eval_barrier_active: false, break_continue_depth: 0, finally_break_continue_bases: Vec::new(), warnings: Vec::new(), diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 194084619b..531d9b064b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -15,8 +15,9 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{ClassMethod, Program, Stmt, StmtKind}; use crate::types::{ + callable_wrapper_sig, traits::{flatten_classes, FlattenedClass}, - TypeEnv, + FunctionSig, PhpType, TypeEnv, }; use super::builtin_types::{ @@ -27,9 +28,7 @@ use super::builtin_types::{ patch_magic_method_signatures, InterfaceDeclInfo, }; use super::builtin_enums::inject_builtin_enums; -use super::builtin_interfaces::{ - apply_implicit_stringable_interfaces, inject_builtin_interfaces, -}; +use super::builtin_interfaces::{apply_implicit_stringable_interfaces, inject_builtin_interfaces}; use super::builtin_iterators::{inject_builtin_iterators, patch_builtin_generator_signatures}; use super::builtin_json::{inject_builtin_json_interfaces, patch_builtin_json_signatures}; use super::builtin_spl_classes::{ @@ -77,20 +76,17 @@ pub(super) fn check_types_impl( checker.collect_function_decls(program, &mut errors); - let (mut flattened_classes, flatten_errors) = flatten_classes(program); + let (mut flattened_classes, mut flattened_enums, flatten_errors) = flatten_classes(program); errors.extend(flatten_errors); // Resolve the relative class types `self`/`static`/`parent` in every member type annotation // now that inheritance and trait flattening have settled the concrete enclosing class. This // single pass feeds the schema signatures, the body-check pass, and codegen (which all read // the flattened method/property declarations), so no later stage sees a symbolic `self`. substitute_relative_class_types_in_flattened(&mut flattened_classes); - let declared_traits: HashSet = program - .iter() - .filter_map(|stmt| match &stmt.kind { - StmtKind::TraitDecl { name, .. } => Some(name.clone()), - _ => None, - }) - .collect(); + substitute_relative_class_types_in_flattened_enums(&mut flattened_enums); + let declared_traits = collect_declared_trait_names(program); + let declared_trait_methods = collect_declared_trait_methods(program); + let declared_trait_constants = collect_declared_trait_constants(program); let mut seen_classes = HashSet::new(); let mut class_map = HashMap::new(); for class in &flattened_classes { @@ -181,13 +177,15 @@ pub(super) fn check_types_impl( if let Err(error) = inject_builtin_user_filter(&mut class_map) { errors.extend(error.flatten()); } - if let Err(error) = - inject_builtin_reflection(&interface_map, &mut class_map, &declared_traits) + if let Err(error) = inject_builtin_reflection(&interface_map, &mut class_map, &declared_traits) { errors.extend(error.flatten()); } checker.declared_classes = class_map.keys().cloned().collect(); checker.declared_interfaces = interface_map.keys().cloned().collect(); + checker.declared_traits = declared_traits.clone(); + checker.declared_trait_methods = declared_trait_methods; + checker.declared_trait_constants = declared_trait_constants; // Enum names must resolve as types in member positions (property and // promoted-constructor-param types), which are checked during the class // schema pass — before the enum-processing phase populates `enums`. Pre- @@ -239,15 +237,30 @@ pub(super) fn check_types_impl( implements, methods, constants, + .. } = &stmt.kind { + let enum_methods = flattened_enums + .get(name) + .map(|flattened| flattened.methods.as_slice()) + .unwrap_or(methods.as_slice()); + let enum_used_traits = flattened_enums + .get(name) + .map(|flattened| flattened.used_traits.as_slice()) + .unwrap_or(&[]); + let enum_trait_aliases = flattened_enums + .get(name) + .map(|flattened| flattened.trait_aliases.as_slice()) + .unwrap_or(&[]); if let Err(error) = build_enum_info( name, backing_type.as_ref(), cases, implements, - methods, + enum_methods, constants, + enum_used_traits, + enum_trait_aliases, stmt.span, &mut checker, &mut next_class_id, @@ -284,7 +297,7 @@ pub(super) fn check_types_impl( // the enum schema pass), so they would otherwise skip body checking entirely. Flatten them // into method-checkable units here — their signatures already live in `checker.classes`. let mut methods_to_check = flattened_classes.clone(); - methods_to_check.extend(flatten_enum_methods(program)); + methods_to_check.extend(flatten_enum_methods(program, &flattened_enums)); checker.type_check_methods_until_stable(&methods_to_check, &global_env, &mut errors)?; patch_builtin_spl_storage_signatures(&mut checker); apply_implicit_stringable_interfaces(&mut checker.classes); @@ -310,19 +323,161 @@ pub(super) fn check_types_impl( Ok((checker, final_global_env)) } -/// Resolves the relative class types `self`/`static`/`parent` to concrete class names across -/// every flattened class's method parameter, method return, and property type annotations. +/// Collects source-declared trait names recursively, including namespace blocks. +fn collect_declared_trait_names(program: &Program) -> HashSet { + let mut names = HashSet::new(); + collect_declared_trait_names_into(program, &mut names); + names +} + +/// Pushes recursive source-declared trait names into `names`. +fn collect_declared_trait_names_into(program: &Program, names: &mut HashSet) { + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { name, .. } => { + names.insert(name.clone()); + } + StmtKind::NamespaceBlock { body, .. } => { + collect_declared_trait_names_into(body, names); + } + _ => {} + } + } +} + +/// Collects source-declared trait method signatures recursively, including namespace blocks. +fn collect_declared_trait_methods( + program: &Program, +) -> HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| { + ( + php_symbol_key(&method.name), + trait_method_reflection_sig(method), + ) + }) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_methods(body)); + } + _ => {} + } + } + methods +} + +/// Collects source-declared trait constant names recursively, including namespace blocks. +fn collect_declared_trait_constants(program: &Program) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constants(body)); + } + _ => {} + } + } + constants +} + +/// Builds the reflection-visible signature for a direct trait method. /// -/// `self`/`static` resolve to the flattened class itself and `parent` to its `extends` target. -/// Because trait methods are already merged into the using class at this point, a trait method's -/// `self` correctly resolves to the using class rather than the trait. Annotations with no -/// relative type are left untouched. +/// Trait direct reflection only needs parameter names, defaults, by-reference +/// flags, variadic shape, and declared-type presence; class-relative type names +/// are resolved when the trait is flattened into a concrete class. +fn trait_method_reflection_sig(method: &ClassMethod) -> FunctionSig { + let params = method + .params + .iter() + .map(|(name, type_ann, _, _)| { + ( + name.clone(), + if type_ann.is_some() { + PhpType::Mixed + } else { + PhpType::Int + }, + ) + }) + .collect(); + let defaults = method + .params + .iter() + .map(|(_, _, default, _)| default.clone()) + .collect(); + let mut ref_params: Vec = method + .params + .iter() + .map(|(_, _, _, by_ref)| *by_ref) + .collect(); + if method.variadic.is_some() { + ref_params.push(method.variadic_by_ref); + } + callable_wrapper_sig(&FunctionSig { + params, + param_type_exprs: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) + .collect(), + param_attributes: method.param_attributes.clone(), + defaults, + return_type: PhpType::Mixed, + declared_return: method.return_type.is_some(), + by_ref_return: method.by_ref_return, + ref_params, + declared_params: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.is_some()) + .chain( + method + .variadic + .iter() + .map(|_| method.variadic_type.is_some()), + ) + .collect(), + variadic: method.variadic.clone(), + deprecation: None, + }) +} + /// Builds method-checkable `FlattenedClass` units for every `enum` in the program so their method /// bodies go through the same validation as class methods. Enum signatures are already registered /// in `checker.classes` by the enum schema pass; these units only carry the names and method /// bodies the method-check pass needs. The relative types `self`/`static` resolve to the enum /// itself (enums have no parent). -fn flatten_enum_methods(program: &[Stmt]) -> Vec { +fn flatten_enum_methods( + program: &[Stmt], + flattened_enums: &HashMap, +) -> Vec { let mut units = Vec::new(); for stmt in program { if let StmtKind::EnumDecl { @@ -333,10 +488,18 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { .. } = &stmt.kind { + if let Some(flattened) = flattened_enums.get(name) { + units.push(flattened.clone()); + continue; + } let mut flattened = FlattenedClass { name: name.clone(), + span: stmt.span, extends: None, - implements: implements.iter().map(|name| name.as_str().to_string()).collect(), + implements: implements + .iter() + .map(|name| name.as_str().to_string()) + .collect(), is_abstract: false, is_final: true, is_readonly_class: false, @@ -345,6 +508,7 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { attributes: stmt.attributes.clone(), constants: constants.clone(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }; substitute_relative_class_types_in_methods(&mut flattened.methods, name, None); units.push(flattened); @@ -353,7 +517,13 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { units } -/// Rewrites `self`/`static`/`parent` annotations across flattened class metadata. +/// Resolves the relative class types `self`/`static`/`parent` to concrete class names across +/// every flattened class's method parameter, method return, and property type annotations. +/// +/// `self`/`static` resolve to the flattened class itself and `parent` to its `extends` target. +/// Because trait methods are already merged into the using class at this point, a trait method's +/// `self` correctly resolves to the using class rather than the trait. Annotations with no +/// relative type are left untouched. fn substitute_relative_class_types_in_flattened(classes: &mut [FlattenedClass]) { for class in classes.iter_mut() { let self_class = class.name.clone(); @@ -368,11 +538,18 @@ fn substitute_relative_class_types_in_flattened(classes: &mut [FlattenedClass]) } } +/// Resolves relative class types inside flattened enum methods. +fn substitute_relative_class_types_in_flattened_enums(enums: &mut HashMap) { + for enum_unit in enums.values_mut() { + let self_class = enum_unit.name.clone(); + substitute_relative_class_types_in_methods(&mut enum_unit.methods, &self_class, None); + } +} + /// Rewrites `self`/`static`/`parent` type annotations on a slice of methods by delegating to -/// `ClassMethod::substitute_relative_class_types`. Used for: -/// - user classes (after trait/inheritance flattening) -/// - interfaces -/// - enums (to prepare method bodies for checking) +/// `ClassMethod::substitute_relative_class_types`. +/// +/// Used for user classes after trait/inheritance flattening, interfaces, and enums. fn substitute_relative_class_types_in_methods( methods: &mut [ClassMethod], self_class: &str, diff --git a/src/types/checker/driver/top_level.rs b/src/types/checker/driver/top_level.rs index ef24e0d09b..45b15f45a1 100644 --- a/src/types/checker/driver/top_level.rs +++ b/src/types/checker/driver/top_level.rs @@ -27,6 +27,8 @@ impl Checker { &mut self, program: &Program, ) -> (TypeEnv, Vec>) { + let saved_eval_barrier_active = self.eval_barrier_active; + self.eval_barrier_active = false; let mut global_env = self.seed_global_env(); let mut all_errors = Vec::with_capacity(program.len()); for stmt in program { @@ -38,6 +40,7 @@ impl Checker { .unwrap_or_default(); all_errors.push(stmt_errors); } + self.eval_barrier_active = saved_eval_barrier_active; (global_env, all_errors) } diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index 1f366a2859..c9481ad9df 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -158,6 +158,13 @@ impl Checker { return result; } + if self.eval_barrier_active { + for arg in args { + self.infer_type(arg, caller_env)?; + } + return Ok(PhpType::Mixed); + } + let decl = self .fn_decls .get(name) @@ -181,15 +188,34 @@ impl Checker { decl.variadic .iter() .cloned() - .map(|name| (name, PhpType::Array(Box::new(PhpType::Int)))), + .map(|name| { + let elem_ty = if decl.variadic_by_ref { + PhpType::Mixed + } else { + PhpType::Int + }; + (name, PhpType::Array(Box::new(elem_ty))) + }), ) - .collect(), + .collect(), + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), by_ref_return: false, ref_params: decl.ref_params.clone(), - declared_params: decl.param_types.iter().map(|type_ann| type_ann.is_some()).collect(), + declared_params: decl + .param_types + .iter() + .map(|type_ann| type_ann.is_some()) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) + .collect(), variadic: decl.variadic.clone(), deprecation: None, }; @@ -383,7 +409,9 @@ impl Checker { } if let Some(ref vp) = decl.variadic { - if let Some(declared) = &decl.variadic_type { + if decl.variadic_by_ref { + param_types.push((vp.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + } else if let Some(declared) = &decl.variadic_type { // A declared element type (`int ...$xs`) constrains every collected argument; // it takes precedence over inference so call validation enforces the hint. let elem_ty = self.resolve_declared_param_type_hint( diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 09f7bbf12a..1594fefe7b 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -92,6 +92,13 @@ impl Checker { let provisional_sig = FunctionSig { params: param_types.clone(), + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), @@ -102,7 +109,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic.clone(), }; @@ -229,6 +236,13 @@ impl Checker { let sig = FunctionSig { params: param_types, + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: return_type.clone(), declared_return: decl.return_type.is_some(), @@ -238,7 +252,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic.clone(), deprecation: crate::types::checker::schema::validation::extract_deprecation( diff --git a/src/types/checker/functions/resolution/specialization.rs b/src/types/checker/functions/resolution/specialization.rs index 0ecae3fbd1..cc38c537b4 100644 --- a/src/types/checker/functions/resolution/specialization.rs +++ b/src/types/checker/functions/resolution/specialization.rs @@ -155,7 +155,10 @@ impl Checker { } seen_idx += 1; } - if stored_sig.variadic.is_some() && seen_idx > regular_param_count { + if stored_sig.variadic.is_some() + && seen_idx > regular_param_count + && !variadic_param_is_by_ref(stored_sig) + { let regular_names: Vec = stored_sig.params[..regular_param_count] .iter() .map(|(name, _)| name.clone()) @@ -278,6 +281,19 @@ fn is_callable_array_type(ty: &PhpType) -> bool { } } +/// Returns whether a stored function signature has a by-reference variadic slot. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + /// Extracts parameter types from a generic `param_types` list, mapping them to the /// parameter names declared in `decl`. /// diff --git a/src/types/checker/inference/expr/class_refs.rs b/src/types/checker/inference/expr/class_refs.rs index dc95c9ce78..e11f86da3b 100644 --- a/src/types/checker/inference/expr/class_refs.rs +++ b/src/types/checker/inference/expr/class_refs.rs @@ -72,6 +72,9 @@ impl Checker { expr: &Expr, ) -> Result { let class_name = self.resolve_static_receiver_class(receiver, expr.span)?; + if !self.scoped_constant_receiver_is_known(&class_name) && self.eval_barrier_active { + return Ok(PhpType::Mixed); + } // First: enum case access (`Color::Red`). Enums shadow classes for // this syntax in PHP since 8.1. A name that is not a declared case is an enum *constant* // (`Scale::FACTOR`), which is resolved through the class-constant table below. @@ -116,6 +119,14 @@ impl Checker { )) } + /// Returns whether a scoped-constant receiver is known in static class-like metadata. + fn scoped_constant_receiver_is_known(&self, class_name: &str) -> bool { + self.classes.contains_key(class_name) + || self.interfaces.contains_key(class_name) + || self.declared_traits.contains(class_name) + || self.enums.contains_key(class_name) + } + /// Looks up a constant by name on an interface, traversing parent interfaces breadth-first /// to find it. Returns the constant's value expression if found. fn lookup_interface_constant( diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 0d89efbbc0..171bd6280b 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -45,6 +45,10 @@ impl Checker { env: &mut TypeEnv, ) -> Result { match &expr.kind { + ExprKind::Variable(name) if self.eval_barrier_active && !env.contains_key(name) => { + env.insert(name.clone(), PhpType::Mixed); + Ok(PhpType::Mixed) + } ExprKind::Assignment { target, value, @@ -215,9 +219,14 @@ impl Checker { // an undeclared property routed to `__isset`/`__unset`, which must // not be inferred as a bare property access here. The call's own // inference handles the operands (with magic routing). - let is_lazy_construct = builtin_name.eq_ignore_ascii_case("isset") - || builtin_name.eq_ignore_ascii_case("unset"); - if !is_lazy_construct { + if matches!( + php_symbol_key(builtin_name).as_str(), + "isset" | "unset" + ) { + for arg in &expanded_args { + self.infer_non_reading_arg_assignment_effects(arg, env)?; + } + } else if !builtin_name.eq_ignore_ascii_case("unset") { for (idx, arg) in expanded_args.iter().enumerate() { if builtin_name.eq_ignore_ascii_case("preg_replace_callback") && idx == 1 { continue; @@ -256,6 +265,9 @@ impl Checker { promote_indexed_local_for_element_unset(arg, env); } } + if builtin_name.eq_ignore_ascii_case("eval") { + self.mark_eval_barrier(env); + } Ok(ty) } ExprKind::NewObject { args, .. } | ExprKind::StaticMethodCall { args, .. } => { @@ -322,6 +334,19 @@ impl Checker { Self::purge_property_narrowings(env); Ok(ty) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + self.infer_type_with_assignment_effects(object, env)?; + self.infer_type_with_assignment_effects(method, env)?; + let expanded_args = crate::types::call_args::expand_static_assoc_spread_args(args); + for arg in &expanded_args { + self.infer_type_with_assignment_effects(arg, env)?; + } + self.infer_type(expr, env) + } ExprKind::BufferNew { len, .. } => { self.infer_type_with_assignment_effects(len, env)?; self.infer_type(expr, env) @@ -339,6 +364,39 @@ impl Checker { } } + /// Infers effects for a language-construct operand without treating properties as reads. + fn infer_non_reading_arg_assignment_effects( + &mut self, + arg: &Expr, + env: &mut TypeEnv, + ) -> Result<(), CompileError> { + match &arg.kind { + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + self.infer_type_with_assignment_effects(object, env)?; + Ok(()) + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + self.infer_type_with_assignment_effects(object, env)?; + self.infer_type_with_assignment_effects(property, env)?; + Ok(()) + } + ExprKind::ArrayAccess { array, index } => { + self.infer_type_with_assignment_effects(array, env)?; + self.infer_type_with_assignment_effects(index, env)?; + Ok(()) + } + ExprKind::NamedArg { value, .. } => { + self.infer_non_reading_arg_assignment_effects(value, env) + } + _ => { + self.infer_type_with_assignment_effects(arg, env)?; + Ok(()) + } + } + } + /// Returns true when an expression call target is first-class `preg_replace_callback`. fn expr_targets_preg_replace_callback(&self, callee: &Expr) -> bool { match &callee.kind { @@ -356,6 +414,22 @@ impl Checker { .get(var_name) .is_some_and(callable_target_is_preg_replace_callback) } + + /// Marks the active statement stream as having crossed eval and widens local facts. + fn mark_eval_barrier(&mut self, env: &mut TypeEnv) { + self.eval_barrier_active = true; + let local_names = env.keys().cloned().collect::>(); + for ty in env.values_mut() { + *ty = PhpType::Mixed; + } + for name in local_names { + self.closure_return_types.remove(&name); + self.callable_sigs.remove(&name); + self.callable_captures.remove(&name); + self.callable_array_targets.remove(&name); + self.first_class_callable_targets.remove(&name); + } + } } /// Returns true when a first-class callable target is PHP `preg_replace_callback`. diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index a8c6edbc44..1a1bf086d0 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -9,7 +9,9 @@ //! - Inference must preserve PHP evaluation errors and avoid treating effectful expressions as pure type facts. use crate::errors::CompileError; +use crate::names::php_symbol_key; use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; use crate::types::{ merge_array_key_types, normalized_array_key_type, packed_type_size, PhpType, TypeEnv, }; @@ -43,9 +45,7 @@ impl Checker { ExprKind::StringLiteral(_) => Ok(PhpType::Str), ExprKind::IntLiteral(_) => Ok(PhpType::Int), ExprKind::FloatLiteral(_) => Ok(PhpType::Float), - ExprKind::Variable(name) => env.get(name).cloned().ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined variable: ${}", name)) - }), + ExprKind::Variable(name) => self.variable_type_or_eval_dynamic(name, expr.span, env), ExprKind::Negate(inner) => { let ty = self.infer_type(inner, env)?; match ty { @@ -100,6 +100,7 @@ impl Checker { expr.span, &format!("Cannot increment/decrement ${} of type {:?}", name, other), )), + None if self.eval_barrier_active => Ok(PhpType::Int), None => Err(CompileError::new( expr.span, &format!("Undefined variable: ${}", name), @@ -463,9 +464,13 @@ impl Checker { ) } ExprKind::ConstRef(name) => { - self.constants.get(name.as_str()).cloned().ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined constant: {}", name)) - }) + self.constants + .get(name.as_str()) + .cloned() + .or_else(|| self.eval_barrier_active.then_some(PhpType::Mixed)) + .ok_or_else(|| { + CompileError::new(expr.span, &format!("Undefined constant: {}", name)) + }) } ExprKind::FirstClassCallable(target) => { self.infer_first_class_callable_target(target, expr.span, env)?; @@ -474,6 +479,7 @@ impl Checker { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type: _, return_type, body, @@ -489,6 +495,7 @@ impl Checker { self.infer_closure_type( params, variadic, + *variadic_by_ref, return_type, body, captures, @@ -524,6 +531,16 @@ impl Checker { ExprKind::NewObject { class_name, args } => { self.infer_new_object_type(class_name.as_str(), args, expr, env) } + ExprKind::Clone(inner) => { + let ty = self.infer_type(inner, env)?; + match ty { + PhpType::Object(class_name) => { + self.check_clone_visibility(&class_name, expr.span)?; + Ok(PhpType::Object(class_name)) + } + _ => Err(CompileError::new(expr.span, "clone requires an object value")), + } + } ExprKind::NewDynamic { name_expr, args } => { // The class is named at runtime; without a literal class // we can't typecheck constructor args or the resulting @@ -578,6 +595,11 @@ impl Checker { method, args, } => self.infer_nullsafe_method_call_type(object, method, args, expr, env), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => self.infer_nullsafe_dynamic_method_call_type(object, method, args, expr, env), ExprKind::StaticMethodCall { receiver, method, @@ -671,6 +693,19 @@ impl Checker { } } + /// Returns a variable type, allowing dynamic eval-created locals after an eval barrier. + fn variable_type_or_eval_dynamic( + &self, + name: &str, + span: Span, + env: &TypeEnv, + ) -> Result { + env.get(name) + .cloned() + .or_else(|| self.eval_barrier_active.then_some(PhpType::Mixed)) + .ok_or_else(|| CompileError::new(span, &format!("Undefined variable: ${}", name))) + } + /// Returns the element type of an array literal that contains at least one /// spread of an associative array. /// @@ -723,6 +758,39 @@ impl Checker { } +impl Checker { + /// Checks whether the current scope may invoke a class's `__clone` hook. + /// + /// PHP permits `__clone` to be non-public, but the actual `clone $object` + /// expression must obey the hook's visibility when a hook exists. + fn check_clone_visibility(&self, class_name: &str, span: Span) -> Result<(), CompileError> { + let normalized = class_name.trim_start_matches('\\'); + let Some(class_info) = self.classes.get(normalized) else { + return Ok(()); + }; + let key = php_symbol_key("__clone"); + let Some(visibility) = class_info.method_visibilities.get(&key) else { + return Ok(()); + }; + let declaring_class = class_info + .method_declaring_classes + .get(&key) + .map(String::as_str) + .unwrap_or(normalized); + if self.can_access_member(declaring_class, visibility) { + return Ok(()); + } + Err(CompileError::new( + span, + &format!( + "Cannot access {} method: {}::__clone", + Self::visibility_label(visibility), + normalized + ), + )) + } +} + /// Returns `true` if `index` is a valid string offset index for a string receiver. /// /// A valid index is an integer type, or a string literal whose value can be diff --git a/src/types/checker/inference/expr/static_closure.rs b/src/types/checker/inference/expr/static_closure.rs index ab3a423d8c..f39029987b 100644 --- a/src/types/checker/inference/expr/static_closure.rs +++ b/src/types/checker/inference/expr/static_closure.rs @@ -417,6 +417,18 @@ fn expr_must_not_use_this(expr: &Expr, span: Span) -> Result<(), CompileError> { } Ok(()) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_must_not_use_this(object, span)?; + expr_must_not_use_this(method, span)?; + for arg in args { + expr_must_not_use_this(arg, span)?; + } + Ok(()) + } ExprKind::ArrayLiteral(items) => { for item in items { expr_must_not_use_this(item, span)?; diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index ad88535ea6..a6c0dae2f2 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -112,36 +112,6 @@ impl Checker { )) } - /// Returns the class whose `magic` method (`__isset` or `__unset`) should - /// handle `isset($obj->prop)` / `unset($obj->prop)`: that is, when `$obj` is - /// an object whose class declares `magic` and `prop` is not a declared - /// property. Infers (and type-checks) the receiver object as a side effect, - /// so callers can skip inferring the bare property access — which would - /// otherwise reject the undeclared property before the magic call is reached. - pub(crate) fn isset_unset_property_magic_class( - &mut self, - arg: &Expr, - magic: &str, - env: &TypeEnv, - ) -> Result, CompileError> { - let (object, property) = match &arg.kind { - ExprKind::PropertyAccess { object, property } - | ExprKind::NullsafePropertyAccess { object, property } => (object.as_ref(), property), - _ => return Ok(None), - }; - let PhpType::Object(class_name) = self.infer_type(object, env)? else { - return Ok(None); - }; - let normalized = class_name.trim_start_matches('\\').to_string(); - let Some(class_info) = self.classes.get(&normalized) else { - return Ok(None); - }; - if class_info.properties.iter().any(|(name, _)| name == property) { - return Ok(None); - } - Ok(class_info.methods.contains_key(magic).then_some(normalized)) - } - /// Infers the type of a nullsafe property access expression (`$obj?->prop`). /// /// For `Mixed` receivers returns `Mixed`. For valid nullable object unions, @@ -258,7 +228,7 @@ impl Checker { { return Ok(ty); } - if let Some((_, ty)) = class_info.properties.iter().find(|(n, _)| n == property) { + if let Some((_, (_, ty))) = class_info.visible_property(property) { return Ok(ty.clone()); } if let Some(sig) = class_info.methods.get("__get") { @@ -412,9 +382,15 @@ impl Checker { expr: &Expr, ) -> Result { let class_name = self.resolve_static_property_receiver(receiver, expr)?; - let class_info = self.classes.get(&class_name).ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined class: {}", class_name)) - })?; + let Some(class_info) = self.classes.get(&class_name) else { + if self.eval_barrier_active && matches!(receiver, StaticReceiver::Named(_)) { + return Ok(PhpType::Mixed); + } + return Err(CompileError::new( + expr.span, + &format!("Undefined class: {}", class_name), + )); + }; if let Some(visibility) = class_info.static_property_visibilities.get(property) { let declaring_class = class_info .static_property_declaring_classes diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 3be8b0ef5e..d8b93f32da 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -15,6 +15,23 @@ use crate::types::{fibers, FunctionSig, PhpType, TypeEnv}; use super::super::super::Checker; +mod reflection; + +/// Compile-time selector accepted by `ReflectionParameter::__construct()`. +enum ReflectionParameterSelector { + Name(String), + Position(i64), +} + +/// Compile-time function or method target accepted by `ReflectionParameter::__construct()`. +enum ReflectionParameterTarget { + Function(String), + Method { + class_name: String, + method_name: String, + }, +} + impl Checker { /// Infers the type of a `new Class(...)` expression. /// @@ -45,6 +62,10 @@ impl Checker { )); } if !self.classes.contains_key(class_name.as_str()) { + if self.eval_barrier_active { + self.infer_eval_barrier_dynamic_constructor_args(args, expr, env)?; + return Ok(PhpType::Mixed); + } return Err(CompileError::new( expr.span, &format!("Undefined class: {}", class_name), @@ -81,7 +102,8 @@ impl Checker { .map(String::as_str) .unwrap_or(class_name.as_str()); if !self.can_access_member(declaring_class, visibility) - && !self.can_construct_internal_iterator_from_builtin_get_iterator(&class_name) + && !self + .can_construct_internal_iterator_from_builtin_get_iterator(&class_name) { return Err(CompileError::new( expr.span, @@ -162,6 +184,27 @@ impl Checker { Ok(PhpType::Object(class_name)) } + /// Infers constructor arguments for a class that may have been declared by a prior eval call. + fn infer_eval_barrier_dynamic_constructor_args( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + if crate::types::call_args::has_named_args(args) + || args.iter().any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return Err(CompileError::new( + expr.span, + "Cannot use named or spread arguments for eval-declared class construction", + )); + } + for arg in args { + self.infer_type(arg, env)?; + } + Ok(()) + } + /// Records the PHAR bridge and decompression libraries needed by PHAR archive helpers. pub(crate) fn require_phar_archive_libraries(&mut self) { self.require_builtin_library("elephc_phar"); @@ -177,12 +220,11 @@ impl Checker { && self.current_method.as_deref() == Some(get_iterator_key.as_str()) } - /// Validates constructor arguments for reflection owner classes - /// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`). + /// Validates constructor arguments for reflection owner classes. /// - /// Extracts the reflected class/method/property from string literal args, - /// then delegates to `validate_reflection_class_attrs`, - /// `validate_reflection_method_attrs`, or `validate_reflection_property_attrs`. + /// Extracts the reflected class/member metadata from literal arguments and + /// delegates to the focused ReflectionClass/Method/Property/Parameter + /// validation helpers. fn validate_reflection_owner_constructor( &mut self, class_name: &str, @@ -190,6 +232,9 @@ impl Checker { expr: &Expr, env: &TypeEnv, ) -> Result<(), CompileError> { + if class_name == "ReflectionMethod" && args.len() == 1 { + return self.validate_reflection_method_constructor_from_method_name(args, expr, env); + } let sig = self .classes .get(class_name) @@ -211,20 +256,21 @@ impl Checker { &format!("Constructor '{}::__construct'", class_name), )?; + if class_name == "ReflectionParameter" { + return self.validate_reflection_parameter_constructor(&normalized_args, expr, env); + } if class_name == "ReflectionFunction" { - let function_name = self.reflection_string_literal_arg( - class_name, - "function name", - normalized_args.first(), - env, - )?; - return self.validate_reflection_function_target(&function_name, expr); + return self.validate_reflection_function_constructor(&normalized_args, expr, env); + } + if class_name == "ReflectionObject" { + return Ok(()); } let reflected_class = self.reflection_class_literal_arg(class_name, &normalized_args[0], env)?; match class_name { "ReflectionClass" => self.validate_reflection_class_attrs(&reflected_class, expr), + "ReflectionEnum" => self.validate_reflection_enum_attrs(&reflected_class, expr), "ReflectionMethod" => { let method_name = self.reflection_string_literal_arg( class_name, @@ -243,10 +289,387 @@ impl Checker { )?; self.validate_reflection_property_attrs(&reflected_class, &property_name, expr) } + "ReflectionClassConstant" => { + let constant_name = self.reflection_string_literal_arg( + class_name, + "constant name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_class_constant_attrs( + &reflected_class, + &constant_name, + expr, + ) + } + "ReflectionEnumUnitCase" => { + let case_name = self.reflection_string_literal_arg( + class_name, + "case name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_enum_case_attrs(&reflected_class, &case_name, false, expr) + } + "ReflectionEnumBackedCase" => { + let case_name = self.reflection_string_literal_arg( + class_name, + "case name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_enum_case_attrs(&reflected_class, &case_name, true, expr) + } _ => Ok(()), } } + /// Validates deprecated `new ReflectionMethod("Class::method")` calls. + fn validate_reflection_method_constructor_from_method_name( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let arg = match &args[0].kind { + ExprKind::NamedArg { name, value } if name == "class_name" => value.as_ref(), + ExprKind::NamedArg { name, value } if name == "objectOrMethod" => value.as_ref(), + ExprKind::NamedArg { name, .. } => { + return Err(CompileError::new( + args[0].span, + &format!( + "Constructor 'ReflectionMethod::__construct' has no parameter ${}", + name + ), + )); + } + _ => &args[0], + }; + let (class_name, method_name) = self.reflection_method_name_literal_arg(arg, env)?; + self.validate_reflection_method_attrs(&class_name, &method_name, expr) + } + + /// Extracts a literal `ClassName::methodName` target for `ReflectionMethod`. + fn reflection_method_name_literal_arg( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result<(String, String), CompileError> { + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty.codegen_repr(), PhpType::Str) { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() first argument must be a string method name", + )); + } + let ExprKind::StringLiteral(target) = &arg.kind else { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() requires a string literal method name (dynamic lookup is not yet supported)", + )); + }; + let Some((raw_class_name, method_name)) = target.rsplit_once("::") else { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() one-argument form requires ClassName::method", + )); + }; + if raw_class_name.is_empty() || method_name.is_empty() { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() one-argument form requires ClassName::method", + )); + } + let class_name = self + .resolve_reflection_class_name(raw_class_name) + .map(str::to_string) + .ok_or_else(|| { + CompileError::new( + arg.span, + &format!( + "ReflectionMethod::__construct(): undefined class '{}'", + raw_class_name + ), + ) + })?; + Ok((class_name, method_name.to_string())) + } + + /// Validates `new ReflectionFunction(function)` for supported static function metadata. + fn validate_reflection_function_constructor( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let function_name = self.reflection_string_literal_arg( + "ReflectionFunction", + "function name", + args.first(), + env, + )?; + if self + .reflection_function_signature(&function_name)? + .is_some() + { + return self.validate_reflection_function_attrs(&function_name, expr); + } + Err(CompileError::new( + expr.span, + &format!( + "ReflectionFunction::__construct(): Function {}() does not exist", + function_name + ), + )) + } + + /// Validates `new ReflectionParameter(target, param)`. + /// + /// Supported targets are statically known user function names, supported + /// callable-builtin function names, and class/interface/trait method arrays. + /// The parameter selector must be an integer position or string name known + /// at compile time. + fn validate_reflection_parameter_constructor( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let target = self.reflection_parameter_target(args.first(), env)?; + let sig = match target { + ReflectionParameterTarget::Function(function_name) => self + .reflection_function_signature(&function_name)? + .ok_or_else(|| { + CompileError::new( + expr.span, + &format!( + "ReflectionParameter::__construct(): Function {}() does not exist", + function_name + ), + ) + })?, + ReflectionParameterTarget::Method { + class_name, + method_name, + } => self + .reflection_method_signature(&class_name, &method_name) + .ok_or_else(|| { + CompileError::new( + expr.span, + &format!( + "ReflectionParameter::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + ) + })?, + }; + let selector = self.reflection_parameter_selector_arg(args.get(1), env)?; + match selector { + ReflectionParameterSelector::Name(name) => { + if sig.params.iter().any(|(param_name, _)| param_name == &name) { + Ok(()) + } else { + Err(CompileError::new( + expr.span, + "ReflectionParameter::__construct(): parameter specified by name could not be found", + )) + } + } + ReflectionParameterSelector::Position(position) => { + if position >= 0 && (position as usize) < sig.params.len() { + Ok(()) + } else { + Err(CompileError::new( + expr.span, + "ReflectionParameter::__construct(): parameter specified by offset could not be found", + )) + } + } + } + } + + /// Extracts the function or class-method target from a ReflectionParameter call. + fn reflection_parameter_target( + &mut self, + arg: Option<&Expr>, + env: &TypeEnv, + ) -> Result { + let arg = arg.expect("reflection parameter constructor arity was validated"); + let arg_ty = self.infer_type(arg, env)?; + match arg_ty.codegen_repr() { + PhpType::Str => self + .reflection_string_literal_arg( + "ReflectionParameter", + "function name", + Some(arg), + env, + ) + .map(ReflectionParameterTarget::Function), + PhpType::Array(_) | PhpType::Mixed => { + self.reflection_parameter_method_target(arg, env) + } + _ => Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() first argument must be a function name string or class-method array", + )), + } + } + + /// Extracts the class and method names from the ReflectionParameter target array. + fn reflection_parameter_method_target( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty.codegen_repr(), PhpType::Array(_) | PhpType::Mixed) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() first argument must be a class-method array", + )); + } + let ExprKind::ArrayLiteral(items) = &arg.kind else { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() requires a literal class-method array target", + )); + }; + if items.len() != 2 { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() expects array(class, method) as first argument", + )); + } + let class_name = self.reflection_parameter_owner_class_name(&items[0], env)?; + let method_name = self.reflection_string_literal_arg( + "ReflectionParameter", + "method name", + items.get(1), + env, + )?; + Ok(ReflectionParameterTarget::Method { + class_name, + method_name, + }) + } + + /// Resolves the class-like name from a class literal or statically known object target. + fn reflection_parameter_owner_class_name( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { + match &arg.kind { + ExprKind::StringLiteral(_) | ExprKind::ClassConstant { .. } => { + self.reflection_class_literal_arg("ReflectionParameter", arg, env) + } + _ => self.reflection_parameter_concrete_object_class_name(arg, env), + } + } + + /// Resolves a statically known object expression to the reflected class name. + fn reflection_parameter_concrete_object_class_name( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { + let target_ty = self.infer_type(arg, env)?.codegen_repr(); + let PhpType::Object(class_name) = target_ty else { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + }; + if class_name.is_empty() || !self.classes.contains_key(class_name.as_str()) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + } + Ok(class_name) + } + + /// Returns the reflected signature for a user function or supported callable builtin. + fn reflection_function_signature( + &mut self, + function_name: &str, + ) -> Result, CompileError> { + let lookup_name = function_name.trim_start_matches('\\'); + let builtin_key = php_symbol_key(lookup_name); + if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_key) { + return Ok(Some(sig)); + } + let canonical = + match self.canonical_function_name_folded(lookup_name) { + Some(canonical) => canonical, + None => return Ok(None), + }; + if let Some(sig) = self.functions.get(&canonical).cloned() { + return Ok(Some(sig)); + } + if self.function_variant_groups.contains_key(&canonical) { + self.ensure_function_variant_group_signature(&canonical, crate::span::Span::dummy())?; + return Ok(self.functions.get(&canonical).cloned()); + } + if let Some(decl) = self.fn_decls.get(&canonical).cloned() { + let param_types = self.initial_function_param_types(&canonical, &decl)?; + self.resolve_function_signature(&canonical, &decl, param_types)?; + return Ok(self.functions.get(&canonical).cloned()); + } + Ok(None) + } + + /// Extracts the name or position selector for `ReflectionParameter`. + fn reflection_parameter_selector_arg( + &mut self, + arg: Option<&Expr>, + env: &TypeEnv, + ) -> Result { + let arg = arg.expect("reflection parameter constructor arity was validated"); + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty, PhpType::Str | PhpType::Int) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() second argument must be a string or int", + )); + } + match &arg.kind { + ExprKind::StringLiteral(name) => Ok(ReflectionParameterSelector::Name(name.clone())), + ExprKind::IntLiteral(position) => Ok(ReflectionParameterSelector::Position(*position)), + _ => Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() requires a literal parameter name or position", + )), + } + } + + /// Returns the reflected method signature for class/interface/trait metadata. + fn reflection_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + let method_key = php_symbol_key(method_name); + if let Some(class_info) = self.classes.get(class_name) { + return class_info + .methods + .get(&method_key) + .or_else(|| class_info.static_methods.get(&method_key)) + .cloned(); + } + if let Some(interface_info) = self.interfaces.get(class_name) { + return interface_info + .methods + .get(&method_key) + .or_else(|| interface_info.static_methods.get(&method_key)) + .cloned(); + } + if let Some(trait_methods) = self.declared_trait_methods.get(class_name) { + return trait_methods.get(&method_key).cloned(); + } + None + } + /// Extracts the class name argument from a reflection constructor call. /// /// Accepts a string literal or `ClassName::class` constant; returns the @@ -259,7 +682,22 @@ impl Checker { env: &TypeEnv, ) -> Result { let arg_ty = self.infer_type(arg, env)?; - if !matches!(arg_ty, PhpType::Str) { + if let PhpType::Object(class_name) = arg_ty.codegen_repr() { + if reflection_type == "ReflectionClass" + && !class_name.is_empty() + && self.classes.contains_key(class_name.as_str()) + { + return Ok(class_name); + } + return Err(CompileError::new( + arg.span, + &format!( + "{}::__construct() first argument must be a string class name", + reflection_type + ), + )); + } + if !matches!(arg_ty.codegen_repr(), PhpType::Str) { return Err(CompileError::new( arg.span, &format!( @@ -331,154 +769,6 @@ impl Checker { } } - /// Validates that `new ReflectionFunction($name)` targets a known - /// user-defined function (matched case-insensitively, as PHP function names - /// are). Builtin functions are not yet reflectable. - fn validate_reflection_function_target( - &self, - function_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let key = crate::names::php_symbol_key(function_name.trim_start_matches('\\')); - let exists = self - .fn_decls - .keys() - .chain(self.functions.keys()) - .any(|name| crate::names::php_symbol_key(name.trim_start_matches('\\')) == key); - if exists { - Ok(()) - } else { - Err(CompileError::new( - expr.span, - &format!( - "ReflectionFunction::__construct(): undefined function '{}'", - function_name - ), - )) - } - } - - /// Validates that a class's attributes do not have unsupported argument metadata. - /// - /// Returns `Ok` if the class has no attribute args or if all args are - /// supported. Used by `ReflectionClass` constructor validation. - fn validate_reflection_class_attrs( - &self, - class_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionClass::__construct(): undefined class '{}'", class_name), - )); - }; - if attributes_have_unsupported_args(&class_info.attribute_names, &class_info.attribute_args) - { - return Err(CompileError::new( - expr.span, - "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - - /// Validates that a method's attributes do not have unsupported argument metadata. - /// - /// Also checks that the method exists on the class. Used by - /// `ReflectionMethod` constructor validation. - fn validate_reflection_method_attrs( - &self, - class_name: &str, - method_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionMethod::__construct(): undefined class '{}'", class_name), - )); - }; - let method_key = php_symbol_key(method_name); - if !class_info.methods.contains_key(&method_key) - && !class_info.static_methods.contains_key(&method_key) - { - return Err(CompileError::new( - expr.span, - &format!( - "ReflectionMethod::__construct(): undefined method '{}::{}'", - class_name, method_name - ), - )); - } - let empty_names = Vec::new(); - let empty_args = Vec::new(); - let names = class_info - .method_attribute_names - .get(&method_key) - .unwrap_or(&empty_names); - let args = class_info - .method_attribute_args - .get(&method_key) - .unwrap_or(&empty_args); - if attributes_have_unsupported_args(names, args) { - return Err(CompileError::new( - expr.span, - "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - - /// Validates that a property's attributes do not have unsupported argument metadata. - /// - /// Also checks that the property exists on the class (instance or static). - /// Used by `ReflectionProperty` constructor validation. - fn validate_reflection_property_attrs( - &self, - class_name: &str, - property_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionProperty::__construct(): undefined class '{}'", class_name), - )); - }; - if !class_info.properties.iter().any(|(name, _)| name == property_name) - && !class_info - .static_properties - .iter() - .any(|(name, _)| name == property_name) - { - return Err(CompileError::new( - expr.span, - &format!( - "ReflectionProperty::__construct(): undefined property '{}::${}'", - class_name, property_name - ), - )); - } - let empty_names = Vec::new(); - let empty_args = Vec::new(); - let names = class_info - .property_attribute_names - .get(property_name) - .unwrap_or(&empty_names); - let args = class_info - .property_attribute_args - .get(property_name) - .unwrap_or(&empty_args); - if attributes_have_unsupported_args(names, args) { - return Err(CompileError::new( - expr.span, - "ReflectionProperty::getAttributes(): property has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - /// Resolves a static receiver to a class name for reflection class constant. /// /// `Named` returns the canonical name. `Self_`/`Static` require a class @@ -490,10 +780,11 @@ impl Checker { ) -> Result { match receiver { StaticReceiver::Named(name) => Ok(name.as_canonical()), - StaticReceiver::Self_ | StaticReceiver::Static => self - .current_class - .clone() - .ok_or_else(|| CompileError::new(span, "Cannot use self::class outside a class context")), + StaticReceiver::Self_ | StaticReceiver::Static => { + self.current_class.clone().ok_or_else(|| { + CompileError::new(span, "Cannot use self::class outside a class context") + }) + } StaticReceiver::Parent => { let current = self.current_class.as_ref().ok_or_else(|| { CompileError::new(span, "Cannot use parent::class outside a class context") @@ -502,10 +793,7 @@ impl Checker { .get(current) .and_then(|info| info.parent.clone()) .ok_or_else(|| { - CompileError::new( - span, - &format!("Class '{}' has no parent class", current), - ) + CompileError::new(span, &format!("Class '{}' has no parent class", current)) }) } } @@ -514,11 +802,13 @@ impl Checker { /// Looks up a class name by PHP case-insensitive symbol key. /// /// Strips leading backslashes and uses `php_symbol_key` for comparison. - /// Returns the canonical class name string if found. + /// Returns the canonical class-like name string if found. fn resolve_reflection_class_name<'a>(&'a self, class_name: &str) -> Option<&'a str> { let class_key = php_symbol_key(class_name.trim_start_matches('\\')); self.classes .keys() + .chain(self.interfaces.keys()) + .chain(self.declared_traits.iter()) .find(|existing| php_symbol_key(existing) == class_key) .map(String::as_str) } @@ -546,7 +836,10 @@ impl Checker { { return Ok(()); } - return Err(CompileError::new(callback.span, "Fiber callback must be callable")); + return Err(CompileError::new( + callback.span, + "Fiber callback must be callable", + )); }; let visible_param_count = match &callback.kind { @@ -572,11 +865,19 @@ impl Checker { } match &callback.kind { - ExprKind::StringLiteral(name) => self.resolve_fiber_string_callable_sig(name, callback.span, env), - ExprKind::ArrayLiteral(_) => self.resolve_fiber_callable_array_literal_sig(callback, env), + ExprKind::StringLiteral(name) => { + self.resolve_fiber_string_callable_sig(name, callback.span, env) + } + ExprKind::ArrayLiteral(_) => { + self.resolve_fiber_callable_array_literal_sig(callback, env) + } ExprKind::Variable(name) => { if let Some(target) = self.callable_array_targets.get(name).cloned() { - return self.resolve_fiber_callable_array_variable_sig(&target, callback.span, env); + return self.resolve_fiber_callable_array_variable_sig( + &target, + callback.span, + env, + ); } self.resolve_fiber_invokable_object_sig(callback, env) } @@ -600,7 +901,9 @@ impl Checker { receiver: StaticReceiver::Named(Name::from(class_name.to_string())), method: method_name.to_string(), }; - return self.resolve_first_class_callable_sig(&target, span, env).map(Some); + return self + .resolve_first_class_callable_sig(&target, span, env) + .map(Some); } let function_name = self @@ -608,7 +911,8 @@ impl Checker { .or_else(|| crate::name_resolver::canonical_builtin_function_name(name)) .unwrap_or_else(|| name.trim_start_matches('\\').to_string()); let target = CallableTarget::Function(Name::from(function_name)); - self.resolve_first_class_callable_sig(&target, span, env).map(Some) + self.resolve_first_class_callable_sig(&target, span, env) + .map(Some) } /// Resolves a literal callable array to a static-method or receiver-bound method signature. @@ -631,7 +935,8 @@ impl Checker { span: crate::span::Span, env: &TypeEnv, ) -> Result, CompileError> { - self.resolve_first_class_callable_sig(target, span, env).map(Some) + self.resolve_first_class_callable_sig(target, span, env) + .map(Some) } /// Resolves an invokable object callback signature for `$object` or `$this`. @@ -694,9 +999,9 @@ impl Checker { ExprKind::StringLiteral(class_name) => self .resolve_fiber_callable_class_name(class_name) .map(str::to_string), - ExprKind::ClassConstant { receiver } => Some( - self.resolve_fiber_callable_static_receiver_class(receiver, span)?, - ), + ExprKind::ClassConstant { receiver } => { + Some(self.resolve_fiber_callable_static_receiver_class(receiver, span)?) + } _ => None, }; Ok(class_name.map(|class_name| StaticReceiver::Named(Name::from(class_name)))) @@ -728,10 +1033,7 @@ impl Checker { .get(current) .and_then(|class_info| class_info.parent.clone()) .ok_or_else(|| { - CompileError::new( - span, - &format!("Class '{}' has no parent class", current), - ) + CompileError::new(span, &format!("Class '{}' has no parent class", current)) }) } } @@ -851,7 +1153,10 @@ impl Checker { let Some(sig) = self.functions.get_mut(name.as_str()) else { return Err(CompileError::new( span, - &format!("Undefined function for CallbackFilterIterator callback: {}", name), + &format!( + "Undefined function for CallbackFilterIterator callback: {}", + name + ), )); }; let callback_arg_types = [ @@ -911,14 +1216,22 @@ fn fiber_callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { } /// Returns `true` if `class_name` is a reflection owner class -/// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`). +/// (`ReflectionClass`, `ReflectionObject`, `ReflectionEnum`, `ReflectionMethod`, `ReflectionProperty`, +/// `ReflectionParameter`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, +/// `ReflectionEnumBackedCase`). fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionObject" + | "ReflectionEnum" + | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" - | "ReflectionFunction" + | "ReflectionParameter" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" ) } diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs new file mode 100644 index 0000000000..2ee5ffccc8 --- /dev/null +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -0,0 +1,414 @@ +//! Purpose: +//! Validates builtin Reflection owner constructor metadata for object inference. +//! Keeps ReflectionClass/Function/Method/Property/Constant/EnumCase attribute checks out +//! of the general constructor inference driver. +//! +//! Called from: +//! - `crate::types::checker::inference::objects::constructors::Checker::validate_reflection_constructor_args()`. +//! +//! Key details: +//! - Constructor validation checks that reflected members exist and that captured +//! attribute arguments are materializable by the current ReflectionAttribute model. + +use crate::errors::CompileError; +use crate::names::php_symbol_key; +use crate::parser::ast::Expr; +use crate::types::checker::Checker; +use crate::types::{collect_attribute_args, collect_attribute_names}; + +type ReflectionAttributeArgs = Vec>>; + +impl Checker { + /// Validates function-level attributes for `ReflectionFunction`. + /// + /// The reflected function must be a statically declared user function or a + /// supported callable builtin; user-function attributes must have + /// materializable `ReflectionAttribute::getArguments()` metadata. + pub(super) fn validate_reflection_function_attrs( + &self, + function_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(canonical) = + self.canonical_function_name_folded(function_name.trim_start_matches('\\')) + else { + let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); + if crate::types::first_class_callable_builtin_sig(&builtin_key).is_some() { + return Ok(()); + } + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionFunction::__construct(): Function {}() does not exist", + function_name + ), + )); + }; + let Some(decl) = self.fn_decls.get(&canonical) else { + return Ok(()); + }; + let names = collect_attribute_names(&decl.attributes); + let args = collect_attribute_args(&decl.attributes); + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionFunction::getAttributes(): function has attribute argument metadata that is not supported yet", + ) + } + + /// Validates class-level attributes for `ReflectionClass`. + /// + /// Returns `Ok` when the class exists and its captured attribute argument + /// metadata can be materialized by `ReflectionAttribute::getArguments()`. + pub(super) fn validate_reflection_class_attrs( + &self, + class_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + if let Some(class_info) = self.classes.get(class_name) { + return self.validate_reflection_attribute_metadata( + &class_info.attribute_names, + &class_info.attribute_args, + expr, + "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", + ); + } + if self.interfaces.contains_key(class_name) || self.declared_traits.contains(class_name) { + return Ok(()); + } + Err(CompileError::new( + expr.span, + &format!( + "ReflectionClass::__construct(): undefined class '{}'", + class_name + ), + )) + } + + /// Validates enum-level attributes for `ReflectionEnum`. + /// + /// The reflected symbol must be a declared enum; enum attributes are stored + /// on the parallel class metadata produced for enum declarations. + pub(super) fn validate_reflection_enum_attrs( + &self, + enum_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + if !self.enums.contains_key(enum_name) { + return Err(CompileError::new( + expr.span, + &format!("ReflectionEnum::__construct(): {} is not an enum", enum_name), + )); + } + if let Some(class_info) = self.classes.get(enum_name) { + return self.validate_reflection_attribute_metadata( + &class_info.attribute_names, + &class_info.attribute_args, + expr, + "ReflectionEnum::getAttributes(): enum has attribute argument metadata that is not supported yet", + ); + } + Ok(()) + } + + /// Validates method-level attributes for `ReflectionMethod`. + /// + /// Checks that the method exists on the reflected class before validating + /// its captured attribute argument metadata. + pub(super) fn validate_reflection_method_attrs( + &self, + class_name: &str, + method_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let method_key = php_symbol_key(method_name); + if let Some(class_info) = self.classes.get(class_name) { + if !class_info.methods.contains_key(&method_key) + && !class_info.static_methods.contains_key(&method_key) + { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + )); + } + let empty_names = Vec::new(); + let empty_args = Vec::new(); + let names = class_info + .method_attribute_names + .get(&method_key) + .unwrap_or(&empty_names); + let args = class_info + .method_attribute_args + .get(&method_key) + .unwrap_or(&empty_args); + return self.validate_reflection_attribute_metadata( + names, + args, + expr, + "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", + ); + } + if let Some(interface_info) = self.interfaces.get(class_name) { + if interface_info.methods.contains_key(&method_key) + || interface_info.static_methods.contains_key(&method_key) + { + return Ok(()); + } + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + )); + } + if let Some(trait_methods) = self.declared_trait_methods.get(class_name) { + if trait_methods.contains_key(&method_key) { + return Ok(()); + } + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + )); + } + Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined class '{}'", + class_name + ), + )) + } + + /// Validates property-level attributes for `ReflectionProperty`. + /// + /// Checks both instance and static properties because PHP reflection accepts + /// either surface through the same constructor. + pub(super) fn validate_reflection_property_attrs( + &self, + class_name: &str, + property_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(class_info) = self.classes.get(class_name) else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionProperty::__construct(): undefined class '{}'", + class_name + ), + )); + }; + if !class_info + .properties + .iter() + .any(|(name, _)| name == property_name) + && !class_info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionProperty::__construct(): undefined property '{}::${}'", + class_name, property_name + ), + )); + } + let empty_names = Vec::new(); + let empty_args = Vec::new(); + let names = class_info + .property_attribute_names + .get(property_name) + .unwrap_or(&empty_names); + let args = class_info + .property_attribute_args + .get(property_name) + .unwrap_or(&empty_args); + self.validate_reflection_attribute_metadata( + names, + args, + expr, + "ReflectionProperty::getAttributes(): property has attribute argument metadata that is not supported yet", + ) + } + + /// Validates class-constant or enum-case attributes for `ReflectionClassConstant`. + /// + /// Enum cases are checked first because PHP exposes them through + /// `ReflectionClassConstant` as well as the enum-case-specific reflectors. + pub(super) fn validate_reflection_class_constant_attrs( + &self, + class_name: &str, + constant_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + if let Some((names, args)) = + self.reflection_enum_case_attribute_metadata(class_name, constant_name) + { + return self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionClassConstant::getAttributes(): enum case has attribute argument metadata that is not supported yet", + ); + } + let Some((names, args)) = self + .reflection_class_constant_attribute_metadata(class_name, constant_name) + .or_else(|| self.reflection_interface_constant_metadata(class_name, constant_name)) + .or_else(|| self.reflection_trait_constant_metadata(class_name, constant_name)) + else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionClassConstant::__construct(): undefined class constant '{}::{}'", + class_name, constant_name + ), + )); + }; + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionClassConstant::getAttributes(): class constant has attribute argument metadata that is not supported yet", + ) + } + + /// Validates enum-case attributes for `ReflectionEnumUnitCase` or `ReflectionEnumBackedCase`. + /// + /// `require_backed` is true for `ReflectionEnumBackedCase`; unit-case + /// reflection accepts backed cases too, matching PHP. + pub(super) fn validate_reflection_enum_case_attrs( + &self, + enum_name: &str, + case_name: &str, + require_backed: bool, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(enum_info) = self.enums.get(enum_name) else { + return Err(CompileError::new( + expr.span, + &format!("{} is not an enum", enum_name), + )); + }; + if require_backed && enum_info.backing_type.is_none() { + return Err(CompileError::new( + expr.span, + &format!( + "Enum case {}::{} is not a backed case", + enum_name, case_name + ), + )); + } + let Some((names, args)) = + self.reflection_enum_case_attribute_metadata(enum_name, case_name) + else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionEnumUnitCase::__construct(): undefined enum case '{}::{}'", + enum_name, case_name + ), + )); + }; + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionEnumUnitCase::getAttributes(): enum case has attribute argument metadata that is not supported yet", + ) + } + + /// Returns cloned class-constant attribute metadata, walking parent classes. + fn reflection_class_constant_attribute_metadata( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + let class_info = self.classes.get(class_name)?; + if class_info.constants.contains_key(constant_name) { + return Some(( + class_info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + class_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + )); + } + let parent = class_info.parent.as_deref()?; + self.reflection_class_constant_attribute_metadata(parent, constant_name) + } + + /// Returns empty attribute metadata when an interface constant exists. + fn reflection_interface_constant_metadata( + &self, + interface_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + if let Some(info) = self.interfaces.get(interface_name) { + if info.constants.contains_key(constant_name) { + return Some((Vec::new(), Vec::new())); + } + } + let class_info = self.classes.get(interface_name)?; + class_info.interfaces.iter().find_map(|implemented| { + self.interfaces + .get(implemented) + .filter(|info| info.constants.contains_key(constant_name)) + .map(|_| (Vec::new(), Vec::new())) + }) + } + + /// Returns empty attribute metadata when a trait constant exists. + fn reflection_trait_constant_metadata( + &self, + trait_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + self.declared_trait_constants + .get(trait_name) + .filter(|constants| constants.contains(constant_name)) + .map(|_| (Vec::new(), Vec::new())) + } + + /// Returns cloned enum-case attribute metadata for one case. + fn reflection_enum_case_attribute_metadata( + &self, + enum_name: &str, + case_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + self.enums + .get(enum_name)? + .cases + .iter() + .find(|case| case.name == case_name) + .map(|case| (case.attribute_names.clone(), case.attribute_args.clone())) + } + + /// Validates one pair of reflection attribute metadata slices. + fn validate_reflection_attribute_metadata( + &self, + names: &[String], + args: &[Option>], + expr: &Expr, + message: &str, + ) -> Result<(), CompileError> { + if super::attributes_have_unsupported_args(names, args) { + return Err(CompileError::new(expr.span, message)); + } + Ok(()) + } +} diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 2c18096673..dc07723306 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -37,11 +37,13 @@ impl Checker { let obj_ty = self.infer_type(object, env)?; if let PhpType::Object(class_name) = &obj_ty { if self.interfaces.contains_key(class_name) { - return self.infer_method_call_on_interface_type( - class_name, method, args, expr, env, - ); + return self + .infer_method_call_on_interface_type(class_name, method, args, expr, env); } - return self.infer_method_call_on_class_type(class_name, method, args, expr, env); + let return_ty = self.infer_method_call_on_class_type(class_name, method, args, expr, env)?; + return Ok(self + .tracked_reflection_class_method_return_type(object, method) + .unwrap_or(return_ty)); } // Method calls on a union object type are allowed when the union has a // single object class. `?Foo` / `Foo|null` faults on a null receiver as in @@ -65,7 +67,11 @@ impl Checker { env, ); } - return self.infer_method_call_on_class_type(&class_name, method, args, expr, env); + let return_ty = + self.infer_method_call_on_class_type(&class_name, method, args, expr, env)?; + return Ok(self + .tracked_reflection_class_method_return_type(object, method) + .unwrap_or(return_ty)); } // Union of two or more distinct object classes (`A|B`, `A|B|false`): // the method must exist on every object member; codegen dispatches on @@ -166,6 +172,24 @@ impl Checker { } } + /// Returns a concrete reflected object type for tracked `ReflectionClass` construction helpers. + fn tracked_reflection_class_method_return_type( + &self, + object: &Expr, + method: &str, + ) -> Option { + let ExprKind::Variable(name) = &object.kind else { + return None; + }; + let reflected_class = self.reflection_class_targets.get(name)?; + match php_symbol_key(method).as_str() { + "newinstance" | "newinstanceargs" | "newinstancewithoutconstructor" => { + Some(PhpType::Object(reflected_class.clone())) + } + _ => None, + } + } + /// Infers the type of a nullsafe method call expression (`$obj?->method(...)`). /// /// Returns `PhpType::Void` for invalid receivers. For valid nullable object @@ -196,6 +220,33 @@ impl Checker { } } + /// Infers `$obj?->$method(...)` when the method name is known only at runtime. + /// + /// The receiver must still be an object-or-null like other nullsafe member + /// accesses. The dynamic target prevents method signature validation, so the + /// return type is `Mixed`; method-name and argument expressions are inferred + /// only when the receiver is not statically null. + pub(crate) fn infer_nullsafe_dynamic_method_call_type( + &mut self, + object: &Expr, + method: &Expr, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result { + let obj_ty = self.infer_type(object, env)?; + let Some((_class_name, _nullable)) = + self.nullsafe_object_receiver(&obj_ty, expr, "method call")? + else { + return Ok(PhpType::Void); + }; + self.infer_type(method, env)?; + for arg in args { + self.infer_type(arg, env)?; + } + Ok(PhpType::Mixed) + } + /// Infers the type of a method call on an interface type. /// /// Looks up the method in the interface schema, validates arguments via @@ -254,12 +305,7 @@ impl Checker { env: &TypeEnv, ) -> Result { self.infer_method_call_on_class_type_with_options( - class_name, - method, - args, - expr, - env, - false, + class_name, method, args, expr, env, false, ) } @@ -273,14 +319,7 @@ impl Checker { expr: &Expr, env: &TypeEnv, ) -> Result { - self.infer_method_call_on_class_type_with_options( - class_name, - method, - args, - expr, - env, - true, - ) + self.infer_method_call_on_class_type_with_options(class_name, method, args, expr, env, true) } /// Shared implementation for class method call inference. @@ -377,8 +416,7 @@ impl Checker { } } else if let Some(sig) = class_info.methods.get("__call") { let magic_args = Self::magic_call_args(method, args, expr.span); - let declared_flags = - Self::declared_method_param_flags(class_info, "__call", false); + let declared_flags = Self::declared_method_param_flags(class_info, "__call", false); let mut effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); Self::relax_magic_call_validation_sig(&mut effective_sig); @@ -447,6 +485,10 @@ impl Checker { for (i, arg_ty) in arg_types.iter().enumerate() { if i < regular_param_count && declared_flags.get(i).copied().unwrap_or(false) + && !Self::method_array_param_keeps_generic_shape( + &impl_class_name, + &method_key, + ) && Self::is_generic_array_hint(&sig.params[i].1) && matches!(arg_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { @@ -474,11 +516,15 @@ impl Checker { sig, regular_param_count, env, - ) { + ) && !method_variadic_param_is_by_ref(sig) + { if let Some((_, variadic_ty)) = sig.params.last_mut() { *variadic_ty = PhpType::Iterable; } - } else if sig.variadic.is_some() && arg_types.len() > regular_param_count { + } else if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -494,6 +540,12 @@ impl Checker { Ok(PhpType::Int) } + /// Returns true for builtin method array params whose accepted shape must remain broad. + fn method_array_param_keeps_generic_shape(class_name: &str, method_key: &str) -> bool { + matches!(class_name, "ReflectionFunction" | "ReflectionMethod") + && method_key == php_symbol_key("invokeArgs") + } + /// Builds synthetic `__call` arguments: `[method_name, [args...]]`. /// /// Constructs a `StringLiteral` for the method name and an `ArrayLiteral` @@ -524,7 +576,7 @@ impl Checker { /// Refines a `__callStatic($name, $args)` signature's array parameter from /// the actual static-call arguments, the static counterpart of /// `specialize_magic_call_signature`. - fn specialize_magic_callstatic_signature( + fn specialize_magic_static_call_signature( &mut self, class_name: &str, args: &[Expr], @@ -713,12 +765,18 @@ impl Checker { return self .check_enum_static_call(&enum_info, class_name, method, args, env, expr.span); } + let method_key = php_symbol_key(method); let normalized_args: Vec; + let mut magic_return_ty = None; + let mut magic_original_args = None; if let Some(class_info) = self.classes.get(class_name) { - if let Some(sig) = class_info.static_methods.get(method) { + if let Some(sig) = class_info.static_methods.get(&method_key) { if let Some(reason) = sig.deprecation.clone() { let message = if reason.is_empty() { - format!("Call to deprecated static method: {}::{}()", class_name, method) + format!( + "Call to deprecated static method: {}::{}()", + class_name, method + ) } else { format!( "Call to deprecated static method: {}::{}() — {}", @@ -728,10 +786,10 @@ impl Checker { self.warnings .push(crate::errors::CompileWarning::new(expr.span, &message)); } - if let Some(visibility) = class_info.static_method_visibilities.get(method) { + if let Some(visibility) = class_info.static_method_visibilities.get(&method_key) { let declaring_class = class_info .static_method_declaring_classes - .get(method) + .get(&method_key) .map(String::as_str) .unwrap_or(class_name); if !self.can_access_member(declaring_class, visibility) { @@ -746,8 +804,13 @@ impl Checker { )); } } - let declared_flags = Self::declared_method_param_flags(class_info, method, true); - let effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); + let declared_flags = + Self::declared_method_param_flags(class_info, &method_key, true); + let mut effective_sig = + Self::callable_sig_for_declared_params(sig, &declared_flags); + if method_key == "__callstatic" { + Self::relax_magic_call_validation_sig(&mut effective_sig); + } normalized_args = self.normalize_named_call_args( &effective_sig, args, @@ -783,16 +846,16 @@ impl Checker { }, )); } - let sig = class_info.methods.get(method).ok_or_else(|| { + let sig = class_info.methods.get(&method_key).ok_or_else(|| { CompileError::new( expr.span, &format!("Undefined method: {}::{}", class_name, method), ) })?; - if let Some(visibility) = class_info.method_visibilities.get(method) { + if let Some(visibility) = class_info.method_visibilities.get(&method_key) { let declaring_class = class_info .method_declaring_classes - .get(method) + .get(&method_key) .map(String::as_str) .unwrap_or(class_name); if !self.can_access_member(declaring_class, visibility) { @@ -807,7 +870,8 @@ impl Checker { )); } } - let declared_flags = Self::declared_method_param_flags(class_info, method, false); + let declared_flags = + Self::declared_method_param_flags(class_info, &method_key, false); let effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); normalized_args = self.normalize_named_call_args( &effective_sig, @@ -848,23 +912,7 @@ impl Checker { ), )?; } - } else if let Some(callstatic_sig) = - class_info.static_methods.get("__callstatic").cloned() - { - // Forward `Foo::missing(...)` to `Foo::__callStatic("missing", [...])`. - let magic_args = Self::magic_call_args(method, args, expr.span); - let mut validation_sig = callstatic_sig.clone(); - Self::relax_magic_call_validation_sig(&mut validation_sig); - self.check_known_callable_call( - &validation_sig, - &magic_args, - expr.span, - env, - &format!("Static method {}::__callStatic", class_name), - )?; - self.specialize_magic_callstatic_signature(class_name, args, env)?; - return Ok(callstatic_sig.return_type.clone()); - } else if class_info.methods.contains_key(method) { + } else if class_info.methods.contains_key(&method_key) { return Err(CompileError::new( expr.span, &format!( @@ -872,18 +920,62 @@ impl Checker { class_name, method ), )); + } else if let Some(sig) = class_info.static_methods.get("__callstatic") { + let magic_args = Self::magic_call_args(method, args, expr.span); + let declared_flags = + Self::declared_method_param_flags(class_info, "__callstatic", true); + let mut effective_sig = + Self::callable_sig_for_declared_params(sig, &declared_flags); + Self::relax_magic_call_validation_sig(&mut effective_sig); + normalized_args = self.normalize_named_call_args( + &effective_sig, + &magic_args, + expr.span, + &format!("Static method {}::__callStatic", class_name), + env, + )?; + if allow_by_ref_spread { + self.check_known_callable_call_allowing_by_ref_spread( + &effective_sig, + &normalized_args, + expr.span, + env, + &format!("Static method {}::__callStatic", class_name), + )?; + } else { + self.check_known_callable_call( + &effective_sig, + &normalized_args, + expr.span, + env, + &format!("Static method {}::__callStatic", class_name), + )?; + } + magic_return_ty = Some(effective_sig.return_type.clone()); + magic_original_args = Some(args.to_vec()); } else { return Err(CompileError::new( expr.span, &format!("Undefined method: {}::{}", class_name, method), )); } + } else if self.eval_barrier_active && matches!(receiver, StaticReceiver::Named(_)) { + for arg in args { + self.infer_type(arg, env)?; + } + return Ok(PhpType::Mixed); } else { return Err(CompileError::new( expr.span, &format!("Undefined class: {}", class_name), )); } + if let Some(return_ty) = magic_return_ty { + if let Some(args) = magic_original_args { + self.specialize_magic_static_call_signature(class_name, &args, env)?; + } + return Ok(return_ty); + } let mut arg_types = Vec::new(); for arg in &normalized_args { arg_types.push(self.infer_type(arg, env)?); @@ -892,7 +984,7 @@ impl Checker { let direct_impl_class_name = if parent_call || self_call { self.classes .get(class_name) - .and_then(|class_info| class_info.method_impl_classes.get(method)) + .and_then(|class_info| class_info.method_impl_classes.get(&method_key)) .cloned() .unwrap_or_else(|| class_name.to_string()) } else { @@ -901,10 +993,10 @@ impl Checker { let static_declared_flags = self .classes .get(class_name) - .map(|class_info| Self::declared_method_param_flags(class_info, method, true)) + .map(|class_info| Self::declared_method_param_flags(class_info, &method_key, true)) .unwrap_or_default(); if let Some(class_info) = self.classes.get_mut(class_name) { - if let Some(sig) = class_info.static_methods.get_mut(method) { + if let Some(sig) = class_info.static_methods.get_mut(&method_key) { let regular_param_count = if sig.variadic.is_some() { sig.params.len().saturating_sub(1) } else { @@ -940,11 +1032,15 @@ impl Checker { sig, regular_param_count, env, - ) { + ) && !method_variadic_param_is_by_ref(sig) + { if let Some((_, variadic_ty)) = sig.params.last_mut() { *variadic_ty = PhpType::Iterable; } - } else if sig.variadic.is_some() && arg_types.len() > regular_param_count { + } else if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -961,12 +1057,12 @@ impl Checker { let instance_declared_flags = self .classes .get(&direct_impl_class_name) - .map(|class_info| Self::declared_method_param_flags(class_info, method, false)) + .map(|class_info| Self::declared_method_param_flags(class_info, &method_key, false)) .unwrap_or_default(); if let Some(sig) = self .classes .get_mut(&direct_impl_class_name) - .and_then(|class_info| class_info.methods.get_mut(method)) + .and_then(|class_info| class_info.methods.get_mut(&method_key)) { let regular_param_count = if sig.variadic.is_some() { sig.params.len().saturating_sub(1) @@ -998,7 +1094,10 @@ impl Checker { } } } - if sig.variadic.is_some() && arg_types.len() > regular_param_count { + if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -1048,6 +1147,19 @@ fn method_variadic_tail_needs_iterable( }) } +/// Returns whether a method signature stores its variadic slot by reference. +fn method_variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + /// Returns true when a spread source can carry string keys into a variadic method tail. fn spread_source_keeps_runtime_keys(expr: &Expr, env: &TypeEnv) -> bool { match &expr.kind { diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index 07f5e22457..d157b6d706 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -310,6 +310,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, return_type: &Option, body: &[Stmt], captures: &[String], @@ -320,6 +321,7 @@ impl Checker { self.infer_closure_type_with_param_hints( params, variadic, + variadic_by_ref, return_type, body, captures, @@ -342,6 +344,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, return_type: &Option, body: &[Stmt], captures: &[String], @@ -353,6 +356,7 @@ impl Checker { let mut closure_sig = self.prepare_closure_signature_context_with_param_hints( params, variadic, + variadic_by_ref, captures, expr.span, env, @@ -363,6 +367,11 @@ impl Checker { .filter(|(_, _, _, is_ref)| *is_ref) .map(|(name, _, _, _)| name.clone()) .collect(); + if variadic_by_ref { + if let Some(name) = variadic { + closure_ref_params.push(name.clone()); + } + } closure_ref_params.extend(capture_refs.iter().cloned()); // Inside a closure body, `$this` is permitted even with no enclosing // class method: the closure may be bound to an object later. Track the @@ -1181,7 +1190,8 @@ fn expr_contains_nullsafe_member(expr: &Expr) -> bool { match &expr.kind { ExprKind::NullsafePropertyAccess { .. } | ExprKind::NullsafeDynamicPropertyAccess { .. } - | ExprKind::NullsafeMethodCall { .. } => true, + | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } => true, ExprKind::PropertyAccess { object, .. } | ExprKind::DynamicPropertyAccess { object, .. } | ExprKind::MethodCall { object, .. } => expr_contains_nullsafe_member(object), diff --git a/src/types/checker/inference/syntactic.rs b/src/types/checker/inference/syntactic.rs index e75a4c12ff..d0a8b08213 100644 --- a/src/types/checker/inference/syntactic.rs +++ b/src/types/checker/inference/syntactic.rs @@ -255,6 +255,7 @@ pub fn infer_expr_type_syntactic(expr: &Expr) -> PhpType { .. } => PhpType::Bool, ExprKind::FunctionCall { name, args } => match name.as_str() { + "eval" => PhpType::Mixed, "substr" | "strtolower" | "strtoupper" | "trim" | "ltrim" | "rtrim" | "str_repeat" | "strrev" | "chr" | "str_replace" | "str_ireplace" | "ucfirst" | "lcfirst" | "ucwords" | "str_pad" | "implode" | "sprintf" | "vsprintf" | "nl2br" | "wordwrap" | "md5" @@ -508,6 +509,25 @@ fn merge_array_literal_element_type_syntactic(existing: PhpType, next: PhpType) #[cfg(test)] mod tests { use super::*; + use crate::names::Name; + use crate::span::Span; + + /// Verifies syntactic type inference treats eval as a runtime Mixed value. + #[test] + fn test_syntactic_eval_return_type_is_mixed() { + let expr = Expr { + kind: ExprKind::FunctionCall { + name: Name::unqualified("eval"), + args: vec![Expr { + kind: ExprKind::StringLiteral("return 1;".to_string()), + span: Span::dummy(), + }], + }, + span: Span::dummy(), + }; + + assert_eq!(infer_expr_type_syntactic(&expr), PhpType::Mixed); + } /// Verifies syntactic indexed plus assoc array union type. #[test] diff --git a/src/types/checker/method_pass.rs b/src/types/checker/method_pass.rs index c677819b95..a5a7d26b88 100644 --- a/src/types/checker/method_pass.rs +++ b/src/types/checker/method_pass.rs @@ -115,11 +115,16 @@ impl Checker { method_env.insert(pname.clone(), ty); } if let Some(variadic_name) = &method.variadic { + let fallback_ty = if method.variadic_by_ref { + PhpType::Array(Box::new(PhpType::Mixed)) + } else { + PhpType::Array(Box::new(PhpType::Int)) + }; let ty = sig_params .as_ref() .and_then(|p| p.get(method.params.len())) .map(|(_, t)| t.clone()) - .unwrap_or(PhpType::Array(Box::new(PhpType::Int))); + .unwrap_or(fallback_ty); method_env.insert(variadic_name.clone(), ty); } if method_key == "__construct" { @@ -190,10 +195,10 @@ impl Checker { continue; } if let Some(Some(prop_name)) = ci.constructor_param_to_prop.get(i) { - if ci.declared_properties.contains(prop_name) { + if ci.visible_property_is_declared(prop_name) { continue; } - if let Some((_, ty)) = ci.properties.iter().find(|(n, _)| n == prop_name) { + if let Some((_, (_, ty))) = ci.visible_property(prop_name) { method_env.insert(pname.clone(), ty.clone()); if let Some(ci_mut) = self.classes.get_mut(&class.name) { if let Some(sig) = ci_mut.methods.get_mut("__construct") { diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 1685e7a0d4..2152a710f7 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -8,7 +8,6 @@ //! Key details: //! - Checker state is populated in ordered phases; later passes assume schemas, builtins, and signatures are complete. -pub(crate) mod builtins; mod builtin_enums; mod builtin_interfaces; mod builtin_iterators; @@ -19,9 +18,8 @@ mod builtin_spl_exceptions; pub(crate) mod builtin_stdclass; mod builtin_types; mod builtin_user_filter; +pub(crate) mod builtins; mod callables; -/// yield_validation -pub(crate) mod yield_validation; mod driver; mod extern_decl; mod functions; @@ -30,6 +28,8 @@ mod method_pass; mod schema; mod stmt_check; mod type_compat; +/// yield_validation +pub(crate) mod yield_validation; use std::collections::{HashMap, HashSet}; @@ -40,8 +40,9 @@ use crate::parser::ast::{ }; use crate::span::Span; use crate::types::{ - CheckResult, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, - InterfaceInfo, PackedClassInfo, PhpType, ThrowAccessInfo, TypeEnv, + collect_attribute_args, collect_attribute_names, CheckResult, ClassInfo, EnumInfo, + ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, PackedClassInfo, PhpType, + ThrowAccessInfo, TypeEnv, }; pub use inference::{infer_expr_type_syntactic, infer_return_type_syntactic}; @@ -97,6 +98,8 @@ pub(crate) struct Checker { pub callable_array_targets: HashMap, /// Tracks first-class callable targets assigned to variables, keyed by variable name. pub first_class_callable_targets: HashMap, + /// Tracks `ReflectionClass` locals whose reflected class is statically known. + pub reflection_class_targets: HashMap, /// Interface definitions collected during the first pass, keyed by canonical name. pub interfaces: HashMap, /// Class definitions collected during the first pass, keyed by canonical name. @@ -109,6 +112,13 @@ pub(crate) struct Checker { /// Canonical interface names declared in the program, available for forward references /// before the full interface definitions are available. pub declared_interfaces: HashSet, + /// Canonical trait names declared in the program, available for reflection + /// and class-like metadata probes that accept traits. + pub declared_traits: HashSet, + /// Reflection-visible method signatures declared directly on each trait. + pub declared_trait_methods: HashMap>, + /// Reflection-visible class constant names declared directly on each trait. + pub declared_trait_constants: HashMap>, /// Name of the class currently being type-checked (used for `$this` resolution). pub current_class: Option, /// Name of the current method being type-checked, when inside a class body. @@ -150,6 +160,11 @@ pub(crate) struct Checker { /// promoting to `AssocArray` like a statically-known string key would. Mirrors /// the lowering's `foreach_int_key_locals` lifetime (per function, not popped). pub foreach_key_locals: HashSet, + /// Whether the active local statement stream has crossed an `eval()` call. + /// + /// Once set, unknown local reads are treated as dynamic `Mixed` values because + /// eval fragments can create caller-scope variables at runtime. + pub eval_barrier_active: bool, /// Active break/continue target depth in the current function or closure body. pub break_continue_depth: usize, /// Stacks of break/continue depths at each enclosing `finally` block boundary, @@ -176,9 +191,13 @@ pub(crate) struct Checker { pub(crate) struct FnDecl { pub params: Vec, pub param_types: Vec>, + /// Attribute groups aligned with the declared parameters plus the variadic parameter, if any. + pub param_attributes: Vec>, pub defaults: Vec>, pub ref_params: Vec, pub variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + pub variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. pub variadic_type: Option, pub return_type: Option, @@ -195,7 +214,10 @@ pub(crate) struct FnDecl { /// a `CheckResult` on success or a `CompileError` on failure. The checker validates /// types, resolves declarations, infers return types, and collects warnings. Abstract /// return types are propagated from concrete implementations before returning. -pub fn check_types(program: &Program, target_platform: Platform) -> Result { +pub fn check_types( + program: &Program, + target_platform: Platform, +) -> Result { let (mut checker, global_env) = driver::check_types_impl(program, target_platform)?; propagate_abstract_return_types(&mut checker); @@ -204,11 +226,23 @@ pub fn check_types(program: &Program, target_platform: Platform) -> Result { ExprKind::Throw(Box::new(rewrite_expr(inner, class_name, parent_name)?)) } + ExprKind::Clone(inner) => { + ExprKind::Clone(Box::new(rewrite_expr(inner, class_name, parent_name)?)) + } ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(rewrite_expr( inner, class_name, @@ -159,6 +162,7 @@ fn rewrite_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -183,6 +187,7 @@ fn rewrite_expr( }) .collect::, CompileError>>()?, variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), body: body.clone(), @@ -266,6 +271,15 @@ fn rewrite_expr( method: method.clone(), args: rewrite_expr_list(args, class_name, parent_name)?, }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(rewrite_expr(object, class_name, parent_name)?), + method: Box::new(rewrite_expr(method, class_name, parent_name)?), + args: rewrite_expr_list(args, class_name, parent_name)?, + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/types/checker/schema/classes/interfaces.rs b/src/types/checker/schema/classes/interfaces.rs index dcd5d55af3..e4affaebbc 100644 --- a/src/types/checker/schema/classes/interfaces.rs +++ b/src/types/checker/schema/classes/interfaces.rs @@ -15,7 +15,7 @@ use crate::names::php_symbol_key; use crate::parser::ast::Visibility; use crate::span::Span; use crate::types::traits::FlattenedClass; -use crate::types::{FunctionSig, PhpType, PropertyHookContract}; +use crate::types::{PhpType, PropertyHookContract}; use super::super::super::Checker; use super::super::validation::{ @@ -136,16 +136,15 @@ pub(super) fn validate_interface_contracts( )?; } for method_name in &interface_info.static_method_order { - let required_sig = interface_info - .static_methods - .get(method_name) - .expect("type checker bug: missing interface static method signature"); - validate_interface_static_method( + validate_static_interface_method( state, class, &interface_name, method_name, - required_sig, + class_map, + checker, + next_class_id, + building, )?; } for property_name in &interface_info.property_order { @@ -169,6 +168,139 @@ pub(super) fn validate_interface_contracts( Ok(()) } +/// Validates that `class` implements the static interface method `method_name`. +/// +/// Checks static/instance kind, signature compatibility, return type declarations, +/// public visibility, and object return metadata. Abstract classes may defer the +/// required static method into their own abstract method set. +#[allow(clippy::too_many_arguments)] +fn validate_static_interface_method( + state: &mut ClassBuildState, + class: &FlattenedClass, + interface_name: &str, + method_name: &str, + class_map: &HashMap, + checker: &mut Checker, + next_class_id: &mut u64, + building: &mut HashSet, +) -> Result<(), CompileError> { + if state.method_sigs.contains_key(method_name) { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Cannot use instance method to satisfy static interface contract: {}::{}", + class.name, method_name + ), + )); + } + let interface_info = checker + .interfaces + .get(interface_name) + .expect("type checker bug: interface exists") + .clone(); + let required_sig = interface_info + .static_methods + .get(method_name) + .expect("type checker bug: missing static interface method signature"); + let actual_sig = match state.static_sigs.get(method_name) { + Some(sig) => sig, + None if class.is_abstract => { + state + .static_sigs + .insert(method_name.to_string(), required_sig.clone()); + state + .static_method_visibilities + .insert(method_name.to_string(), Visibility::Public); + state + .static_method_declaring_classes + .insert(method_name.to_string(), class.name.clone()); + state.static_method_impl_classes.remove(method_name); + if !state.static_vtable_slots.contains_key(method_name) { + let slot = state.static_vtable_methods.len(); + state + .static_vtable_slots + .insert(method_name.to_string(), slot); + state.static_vtable_methods.push(method_name.to_string()); + } + return Ok(()); + } + None => { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Class {} must implement interface static method {}::{}", + class.name, interface_name, method_name + ), + )) + } + }; + validate_signature_compatibility( + crate::span::Span::dummy(), + &class.name, + method_name, + actual_sig, + required_sig, + "static method", + "implementing interface", + )?; + let actual_method = class + .methods + .iter() + .find(|m| m.is_static && php_symbol_key(&m.name) == method_name); + if required_sig.declared_return && !actual_sig.declared_return { + return Err(CompileError::new( + actual_method + .map(|m| m.span) + .unwrap_or_else(crate::span::Span::dummy), + &format!( + "Cannot implement interface static method {}::{} without declaring a compatible return type (interface returns {})", + class.name, method_name, required_sig.return_type + ), + )); + } + if let PhpType::Object(actual_name) = &actual_sig.return_type { + if actual_name != &class.name + && class_map.contains_key(actual_name) + && !checker.classes.contains_key(actual_name) + { + super::build_class_info_recursive( + actual_name, + class_map, + checker, + next_class_id, + building, + )?; + } + } + if required_sig.declared_return + && !declared_return_type_compatible( + checker, + &required_sig.return_type, + &actual_sig.return_type, + ) + { + return Err(CompileError::new( + actual_method + .map(|m| m.span) + .unwrap_or_else(crate::span::Span::dummy), + &format!( + "Cannot implement interface static method {}::{} with incompatible return type {} (interface returns {})", + class.name, method_name, actual_sig.return_type, required_sig.return_type + ), + )); + } + if state.static_method_visibilities.get(method_name) != Some(&Visibility::Public) { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Interface static method implementation must be public: {}::{}", + class.name, method_name + ), + )); + } + Ok(()) +} + /// Validates that a concrete (non-abstract) `class` has implementations for all deferred abstract /// methods and properties accumulated in `state`. /// @@ -234,37 +366,6 @@ pub(super) fn ensure_concrete_class_implements_abstracts( /// Checks signature compatibility, return type declarations, visibility (must be public), and /// that non-public static methods cannot satisfy interface contracts. For abstract classes, /// missing methods are inserted into the class state as deferred contracts. -/// Validates that `class` satisfies the static interface method `method_name` from -/// `interface_name` — a concrete class must declare a compatible `static` method; an abstract -/// class may defer. PHP 8.3+ static interface methods. -fn validate_interface_static_method( - state: &ClassBuildState, - class: &FlattenedClass, - interface_name: &str, - method_name: &str, - required_sig: &FunctionSig, -) -> Result<(), CompileError> { - match state.static_sigs.get(method_name) { - Some(actual_sig) => validate_signature_compatibility( - Span::dummy(), - &class.name, - method_name, - actual_sig, - required_sig, - "method", - "implementing interface", - ), - None if class.is_abstract => Ok(()), - None => Err(CompileError::new( - Span::dummy(), - &format!( - "Class {} must implement static interface method {}::{}", - class.name, interface_name, method_name - ), - )), - } -} - #[allow(clippy::too_many_arguments)] fn validate_interface_method( state: &mut ClassBuildState, diff --git a/src/types/checker/schema/classes/methods.rs b/src/types/checker/schema/classes/methods.rs index 0d14c4df8b..a938c1d4df 100644 --- a/src/types/checker/schema/classes/methods.rs +++ b/src/types/checker/schema/classes/methods.rs @@ -18,7 +18,8 @@ use super::super::validation::{ build_method_sig, matches_global_builtin_attribute, validate_override_signature, visibility_rank, }; -use super::state::{collect_attribute_args, collect_attribute_names, ClassBuildState}; +use super::state::ClassBuildState; +use super::{collect_attribute_args, collect_attribute_names}; /// Validates and registers all methods of a flattened class into the build state. /// Enforces abstract/final modifiers, method body presence, and delegates to @@ -134,7 +135,7 @@ fn apply_static_method( if let Some(parent_sig) = state.static_sigs.get(&method_key) { validate_override_signature(checker, &class.name, method, parent_sig, true)?; } else if has_override_attribute(method) - && !interface_declares_method(checker, state, class, &method_key) + && !interface_declares_method(checker, state, class, &method_key, true) { return Err(missing_override_target(class, method)); } @@ -231,7 +232,7 @@ fn apply_instance_method( if let Some(parent_sig) = state.method_sigs.get(&method_key) { validate_override_signature(checker, &class.name, method, parent_sig, false)?; } else if has_override_attribute(method) - && !interface_declares_method(checker, state, class, &method_key) + && !interface_declares_method(checker, state, class, &method_key, false) { return Err(missing_override_target(class, method)); } @@ -313,17 +314,19 @@ fn has_override_attribute(method: &ClassMethod) -> bool { } /// Returns `true` if any interface implemented by the class (directly or -/// transitively via parent interfaces or parent classes) declares the method — -/// instance OR PHP 8.3+ static (a `#[\Override]` on a static interface-method -/// implementation is valid). Seeds from `class.implements` because `apply_methods` -/// runs before `collect_interfaces` has added the class's own clause to -/// `state.interfaces`, plus `state.interfaces`, which at this point carries the -/// interfaces inherited from the parent class chain. +/// transitively via parent interfaces or inherited parent-class contracts) +/// declares the method with the requested static/instance kind. +/// +/// Seeds from `class.implements` because `apply_methods` runs before +/// `collect_interfaces` has added the class's own clause to `state.interfaces`; +/// also scans `state.interfaces`, which already carries interfaces inherited +/// from the parent class chain. fn interface_declares_method( checker: &Checker, state: &ClassBuildState, class: &FlattenedClass, method_key: &str, + is_static: bool, ) -> bool { let mut visited = std::collections::HashSet::new(); let mut queue: Vec = class.implements.clone(); @@ -335,7 +338,12 @@ fn interface_declares_method( let Some(info) = checker.interfaces.get(&name) else { continue; }; - if info.methods.contains_key(method_key) || info.static_methods.contains_key(method_key) { + let declares_method = if is_static { + info.static_methods.contains_key(method_key) + } else { + info.methods.contains_key(method_key) + }; + if declares_method { return true; } queue.extend(info.parents.iter().cloned()); diff --git a/src/types/checker/schema/classes/mod.rs b/src/types/checker/schema/classes/mod.rs index ce4f6eae69..55ab3d1943 100644 --- a/src/types/checker/schema/classes/mod.rs +++ b/src/types/checker/schema/classes/mod.rs @@ -25,6 +25,8 @@ use super::super::Checker; use super::validation::build_constructor_param_map; use state::ClassBuildState; +pub(super) use crate::types::{collect_attribute_args, collect_attribute_names}; + /// Recursively builds and registers `ClassInfo` for `class_name` and its inheritance chain. /// /// Uses `building` set to detect circular inheritance. Validates modifiers, resolves the parent @@ -67,6 +69,7 @@ pub(crate) fn build_class_info_recursive( properties::apply_properties(&mut state, &class, checker)?; methods::apply_methods(&mut state, &class, checker)?; interfaces::collect_interfaces(&mut state, &class, class_map, checker)?; + validate_final_constant_constraints(&class, parent_info.as_ref(), &state, checker)?; interfaces::validate_interface_contracts( &mut state, &class, @@ -77,8 +80,7 @@ pub(crate) fn build_class_info_recursive( )?; interfaces::ensure_concrete_class_implements_abstracts(&state, &class)?; - let constructor_param_to_prop = - constructor_param_to_prop_for(&class, parent_info.as_ref()); + let constructor_param_to_prop = constructor_param_to_prop_for(&class, parent_info.as_ref()); let class_info = state.into_class_info(*next_class_id, &class, constructor_param_to_prop)?; checker.classes.insert(class.name.clone(), class_info); *next_class_id += 1; @@ -166,7 +168,10 @@ fn validate_parent_constraints( if parent.is_final { return Err(CompileError::new( crate::span::Span::dummy(), - &format!("Class {} cannot extend final class {}", class.name, parent_name), + &format!( + "Class {} cannot extend final class {}", + class.name, parent_name + ), )); } if class.is_readonly_class != parent.is_readonly_class { @@ -183,6 +188,50 @@ fn validate_parent_constraints( Ok(()) } +/// Validates PHP final class-constant inheritance constraints for one class. +fn validate_final_constant_constraints( + class: &FlattenedClass, + parent_info: Option<&ClassInfo>, + state: &ClassBuildState, + checker: &Checker, +) -> Result<(), CompileError> { + for constant in &class.constants { + if constant.is_final && constant.visibility == crate::parser::ast::Visibility::Private { + return Err(CompileError::new( + constant.span, + &format!( + "Private constant {}::{} cannot be final", + class.name, constant.name + ), + )); + } + if parent_info.is_some_and(|parent| parent.final_constants.contains(&constant.name)) { + return Err(CompileError::new( + constant.span, + &format!( + "{}::{} cannot override final constant", + class.name, constant.name + ), + )); + } + for interface_name in &state.interfaces { + let Some(interface_info) = checker.interfaces.get(interface_name) else { + continue; + }; + if interface_info.final_constants.contains(&constant.name) { + return Err(CompileError::new( + constant.span, + &format!( + "{}::{} cannot override final interface constant", + class.name, constant.name + ), + )); + } + } + } + Ok(()) +} + /// Builds a mapping from constructor parameter position to promoted property name for `class`. /// /// If `class` defines `__construct`, maps each parameter (by order) to the property it promotes, diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index b9019beeba..afd9ceb7f7 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -17,7 +17,8 @@ use crate::types::PhpType; use super::super::super::{infer_expr_type_syntactic, Checker}; use super::super::validation::visibility_rank; use super::super::interfaces::{build_property_contract, merge_property_contract}; -use super::state::{collect_attribute_args, collect_attribute_names, ClassBuildState}; +use super::state::ClassBuildState; +use super::{collect_attribute_args, collect_attribute_names}; /// Applies property schema validation and metadata for all static and instance /// properties declared in `class`. Static properties are validated for PHP @@ -247,6 +248,7 @@ fn apply_instance_property( ); } + let mut is_declared_slot = false; let ty = if let Some(declared_ty) = resolve_property_declared_type(checker, &class.name, prop)? { checker.validate_schema_declared_default_type( &declared_ty, @@ -255,6 +257,7 @@ fn apply_instance_property( &format!("Property {}::${} default", class.name, prop.name), )?; state.declared_properties.insert(prop.name.clone()); + is_declared_slot = true; refine_declared_array_type_from_default(declared_ty, prop.default.as_ref()) } else if let Some(default) = &prop.default { infer_untyped_property_default_type(default) @@ -270,6 +273,8 @@ fn apply_instance_property( state .property_declaring_classes .insert(prop.name.clone(), class.name.clone()); + state.property_declared_slots.push(is_declared_slot); + state.property_reference_slots.push(prop.by_ref); state .property_attribute_names .insert(prop.name.clone(), collect_attribute_names(&prop.attributes)); @@ -292,6 +297,11 @@ fn apply_instance_property( if prop.by_ref { state.reference_properties.insert(prop.name.clone()); } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } // Fresh declarations only ever add to `abstract_properties`. Concrete // declarations of a brand-new property never appear there in the first // place, so there is nothing to remove; the only path that clears a @@ -321,6 +331,14 @@ fn apply_instance_property_redeclaration( prop: &ClassProperty, parent_declaring_class: &str, ) -> Result<(), CompileError> { + let inherited_visibility = state + .property_visibilities + .get(&prop.name) + .cloned() + .unwrap_or(Visibility::Public); + if inherited_visibility == Visibility::Private { + return apply_private_parent_property_shadowing(state, class, checker, prop); + } if state.final_properties.contains(&prop.name) { return Err(CompileError::new( prop.span, @@ -331,15 +349,16 @@ fn apply_instance_property_redeclaration( )); } let declared_ty = resolve_property_declared_type(checker, &class.name, prop)?; - validate_instance_property_override( - state, - class, - checker, - prop, - declared_ty.as_ref(), - parent_declaring_class, + validate_instance_property_override( + state, + class, + checker, + prop, + declared_ty.as_ref(), + parent_declaring_class, )?; + let is_declared_slot = declared_ty.is_some(); let ty = if let Some(declared_ty) = declared_ty { checker.validate_schema_declared_default_type( &declared_ty, @@ -358,6 +377,12 @@ fn apply_instance_property_redeclaration( let slot = find_instance_property_slot(state, &prop.name); state.prop_types[slot] = (prop.name.clone(), ty); state.defaults[slot] = prop.default.clone(); + if let Some(slot_declared) = state.property_declared_slots.get_mut(slot) { + *slot_declared = is_declared_slot; + } + if let Some(slot_reference) = state.property_reference_slots.get_mut(slot) { + *slot_reference = prop.by_ref; + } state .property_declaring_classes .insert(prop.name.clone(), class.name.clone()); @@ -395,6 +420,104 @@ fn apply_instance_property_redeclaration( if prop.by_ref { state.reference_properties.insert(prop.name.clone()); } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } + Ok(()) +} + +/// Records a child property with the same name as a private parent property as a +/// fresh physical slot, matching PHP's private-property shadowing semantics. +fn apply_private_parent_property_shadowing( + state: &mut ClassBuildState, + class: &FlattenedClass, + checker: &Checker, + prop: &ClassProperty, +) -> Result<(), CompileError> { + let declared_ty = resolve_property_declared_type(checker, &class.name, prop)?; + let is_declared_slot = declared_ty.is_some(); + let ty = if let Some(declared_ty) = declared_ty { + checker.validate_declared_default_type( + &declared_ty, + prop.default.as_ref(), + prop.span, + &format!("Property {}::${} default", class.name, prop.name), + )?; + state.declared_properties.insert(prop.name.clone()); + refine_declared_array_type_from_default(declared_ty, prop.default.as_ref()) + } else if let Some(default) = &prop.default { + state.declared_properties.remove(&prop.name); + infer_expr_type_syntactic(default) + } else { + state.declared_properties.remove(&prop.name); + PhpType::Int + }; + + let slot_index = state.prop_types.len(); + state.prop_types.push((prop.name.clone(), ty)); + state + .property_offsets + .insert(prop.name.clone(), 8 + slot_index * 16); + state.defaults.push(prop.default.clone()); + state.property_declared_slots.push(is_declared_slot); + state.property_reference_slots.push(prop.by_ref); + state + .property_declaring_classes + .insert(prop.name.clone(), class.name.clone()); + state + .property_attribute_names + .insert(prop.name.clone(), collect_attribute_names(&prop.attributes)); + state + .property_attribute_args + .insert(prop.name.clone(), collect_attribute_args(&prop.attributes)); + state + .property_visibilities + .insert(prop.name.clone(), prop.visibility.clone()); + apply_set_visibility(state, prop); + replace_active_property_flags(state, class, checker, prop)?; + Ok(()) +} + +/// Replaces the name-keyed flags for the property currently visible from this +/// class after private-parent shadowing creates a fresh child slot. +fn replace_active_property_flags( + state: &mut ClassBuildState, + class: &FlattenedClass, + checker: &Checker, + prop: &ClassProperty, +) -> Result<(), CompileError> { + if prop.is_final { + state.final_properties.insert(prop.name.clone()); + } else { + state.final_properties.remove(&prop.name); + } + if class.is_readonly_class || prop.readonly { + state.readonly_properties.insert(prop.name.clone()); + } else { + state.readonly_properties.remove(&prop.name); + } + if prop.by_ref { + state.reference_properties.insert(prop.name.clone()); + } else { + state.reference_properties.remove(&prop.name); + } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } + if prop.is_abstract { + state.abstract_properties.insert(prop.name.clone()); + let contract = build_property_contract(checker, &class.name, prop)?; + state + .abstract_property_hooks + .insert(prop.name.clone(), contract); + } else { + state.abstract_properties.remove(&prop.name); + state.abstract_property_hooks.remove(&prop.name); + } Ok(()) } @@ -417,17 +540,6 @@ fn validate_instance_property_override( .get(&prop.name) .cloned() .unwrap_or(Visibility::Public); - if inherited_visibility == Visibility::Private { - // PHP allows shadowing a private parent property with a fresh slot in the child, - // but our property layout uses one slot per name. Reject until proper scoping is added. - return Err(CompileError::new( - prop.span, - &format!( - "Cannot redeclare property {}::${}: parent class {} has a private property with the same name (shadowing private parent properties is not yet supported)", - class.name, prop.name, parent_declaring_class - ), - )); - } if visibility_rank(&prop.visibility) < visibility_rank(&inherited_visibility) { return Err(CompileError::new( prop.span, @@ -579,10 +691,10 @@ fn inherited_static_property_type(state: &ClassBuildState, property: &str) -> Ph /// parent's resolved types in `state.prop_types`. Returns `PhpType::Int` /// if the property is not found, matching the undeclared-property default. fn inherited_instance_property_type(state: &ClassBuildState, property: &str) -> PhpType { + let slot = find_instance_property_slot(state, property); state .prop_types - .iter() - .find(|(name, _)| name == property) + .get(slot) .map(|(_, ty)| ty.clone()) .unwrap_or(PhpType::Int) } @@ -591,10 +703,20 @@ fn inherited_instance_property_type(state: &ClassBuildState, property: &str) -> /// Panics if the property is not found; the caller is responsible for ensuring /// the property exists via prior checks on `state.property_declaring_classes`. fn find_instance_property_slot(state: &ClassBuildState, name: &str) -> usize { + if let Some(index) = state + .property_offsets + .get(name) + .and_then(|offset| offset.checked_sub(8)) + .filter(|payload_offset| payload_offset % 16 == 0) + .map(|payload_offset| payload_offset / 16) + .filter(|index| *index < state.prop_types.len()) + { + return index; + } state .prop_types .iter() - .position(|(prop_name, _)| prop_name == name) + .rposition(|(prop_name, _)| prop_name == name) .expect("redeclaration path: property must exist in prop_types when declaring_classes has it") } diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index d54674e11c..f5b2e35f50 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -34,9 +34,12 @@ pub(super) struct ClassBuildState { pub(super) property_visibilities: HashMap, pub(super) property_set_visibilities: HashMap, pub(super) declared_properties: HashSet, + pub(super) property_declared_slots: Vec, pub(super) final_properties: HashSet, pub(super) readonly_properties: HashSet, pub(super) reference_properties: HashSet, + pub(super) promoted_properties: HashSet, + pub(super) property_reference_slots: Vec, pub(super) abstract_properties: HashSet, pub(super) abstract_property_hooks: HashMap, pub(super) static_prop_types: Vec<(String, PhpType)>, @@ -100,8 +103,29 @@ impl ClassBuildState { constructor_param_to_prop: Vec>, ) -> Result { let attribute_args = collect_attribute_args(&class.attributes); + let constant_attribute_names = class + .constants + .iter() + .map(|constant| { + ( + constant.name.clone(), + collect_attribute_names(&constant.attributes), + ) + }) + .collect(); + let constant_attribute_args = class + .constants + .iter() + .map(|constant| { + ( + constant.name.clone(), + collect_attribute_args(&constant.attributes), + ) + }) + .collect(); Ok(ClassInfo { class_id, + declaration_span: class.span, parent: class.extends.clone(), is_abstract: class.is_abstract, is_final: class.is_final, @@ -118,13 +142,27 @@ impl ClassBuildState { )) }) .collect::, CompileError>>()?, + constant_visibilities: class + .constants + .iter() + .map(|constant| (constant.name.clone(), constant.visibility.clone())) + .collect(), + final_constants: class + .constants + .iter() + .filter(|constant| constant.is_final) + .map(|constant| constant.name.clone()) + .collect(), attribute_names: collect_attribute_names(&class.attributes), attribute_args, method_attribute_names: self.method_attribute_names, method_attribute_args: self.method_attribute_args, property_attribute_names: self.property_attribute_names, property_attribute_args: self.property_attribute_args, + constant_attribute_names, + constant_attribute_args, used_traits: class.used_traits.clone(), + trait_aliases: class.trait_aliases.clone(), properties: self.prop_types, property_offsets: self.property_offsets, property_declaring_classes: self.property_declaring_classes, @@ -132,10 +170,13 @@ impl ClassBuildState { property_visibilities: self.property_visibilities, property_set_visibilities: self.property_set_visibilities, declared_properties: self.declared_properties, + property_declared_slots: self.property_declared_slots, final_properties: self.final_properties, readonly_properties: self.readonly_properties, reference_properties: self.reference_properties, owned_reference_properties: HashSet::new(), + promoted_properties: self.promoted_properties, + property_reference_slots: self.property_reference_slots, abstract_properties: self.abstract_properties, abstract_property_hooks: self.abstract_property_hooks, static_properties: self.static_prop_types, @@ -165,14 +206,13 @@ impl ClassBuildState { constructor_param_to_prop, }) } - } /// Collect attribute names from a class's attribute groups, preserving source /// order. Name resolution has already canonicalised fully-qualified names by /// the time this runs, so names are emitted in ReflectionAttribute::getName() /// shape without a synthetic leading backslash. -pub(super) fn collect_attribute_names( +pub(in crate::types::checker::schema) fn collect_attribute_names( groups: &[crate::parser::ast::AttributeGroup], ) -> Vec { let mut out = Vec::new(); @@ -195,7 +235,7 @@ pub(super) fn collect_attribute_names( /// `#[Status(-1)]` survives parsing. Unsupported metadata is marked as /// `None` so legal PHP attribute syntax can still compile until a runtime /// reflection helper needs the missing argument payload. -pub(super) fn collect_attribute_args( +pub(in crate::types::checker::schema) fn collect_attribute_args( groups: &[crate::parser::ast::AttributeGroup], ) -> Vec>> { use crate::parser::ast::ExprKind; @@ -257,6 +297,12 @@ fn fold_attr_value(expr: &crate::parser::ast::Expr) -> Option Some(AttrArgValue::Str(name.as_str().to_string())), + ExprKind::ClassConstant { .. } => None, ExprKind::Negate(inner) => match &inner.kind { ExprKind::IntLiteral(n) => Some(AttrArgValue::Int(n.wrapping_neg())), ExprKind::FloatLiteral(n) => Some(AttrArgValue::Float((-n).to_bits())), @@ -339,6 +385,20 @@ impl ClassBuildState { self.prop_types.push((name.clone(), ty.clone())); self.property_offsets.insert(name.clone(), 8 + index * 16); self.defaults.push(parent.defaults[index].clone()); + self.property_declared_slots.push( + parent + .property_declared_slots + .get(index) + .copied() + .unwrap_or_else(|| parent.declared_properties.contains(name)), + ); + self.property_reference_slots.push( + parent + .property_reference_slots + .get(index) + .copied() + .unwrap_or_else(|| parent.reference_properties.contains(name)), + ); if let Some(visibility) = parent.property_visibilities.get(name) { self.property_visibilities .insert(name.clone(), visibility.clone()); @@ -371,6 +431,9 @@ impl ClassBuildState { if parent.reference_properties.contains(name) { self.reference_properties.insert(name.clone()); } + if parent.promoted_properties.contains(name) { + self.promoted_properties.insert(name.clone()); + } if parent.abstract_properties.contains(name) { self.abstract_properties.insert(name.clone()); } diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 143edc4638..6784f995f2 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -16,6 +16,7 @@ use crate::parser::ast::{ClassMethod, ExprKind, Visibility}; use crate::types::{ClassInfo, EnumCaseInfo, EnumCaseValue, EnumInfo, FunctionSig, PhpType}; use super::super::Checker; +use super::classes::{collect_attribute_args, collect_attribute_names}; use super::validation::build_method_sig; /// Propagates concrete return types from overrides to their abstract parent declarations. @@ -101,6 +102,7 @@ pub(crate) fn propagate_abstract_return_types(checker: &mut Checker) { /// - `backing_type`: optional `TypeExpr` for backed enums /// - `cases`: parsed enum case declarations /// - `span`: source location for error reporting +/// - `used_traits` / `trait_aliases`: flattened direct enum trait-use metadata /// - `checker`: type checker state (classes, interfaces, enums, resolve_type_expr) /// - `next_class_id`: incrementing class ID counter /// @@ -118,6 +120,8 @@ pub(crate) fn build_enum_info( implements: &[crate::names::Name], user_methods: &[crate::parser::ast::ClassMethod], user_constants: &[crate::parser::ast::ClassConst], + used_traits: &[String], + trait_aliases: &[(String, String)], span: crate::span::Span, checker: &mut Checker, next_class_id: &mut u64, @@ -224,6 +228,8 @@ pub(crate) fn build_enum_info( enum_cases.push(EnumCaseInfo { name: case.name.clone(), value, + attribute_names: collect_attribute_names(&case.attributes), + attribute_args: collect_attribute_args(&case.attributes), }); } @@ -234,6 +240,9 @@ pub(crate) fn build_enum_info( implements, user_methods, user_constants, + used_traits, + trait_aliases, + span, checker, next_class_id, ) @@ -243,8 +252,9 @@ pub(crate) fn build_enum_info( /// /// Used by parsed enum declarations and builtin enum injection after case/backing /// validation has already happened. Synthesizes the static enum methods exposed -/// by PHP: all enums get `cases()`, while backed enums also get `from()` and -/// `tryFrom()`. +/// by PHP, and preserves flattened trait-use metadata for reflection/runtime +/// class-like queries. All enums get `cases()`, while backed enums also get +/// `from()` and `tryFrom()`. #[allow(clippy::too_many_arguments)] pub(crate) fn insert_enum_metadata( name: &str, @@ -253,6 +263,9 @@ pub(crate) fn insert_enum_metadata( implements: &[crate::names::Name], user_methods: &[ClassMethod], user_constants: &[crate::parser::ast::ClassConst], + used_traits: &[String], + trait_aliases: &[(String, String)], + declaration_span: crate::span::Span, checker: &mut Checker, next_class_id: &mut u64, ) -> Result<(), CompileError> { @@ -262,30 +275,43 @@ pub(crate) fn insert_enum_metadata( let mut defaults = Vec::new(); let mut property_visibilities = HashMap::new(); let mut declared_properties = HashSet::new(); + let mut property_declared_slots = Vec::new(); let final_properties = HashSet::new(); let mut readonly_properties = HashSet::new(); let reference_properties = HashSet::new(); + let mut property_reference_slots = Vec::new(); if let Some(backing_ty) = &backing_type { - properties.push(("value".to_string(), backing_ty.clone())); - property_offsets.insert("value".to_string(), 8); - property_declaring_classes.insert("value".to_string(), name.to_string()); - defaults.push(None); - property_visibilities.insert("value".to_string(), Visibility::Public); - declared_properties.insert("value".to_string()); - readonly_properties.insert("value".to_string()); + push_enum_readonly_property( + "value", + backing_ty.clone(), + name, + &mut properties, + &mut property_offsets, + &mut property_declaring_classes, + &mut defaults, + &mut property_visibilities, + &mut declared_properties, + &mut property_declared_slots, + &mut readonly_properties, + &mut property_reference_slots, + ); } - // Every enum case (pure or backed) exposes a readonly public `name` string holding the case - // identifier, mirroring PHP's `UnitEnum::$name`. Append it after any backing `value` so backed - // enums keep `value` at offset 8; the offset matches the singleton property slot layout used by - // EIR codegen (`8 + index * 16`). - let name_offset = 8 + properties.len() * 16; - properties.push(("name".to_string(), PhpType::Str)); - property_offsets.insert("name".to_string(), name_offset); - property_declaring_classes.insert("name".to_string(), name.to_string()); - defaults.push(None); - property_visibilities.insert("name".to_string(), Visibility::Public); - declared_properties.insert("name".to_string()); - readonly_properties.insert("name".to_string()); + // Append `name` after any backing `value` so backed enums keep `value` at + // offset 8, matching singleton initialization in codegen. + push_enum_readonly_property( + "name", + PhpType::Str, + name, + &mut properties, + &mut property_offsets, + &mut property_declaring_classes, + &mut defaults, + &mut property_visibilities, + &mut declared_properties, + &mut property_declared_slots, + &mut readonly_properties, + &mut property_reference_slots, + ); let mut static_methods = HashMap::new(); let mut static_method_visibilities = HashMap::new(); @@ -295,6 +321,8 @@ pub(crate) fn insert_enum_metadata( "cases".to_string(), FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Array(Box::new(PhpType::Object(name.to_string()))), declared_return: true, @@ -314,6 +342,8 @@ pub(crate) fn insert_enum_metadata( method_name.to_string(), FunctionSig { params: vec![("value".to_string(), backing_ty.clone())], + param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: if method_name == "from" { PhpType::Object(name.to_string()) @@ -374,8 +404,28 @@ pub(crate) fn insert_enum_metadata( // User-declared enum constants. Values are kept as their parsed expressions, matching the // class-constant representation. let mut constants = HashMap::new(); + let mut constant_visibilities = HashMap::new(); + let mut final_constants = HashSet::new(); + let mut constant_attribute_names = HashMap::new(); + let mut constant_attribute_args = HashMap::new(); for constant in user_constants { constants.insert(constant.name.clone(), constant.value.clone()); + constant_visibilities.insert(constant.name.clone(), constant.visibility.clone()); + if constant.is_final { + final_constants.insert(constant.name.clone()); + } + constant_attribute_names.insert( + constant.name.clone(), + collect_attribute_names(&constant.attributes), + ); + constant_attribute_args.insert( + constant.name.clone(), + collect_attribute_args(&constant.attributes), + ); + } + for case in &enum_cases { + constant_attribute_names.insert(case.name.clone(), case.attribute_names.clone()); + constant_attribute_args.insert(case.name.clone(), case.attribute_args.clone()); } let interfaces: Vec = implements @@ -387,19 +437,25 @@ pub(crate) fn insert_enum_metadata( name.to_string(), ClassInfo { class_id: *next_class_id, + declaration_span, parent: None, is_abstract: false, is_final: true, is_readonly_class: true, allow_dynamic_properties: false, constants, + constant_visibilities, + final_constants, attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), - used_traits: Vec::new(), + constant_attribute_names, + constant_attribute_args, + used_traits: used_traits.to_vec(), + trait_aliases: trait_aliases.to_vec(), properties, property_offsets, property_declaring_classes, @@ -407,10 +463,13 @@ pub(crate) fn insert_enum_metadata( property_visibilities, property_set_visibilities: HashMap::new(), declared_properties, + property_declared_slots, final_properties, readonly_properties, reference_properties, owned_reference_properties: HashSet::new(), + promoted_properties: HashSet::new(), + property_reference_slots, abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), @@ -450,3 +509,31 @@ pub(crate) fn insert_enum_metadata( *next_class_id += 1; Ok(()) } + +/// Appends one synthetic public readonly enum case property to class metadata. +fn push_enum_readonly_property( + property: &str, + php_type: PhpType, + enum_name: &str, + properties: &mut Vec<(String, PhpType)>, + property_offsets: &mut HashMap, + property_declaring_classes: &mut HashMap, + defaults: &mut Vec>, + property_visibilities: &mut HashMap, + declared_properties: &mut HashSet, + property_declared_slots: &mut Vec, + readonly_properties: &mut HashSet, + property_reference_slots: &mut Vec, +) { + let offset = 8 + properties.len() * 16; + let property = property.to_string(); + properties.push((property.clone(), php_type)); + property_offsets.insert(property.clone(), offset); + property_declaring_classes.insert(property.clone(), enum_name.to_string()); + defaults.push(None); + property_visibilities.insert(property.clone(), Visibility::Public); + declared_properties.insert(property.clone()); + property_declared_slots.push(true); + readonly_properties.insert(property); + property_reference_slots.push(false); +} diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index 7fa53e88ef..292646385b 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -70,7 +70,8 @@ pub(crate) fn build_interface_info_recursive( let mut method_order = Vec::new(); let mut method_slots = HashMap::new(); let mut static_methods = HashMap::new(); - let mut static_method_order: Vec = Vec::new(); + let mut static_method_declaring_interfaces = HashMap::new(); + let mut static_method_order = Vec::new(); let mut properties = HashMap::new(); let mut property_order = Vec::new(); @@ -107,6 +108,13 @@ pub(crate) fn build_interface_info_recursive( .methods .get(method_name) .expect("type checker bug: missing interface parent method signature"); + if static_methods.contains_key(method_name) { + return Err(interface_method_kind_conflict( + interface.span, + &interface.name, + method_name, + )); + } if let Some(existing_sig) = methods.get(method_name) { validate_signature_compatibility( interface.span, @@ -134,7 +142,14 @@ pub(crate) fn build_interface_info_recursive( let parent_sig = parent_info .static_methods .get(method_name) - .expect("type checker bug: missing interface parent static method signature"); + .expect("type checker bug: missing parent static interface method signature"); + if methods.contains_key(method_name) { + return Err(interface_method_kind_conflict( + interface.span, + &interface.name, + method_name, + )); + } if let Some(existing_sig) = static_methods.get(method_name) { validate_signature_compatibility( interface.span, @@ -142,12 +157,18 @@ pub(crate) fn build_interface_info_recursive( method_name, existing_sig, parent_sig, - "method", + "static method", "combining interface parent", )?; continue; } static_methods.insert(method_name.clone(), parent_sig.clone()); + let declaring = parent_info + .static_method_declaring_interfaces + .get(method_name) + .cloned() + .unwrap_or_else(|| parent_name.clone()); + static_method_declaring_interfaces.insert(method_name.clone(), declaring); static_method_order.push(method_name.clone()); } for property_name in &parent_info.property_order { @@ -243,26 +264,39 @@ pub(crate) fn build_interface_info_recursive( let sig = build_method_sig(checker, method)?; if method.is_static { - // PHP 8.3+ static interface method: a bodyless static contract. Recorded apart - // from instance methods (no vtable slot — dispatch is by class). Implementing - // classes satisfy it with a static method (see validate_interface_contracts). - if let Some(existing) = static_methods.get(&method_key) { + if methods.contains_key(&method_key) { + return Err(interface_method_kind_conflict( + method.span, + &interface.name, + &method.name, + )); + } + if let Some(parent_sig) = static_methods.get(&method_key) { validate_signature_compatibility( method.span, &interface.name, &method.name, &sig, - existing, - "method", + parent_sig, + "static method", "redeclaring interface", )?; } static_methods.insert(method_key.clone(), sig); + static_method_declaring_interfaces + .insert(method_key.clone(), interface.name.clone()); if !static_method_order.contains(&method_key) { - static_method_order.push(method_key.clone()); + static_method_order.push(method_key); } continue; } + if static_methods.contains_key(&method_key) { + return Err(interface_method_kind_conflict( + method.span, + &interface.name, + &method.name, + )); + } if let Some(parent_sig) = methods.get(&method_key) { validate_signature_compatibility( method.span, @@ -284,22 +318,54 @@ pub(crate) fn build_interface_info_recursive( } let mut iface_constants: HashMap = HashMap::new(); + let mut constant_declaring_interfaces = HashMap::new(); + let mut final_constants = HashSet::new(); for parent_name in &interface.extends { if let Some(parent_info) = checker.interfaces.get(parent_name) { for (k, v) in &parent_info.constants { - iface_constants - .entry(k.clone()) - .or_insert_with(|| v.clone()); + if !iface_constants.contains_key(k) { + iface_constants.insert(k.clone(), v.clone()); + constant_declaring_interfaces.insert( + k.clone(), + parent_info + .constant_declaring_interfaces + .get(k) + .cloned() + .unwrap_or_else(|| parent_name.clone()), + ); + } } + final_constants.extend(parent_info.final_constants.iter().cloned()); } } for c in &interface.constants { + for parent_name in &interface.extends { + let Some(parent_info) = checker.interfaces.get(parent_name) else { + continue; + }; + if parent_info.final_constants.contains(&c.name) { + return Err(CompileError::new( + c.span, + &format!( + "{}::{} cannot override final interface constant", + interface.name, c.name + ), + )); + } + } iface_constants.insert(c.name.clone(), c.value.clone()); + constant_declaring_interfaces.insert(c.name.clone(), interface.name.clone()); + if c.is_final { + final_constants.insert(c.name.clone()); + } else { + final_constants.remove(&c.name); + } } checker.interfaces.insert( interface.name.clone(), InterfaceInfo { interface_id: *next_interface_id, + declaration_span: interface.span, parents: interface.extends.clone(), properties, property_order, @@ -308,8 +374,11 @@ pub(crate) fn build_interface_info_recursive( method_order, method_slots, static_methods, + static_method_declaring_interfaces, static_method_order, constants: iface_constants, + constant_declaring_interfaces, + final_constants, }, ); *next_interface_id += 1; @@ -317,6 +386,21 @@ pub(crate) fn build_interface_info_recursive( Ok(()) } +/// Constructs a diagnostic for conflicting static/non-static interface methods. +fn interface_method_kind_conflict( + span: crate::span::Span, + interface_name: &str, + method_name: &str, +) -> CompileError { + CompileError::new( + span, + &format!( + "Cannot combine static and non-static interface method: {}::{}", + interface_name, method_name + ), + ) +} + /// Validates a single interface property declaration for syntactic correctness. /// Checks that the property is public, non-static, non-readonly, and has at least one hook. /// diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index bb459cd709..d3ad647eea 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -57,7 +57,7 @@ pub(crate) fn build_method_sig( }) .collect::, CompileError>>()?; let defaults: Vec> = method.params.iter().map(|(_, _, d, _)| d.clone()).collect(); - let ref_params: Vec = method.params.iter().map(|(_, _, _, r)| *r).collect(); + let mut ref_params: Vec = method.params.iter().map(|(_, _, _, r)| *r).collect(); for ((param_name, type_ann, default, _), (_, resolved_ty)) in method.params.iter().zip(params.iter()) { @@ -78,8 +78,18 @@ pub(crate) fn build_method_sig( )?, None => super::super::infer_return_type_syntactic(&method.body), }; + if method.variadic.is_some() { + ref_params.push(method.variadic_by_ref); + } let mut sig = Checker::callable_wrapper_sig(&FunctionSig { params, + param_type_exprs: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) + .collect(), + param_attributes: method.param_attributes.clone(), defaults, return_type, declared_return: method.return_type.is_some(), @@ -89,7 +99,12 @@ pub(crate) fn build_method_sig( .params .iter() .map(|(_, type_ann, _, _)| type_ann.is_some()) - .chain(method.variadic.iter().map(|_| method.variadic_type.is_some())) + .chain( + method + .variadic + .iter() + .map(|_| method.variadic_type.is_some()), + ) .collect(), variadic: method.variadic.clone(), deprecation: extract_deprecation(&method.attributes), @@ -97,17 +112,19 @@ pub(crate) fn build_method_sig( // A declared element type on the variadic (`int ...$xs`) constrains every collected argument. // `callable_wrapper_sig` defaults the variadic container to `array`; refine it to the // declared element type so call validation enforces it. - if let Some(variadic_type) = &method.variadic_type { - let elem_ty = checker.resolve_declared_param_type_hint( - variadic_type, - method.span, - &format!( - "Method variadic parameter ${}", - method.variadic.as_deref().unwrap_or_default() - ), - )?; - if let Some((_, ty)) = sig.params.last_mut() { - *ty = PhpType::Array(Box::new(elem_ty)); + if !method.variadic_by_ref { + if let Some(variadic_type) = &method.variadic_type { + let elem_ty = checker.resolve_declared_param_type_hint( + variadic_type, + method.span, + &format!( + "Method variadic parameter ${}", + method.variadic.as_deref().unwrap_or_default() + ), + )?; + if let Some((_, ty)) = sig.params.last_mut() { + *ty = PhpType::Array(Box::new(elem_ty)); + } } } Ok(sig) @@ -117,9 +134,7 @@ pub(crate) fn build_method_sig( /// marker, with `reason` set to the attribute's first string argument (or an /// empty string if absent). Match is case-insensitive on the last segment of /// the attribute name. -pub(crate) fn extract_deprecation( - groups: &[crate::parser::ast::AttributeGroup], -) -> Option { +pub(crate) fn extract_deprecation(groups: &[crate::parser::ast::AttributeGroup]) -> Option { for group in groups { for attr in &group.attributes { if !matches_global_builtin_attribute(attr, "Deprecated") { @@ -325,7 +340,11 @@ pub(crate) fn validate_override_signature( )); } if parent_sig.declared_return - && !declared_return_type_compatible(checker, &parent_sig.return_type, &child_sig.return_type) + && !declared_return_type_compatible( + checker, + &parent_sig.return_type, + &child_sig.return_type, + ) { return Err(CompileError::new( method.span, diff --git a/src/types/checker/stmt_check/assignments/locals.rs b/src/types/checker/stmt_check/assignments/locals.rs index 173e1ef988..d97c40e368 100644 --- a/src/types/checker/stmt_check/assignments/locals.rs +++ b/src/types/checker/stmt_check/assignments/locals.rs @@ -147,6 +147,7 @@ pub(super) fn check_assign( } else { value }; + let reflection_class_target = reflection_class_assignment_target(checker, callable_source, env); let ty_result: Result = (|| { if let Some(default) = null_coalesce_default { if let Some(existing) = env.get(name).cloned() { @@ -181,6 +182,7 @@ pub(super) fn check_assign( } let ty = ty_result?; metadata_result?; + update_reflection_class_assignment_metadata(checker, name, reflection_class_target); merge_local_assignment_type(checker, name, &ty, span, env) } @@ -277,6 +279,7 @@ fn check_ref_assign_variable( } else { clear_callable_metadata(checker, target); } + copy_reflection_class_metadata(checker, target, source); Ok(()) } @@ -457,6 +460,82 @@ pub(super) fn update_callable_assignment_metadata( Ok(()) } +/// Updates static `ReflectionClass` metadata for one assigned local. +fn update_reflection_class_assignment_metadata( + checker: &mut Checker, + name: &str, + reflected_class: Option, +) { + if let Some(reflected_class) = reflected_class { + checker + .reflection_class_targets + .insert(name.to_string(), reflected_class); + } else { + checker.reflection_class_targets.remove(name); + } +} + +/// Mirrors static `ReflectionClass` metadata across a reference alias assignment. +fn copy_reflection_class_metadata(checker: &mut Checker, target: &str, source: &str) { + if let Some(reflected_class) = checker.reflection_class_targets.get(source).cloned() { + checker + .reflection_class_targets + .insert(target.to_string(), reflected_class); + } else { + checker.reflection_class_targets.remove(target); + } +} + +/// Resolves the statically-known class reflected by a `new ReflectionClass(...)` expression. +fn reflection_class_assignment_target( + checker: &Checker, + source: &Expr, + env: &TypeEnv, +) -> Option { + let ExprKind::NewObject { class_name, args } = &source.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionclass" { + return None; + } + let class_arg = reflection_class_constructor_arg(args)?; + match &class_arg.kind { + ExprKind::StringLiteral(class_name) => { + resolve_class_name(checker, class_name).map(str::to_string) + } + ExprKind::ClassConstant { receiver } => { + resolve_static_receiver_class(checker, receiver, class_arg.span).ok() + } + _ => match env + .get(variable_name(class_arg).unwrap_or_default()) + .cloned() + .unwrap_or_else(|| crate::types::checker::infer_expr_type_syntactic(class_arg)) + .codegen_repr() + { + PhpType::Object(class_name) if !class_name.is_empty() => Some(class_name), + _ => None, + }, + } +} + +/// Returns the ReflectionClass constructor class argument after simple named-argument matching. +fn reflection_class_constructor_arg(args: &[Expr]) -> Option<&Expr> { + if let Some(positional) = args.iter().find(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. })) { + return Some(positional); + } + args.iter().find_map(|arg| { + let ExprKind::NamedArg { name, value } = &arg.kind else { + return None; + }; + match php_symbol_key(name).as_str() { + "class" | "classname" | "class_name" | "objectorclass" | "object_or_class" => { + Some(value.as_ref()) + } + _ => None, + } + }) +} + /// Returns true when a local assignment stores an array whose elements are callable descriptors. fn is_callable_array_type(ty: &PhpType) -> bool { match ty { @@ -698,6 +777,8 @@ pub(super) fn check_typed_assign( )); } env.insert(name.to_string(), declared_ty); + let reflected_class = reflection_class_assignment_target(checker, value, env); + update_reflection_class_assignment_metadata(checker, name, reflected_class); Ok(()) } @@ -736,6 +817,7 @@ pub(super) fn check_list_unpack( let unpack_ty = *elem_ty.clone(); env.insert(var.clone(), unpack_ty.clone()); update_list_unpack_callable_metadata(checker, var, value, &unpack_ty); + checker.reflection_class_targets.remove(var); } } _ => { diff --git a/src/types/checker/stmt_check/assignments/properties.rs b/src/types/checker/stmt_check/assignments/properties.rs index 150638c964..eb252b6b9c 100644 --- a/src/types/checker/stmt_check/assignments/properties.rs +++ b/src/types/checker/stmt_check/assignments/properties.rs @@ -210,7 +210,7 @@ fn check_object_property_write( return Ok(()); } if let Some(class_info) = checker.classes.get(class_name) { - if !class_info.properties.iter().any(|(n, _)| n == property) { + if class_info.visible_property(property).is_none() { if class_info.methods.contains_key("__set") { return Ok(()); } @@ -227,10 +227,8 @@ fn check_object_property_write( } validate_object_property_access(checker, class_name, property, true, span)?; let expected_ty = class_info - .properties - .iter() - .find(|(n, _)| n == property) - .map(|(_, ty)| ty.clone()) + .visible_property(property) + .map(|(_, (_, ty))| ty.clone()) .unwrap_or(PhpType::Int); let readonly_non_null_coalesce_keep = null_coalesce_property_keeps_non_null(object, property, value, &expected_ty); @@ -284,7 +282,7 @@ fn check_object_property_write( ), )); } - if class_info.declared_properties.contains(property) { + if class_info.visible_property_is_declared(property) { checker.require_compatible_arg_type( &expected_ty, val_ty, @@ -361,12 +359,9 @@ fn refine_object_property_type( val_ty: &PhpType, ) { if let Some(class_info) = checker.classes.get_mut(class_name) { - let property_has_declared_type = class_info.declared_properties.contains(property); - if let Some(prop) = class_info - .properties - .iter_mut() - .find(|(n, _)| n == property) - { + let property_has_declared_type = class_info.visible_property_is_declared(property); + if let Some(slot) = class_info.visible_property_index(property) { + let prop = &mut class_info.properties[slot]; if !property_has_declared_type { if matches!(prop.1, PhpType::Int | PhpType::Void) && prop.1 != *val_ty { prop.1 = val_ty.clone(); @@ -451,7 +446,7 @@ fn resolve_object_array_property( .classes .get(class_name) .ok_or_else(|| CompileError::new(span, &format!("Undefined class: {}", class_name)))?; - if !class_info.properties.iter().any(|(n, _)| n == property) { + if class_info.visible_property(property).is_none() { return Err(CompileError::new( span, &format!("Undefined property: {}::{}", class_name, property), @@ -460,12 +455,10 @@ fn resolve_object_array_property( // Indirect array modification (`$obj->prop[] = x` / `$obj->prop[$k] = x`) is a write, so it // must honor PHP 8.4 asymmetric `set` visibility — not the read visibility. validate_object_property_access(checker, class_name, property, true, span)?; - let property_has_declared_type = class_info.declared_properties.contains(property); + let property_has_declared_type = class_info.visible_property_is_declared(property); let prop_ty = class_info - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| ty.clone()) + .visible_property(property) + .map(|(_, (_, ty))| ty.clone()) .unwrap_or(PhpType::Int); Ok((prop_ty, property_has_declared_type)) } diff --git a/src/types/checker/stmt_check/assignments/static_properties.rs b/src/types/checker/stmt_check/assignments/static_properties.rs index 0ea6566129..5928d2002e 100644 --- a/src/types/checker/stmt_check/assignments/static_properties.rs +++ b/src/types/checker/stmt_check/assignments/static_properties.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::parser::ast::{Expr, StaticReceiver}; use crate::span::Span; -use crate::types::{PhpType, TypeEnv}; +use crate::types::{normalized_array_key_type, PhpType, TypeEnv}; use super::super::super::Checker; @@ -22,6 +22,7 @@ struct StaticPropertyAssignmentTarget { declaring_class: String, property_has_declared_type: bool, prop_ty: PhpType, + dynamic_eval_target: bool, } /// Type-checks a direct static property assignment `Class::$prop = value`. @@ -75,6 +76,9 @@ pub(super) fn check_static_property_array_push( ) -> Result<(), CompileError> { let val_ty = checker.infer_type_with_assignment_effects(value, env)?; let target = resolve_static_property_assignment_target(checker, receiver, property, span)?; + if target.dynamic_eval_target { + return Ok(()); + } let updated_prop_ty = match target.prop_ty { PhpType::Array(elem_ty) => { if target.property_has_declared_type { @@ -139,6 +143,13 @@ pub(super) fn check_static_property_array_assign( let idx_ty = checker.infer_type_with_assignment_effects(index, env)?; let val_ty = checker.infer_type_with_assignment_effects(value, env)?; let target = resolve_static_property_assignment_target(checker, receiver, property, span)?; + if target.dynamic_eval_target { + let normalized_idx_ty = normalized_array_key_type(index, idx_ty); + if !is_dynamic_eval_static_property_array_key_type(&normalized_idx_ty) { + return Err(CompileError::new(span, "Array index must be integer")); + } + return Ok(()); + } if let PhpType::Object(class_name) = &target.prop_ty { if checker.object_type_implements_interface(class_name, "ArrayAccess") { return Ok(()); @@ -231,6 +242,19 @@ fn resolve_static_property_assignment_target( } }; + if checker.eval_barrier_active + && matches!(receiver, StaticReceiver::Named(_)) + && !checker.classes.contains_key(&class_name) + { + return Ok(StaticPropertyAssignmentTarget { + class_name: class_name.clone(), + declaring_class: class_name, + property_has_declared_type: false, + prop_ty: PhpType::Mixed, + dynamic_eval_target: true, + }); + } + let class_info = checker .classes .get(&class_name) @@ -281,9 +305,15 @@ fn resolve_static_property_assignment_target( declaring_class, property_has_declared_type, prop_ty, + dynamic_eval_target: false, }) } +/// Returns true when a dynamic eval static-property key can be handled by runtime array writes. +fn is_dynamic_eval_static_property_array_key_type(ty: &PhpType) -> bool { + matches!(ty, PhpType::Int | PhpType::Str | PhpType::Mixed) +} + /// Updates the type of a static property in all classes that declare it. /// /// Iterates over all classes and mutates the type entry for `property` on the diff --git a/src/types/checker/stmt_check/narrowing.rs b/src/types/checker/stmt_check/narrowing.rs index 8d33408da1..b457236c88 100644 --- a/src/types/checker/stmt_check/narrowing.rs +++ b/src/types/checker/stmt_check/narrowing.rs @@ -233,7 +233,7 @@ fn guard_receiver_and_type(cond: &Expr) -> Option<(&Expr, PhpType)> { ExprKind::FunctionCall { name, args } if args.len() == 1 => { let target = match name.as_str().to_ascii_lowercase().as_str() { "is_int" | "is_integer" | "is_long" => PhpType::Int, - "is_float" | "is_double" => PhpType::Float, + "is_float" | "is_double" | "is_real" => PhpType::Float, "is_string" => PhpType::Str, "is_bool" => PhpType::Bool, // `is_null($x)`: same narrowing as `$x === null` — elephc models a `?T` value's diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index ec7b0415ef..963ef1cd1b 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -211,8 +211,8 @@ impl Checker { } /// Builds the initial parameter type list for a function declaration, resolving type hints, - /// validating defaults, and inferring types for untyped parameters. Adds variadic parameter - /// type as `PhpType::Array(Int)` if the function is variadic. + /// validating defaults, and inferring types for untyped parameters. Adds a variadic parameter + /// array type, using the declared element type for typed variadics. pub(crate) fn initial_function_param_types( &self, name: &str, @@ -240,10 +240,18 @@ impl Checker { } } if let Some(variadic_name) = decl.variadic.as_ref() { - param_types.push(( - variadic_name.clone(), - PhpType::Array(Box::new(PhpType::Int)), - )); + let elem_ty = if decl.variadic_by_ref { + PhpType::Mixed + } else if let Some(type_ann) = decl.variadic_type.as_ref() { + self.resolve_declared_param_type_hint( + type_ann, + decl.span, + &format!("Function '{}' variadic parameter ${}", name, variadic_name), + )? + } else { + PhpType::Int + }; + param_types.push((variadic_name.clone(), PhpType::Array(Box::new(elem_ty)))); } Ok(param_types) } @@ -307,6 +315,7 @@ impl Checker { let saved_globals = self.active_globals.clone(); let saved_statics = self.active_statics.clone(); let saved_foreach_keys = self.foreach_key_locals.clone(); + let saved_eval_barrier_active = self.eval_barrier_active; let saved_break_continue_depth = self.break_continue_depth; let saved_finally_break_continue_bases = self.finally_break_continue_bases.clone(); @@ -314,6 +323,7 @@ impl Checker { self.active_globals.clear(); self.active_statics.clear(); self.foreach_key_locals.clear(); + self.eval_barrier_active = false; self.break_continue_depth = 0; self.finally_break_continue_bases.clear(); @@ -323,6 +333,7 @@ impl Checker { self.active_globals = saved_globals; self.active_statics = saved_statics; self.foreach_key_locals = saved_foreach_keys; + self.eval_barrier_active = saved_eval_barrier_active; self.break_continue_depth = saved_break_continue_depth; self.finally_break_continue_bases = saved_finally_break_continue_bases; diff --git a/src/types/checker/type_compat/object_types.rs b/src/types/checker/type_compat/object_types.rs index 0226b6656e..696161a75e 100644 --- a/src/types/checker/type_compat/object_types.rs +++ b/src/types/checker/type_compat/object_types.rs @@ -374,14 +374,12 @@ impl Checker { continue; } - let property_has_declared_type = class_info.declared_properties.contains(&prop_name); + let property_has_declared_type = class_info.visible_property_is_declared(&prop_name); if !property_has_declared_type { - if let Some(prop) = class_info - .properties - .iter_mut() - .find(|(name, _)| name == &prop_name) - { - prop.1 = arg_ty.clone(); + if let Some(slot) = class_info.visible_property_index(&prop_name) { + if let Some(prop) = class_info.properties.get_mut(slot) { + prop.1 = arg_ty.clone(); + } } } diff --git a/src/types/checker/type_compat/pointers.rs b/src/types/checker/type_compat/pointers.rs index be6af3547c..afdd815fdb 100644 --- a/src/types/checker/type_compat/pointers.rs +++ b/src/types/checker/type_compat/pointers.rs @@ -117,6 +117,7 @@ impl Checker { "string" => Ok(PhpType::Str), "mixed" => Ok(PhpType::Mixed), "callable" => Ok(PhpType::Callable), + "object" => Ok(PhpType::Object(String::new())), "void" => Ok(PhpType::Void), "array" => Ok(PhpType::Array(Box::new(PhpType::Mixed))), // Relative class types only survive to this point when used outside a class diff --git a/src/types/checker/type_compat/unions.rs b/src/types/checker/type_compat/unions.rs index f7f0c66c62..5261a6cabc 100644 --- a/src/types/checker/type_compat/unions.rs +++ b/src/types/checker/type_compat/unions.rs @@ -105,7 +105,8 @@ impl Checker { }, PhpType::Object(expected_name) => match actual { PhpType::Object(actual_name) => { - expected_name == actual_name + expected_name.is_empty() + || expected_name == actual_name || self.is_subclass_of(actual_name, expected_name) || self.class_implements_interface(actual_name, expected_name) || self.interface_extends_interface(actual_name, expected_name) diff --git a/src/types/checker/yield_validation/detect.rs b/src/types/checker/yield_validation/detect.rs index 5ce8923bb0..52cca7c211 100644 --- a/src/types/checker/yield_validation/detect.rs +++ b/src/types/checker/yield_validation/detect.rs @@ -182,6 +182,15 @@ fn expr_contains_yield(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_contains_yield(object) || args.iter().any(expr_contains_yield) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_yield(object) + || expr_contains_yield(method) + || args.iter().any(expr_contains_yield) + } ExprKind::ArrayLiteral(items) => items.iter().any(expr_contains_yield), ExprKind::ArrayLiteralAssoc(pairs) => pairs .iter() diff --git a/src/types/checker/yield_validation/validate.rs b/src/types/checker/yield_validation/validate.rs index d25a9f9e07..22e52e8d1a 100644 --- a/src/types/checker/yield_validation/validate.rs +++ b/src/types/checker/yield_validation/validate.rs @@ -284,6 +284,7 @@ fn visit_expr(expr: &Expr, st: &mut State) { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Spread(inner) | ExprKind::Cast { expr: inner, .. } @@ -332,6 +333,17 @@ fn visit_expr(expr: &Expr, st: &mut State) { visit_expr(a, st); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + visit_expr(object, st); + visit_expr(method, st); + for a in args { + visit_expr(a, st); + } + } ExprKind::ArrayLiteral(items) => { for it in items { visit_expr(it, st); diff --git a/src/types/mod.rs b/src/types/mod.rs index 2d34cda20e..fbb5baca80 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -55,6 +55,7 @@ pub use schema::{ ExternClassInfo, ExternFieldInfo, ExternFunctionSig, InterfaceInfo, PackedClassInfo, PackedFieldInfo, PropertyHookContract, }; +pub(crate) use schema::{collect_attribute_args, collect_attribute_names}; pub(crate) use signatures::{ builtin_call_sig, callable_wrapper_sig, first_class_callable_builtin_sig, }; diff --git a/src/types/result.rs b/src/types/result.rs index 97f548bfa6..71dae9c32c 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -17,8 +17,8 @@ use crate::parser::ast::Program; use crate::span::Span; use super::{ - checker, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, - PackedClassInfo, PhpType, TypeEnv, + checker, AttrArgEntry, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, + InterfaceInfo, PackedClassInfo, PhpType, TypeEnv, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -59,6 +59,8 @@ pub enum ThrowAccessKind { pub struct CheckResult { pub global_env: TypeEnv, pub functions: HashMap, + pub function_attribute_names: HashMap>, + pub function_attribute_args: HashMap>>>, pub callable_param_sigs: HashMap<(String, String), FunctionSig>, #[allow(dead_code)] pub callable_return_sigs: HashMap, @@ -121,4 +123,37 @@ mod tests { .expect("mac type check failed"); assert_eq!(mac.required_libraries, vec!["elephc_crypto"]); } + + /// Verifies enum class metadata preserves flattened trait relation data for runtime reflection. + #[test] + fn test_enum_class_info_preserves_trait_metadata() { + let program = parse_program( + r#" Vec { + let mut out = Vec::new(); + for group in groups { + for attr in &group.attributes { + out.push(attr.name.as_str().to_string()); + } + } + out +} + +/// Collects materializable positional, named, and array attribute arguments in source order. +/// +/// Legal PHP attribute expressions outside the current literal subset are +/// represented as `None` so compilation can proceed until a reflection query +/// needs the missing payload and reports the unsupported metadata. +pub(crate) fn collect_attribute_args( + groups: &[AttributeGroup], +) -> Vec>> { + let mut out = Vec::new(); + for group in groups { + for attr in &group.attributes { + let mut entries = Vec::new(); + let mut supported = true; + for arg_expr in &attr.args { + let (key, value_expr) = match &arg_expr.kind { + ExprKind::NamedArg { name, value } => { + (Some(AttrKey::Str(name.clone())), value.as_ref()) + } + _ => (None, arg_expr), + }; + match fold_attr_value(value_expr) { + Some(value) => entries.push(AttrArgEntry { key, value }), + None => { + supported = false; + break; + } + } + } + out.push(if supported { Some(entries) } else { None }); + } + } + out +} + +/// Folds one attribute argument expression to retained reflection metadata. +fn fold_attr_value(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::StringLiteral(value) => Some(AttrArgValue::Str(value.clone())), + ExprKind::IntLiteral(value) => Some(AttrArgValue::Int(*value)), + ExprKind::FloatLiteral(value) => Some(AttrArgValue::Float(value.to_bits())), + ExprKind::BoolLiteral(value) => Some(AttrArgValue::Bool(*value)), + ExprKind::Null => Some(AttrArgValue::Null), + ExprKind::ConstRef(name) => Some(AttrArgValue::ConstRef(name.as_str().to_string())), + ExprKind::ScopedConstantAccess { receiver, name } => scoped_receiver_type_name(receiver) + .map(|type_name| AttrArgValue::ScopedConst(type_name, name.clone())), + ExprKind::ClassConstant { + receiver: StaticReceiver::Named(name), + } => Some(AttrArgValue::Str(name.as_str().to_string())), + ExprKind::ClassConstant { .. } => None, + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(n) => Some(AttrArgValue::Int(n.wrapping_neg())), + ExprKind::FloatLiteral(n) => Some(AttrArgValue::Float((-*n).to_bits())), + _ => None, + }, + ExprKind::ArrayLiteral(elements) => { + let mut entries = Vec::with_capacity(elements.len()); + for element in elements { + entries.push(AttrArgEntry { + key: None, + value: fold_attr_value(element)?, + }); + } + Some(AttrArgValue::Array(entries)) + } + ExprKind::ArrayLiteralAssoc(pairs) => { + let mut entries = Vec::with_capacity(pairs.len()); + for (key_expr, value_expr) in pairs { + entries.push(AttrArgEntry { + key: Some(fold_attr_key(key_expr)?), + value: fold_attr_value(value_expr)?, + }); + } + Some(AttrArgValue::Array(entries)) + } + _ => None, + } +} + +/// Folds one supported associative attribute array key. +fn fold_attr_key(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(AttrKey::Int(*value)), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(n) => Some(AttrKey::Int(n.wrapping_neg())), + _ => None, + }, + ExprKind::StringLiteral(value) => Some(AttrKey::Str(value.clone())), + _ => None, + } +} + +/// Returns the canonical named receiver for class-constant attribute arguments. +fn scoped_receiver_type_name(receiver: &StaticReceiver) -> Option { + match receiver { + StaticReceiver::Named(name) => Some(name.as_str().to_string()), + StaticReceiver::Self_ | StaticReceiver::Static | StaticReceiver::Parent => None, + } +} + /// Property hook contract for `get`/`set` hook declarations in classes and interfaces. #[derive(Debug, Clone)] pub struct PropertyHookContract { @@ -89,24 +204,37 @@ impl PartialEq for PropertyHookContract { } /// Interface metadata for resolved declarations. Tracks parents, properties, -/// methods, constants, and vtable layout after name resolution and inheritance flattening. +/// instance/static methods, constants, and instance vtable layout after name +/// resolution and inheritance flattening. #[derive(Debug, Clone)] pub struct InterfaceInfo { pub interface_id: u64, + /// Source span of the interface declaration, or `Span::dummy()` for compiler-injected interfaces. + pub declaration_span: crate::span::Span, pub parents: Vec, pub properties: HashMap, pub property_order: Vec, + /// Instance method contracts, keyed by PHP's case-insensitive method key. + /// + /// These entries are the only methods that participate in interface + /// dispatch tables and `method_slots`. pub methods: HashMap, pub method_declaring_interfaces: HashMap, pub method_order: Vec, pub method_slots: HashMap, - /// Static interface methods (PHP 8.3+), keyed by method key. Recorded separately from - /// instance `methods` — static dispatch is by class, so they take no vtable slot. + /// Static method contracts, keyed by PHP's case-insensitive method key. + /// + /// PHP requires implementors to provide matching public static methods, but + /// these entries never participate in instance interface dispatch tables. pub static_methods: HashMap, - /// Declaration order of `static_methods`, for deterministic conformance checking. + pub static_method_declaring_interfaces: HashMap, pub static_method_order: Vec, /// Interface constants (PHP 5.0+). Inherited from parent interfaces. pub constants: HashMap, + /// Declaring interface for each visible constant, keyed by case-sensitive constant name. + pub constant_declaring_interfaces: HashMap, + /// Interface constants declared with PHP 8.1+ `final`, including inherited parents. + pub final_constants: HashSet, } /// Class metadata for resolved declarations. Tracks inheritance, properties, @@ -114,6 +242,8 @@ pub struct InterfaceInfo { #[derive(Debug, Clone, PartialEq)] pub struct ClassInfo { pub class_id: u64, + /// Source span of the class-like declaration, or `Span::dummy()` for compiler-injected classes. + pub declaration_span: crate::span::Span, pub parent: Option, pub is_abstract: bool, pub is_final: bool, @@ -126,6 +256,10 @@ pub struct ClassInfo { /// User-declared class constants (PHP 7.1+). Maps the constant name to /// its value expression — codegen inlines the literal at access time. pub constants: HashMap, + /// Class constant visibilities keyed by case-sensitive constant name. + pub constant_visibilities: HashMap, + /// Class constants declared with PHP 8.1+ `final`, keyed by constant name. + pub final_constants: HashSet, /// Names of PHP 8 attributes attached to this class declaration, in /// source order. Name resolution stores canonical class-like text without /// a synthetic leading backslash, matching `ReflectionAttribute::getName()`. @@ -148,8 +282,15 @@ pub struct ClassInfo { pub property_attribute_names: HashMap>, /// Literal property-attribute args aligned with `property_attribute_names`. pub property_attribute_args: HashMap>>>, + /// Attribute names attached to class constants visible on this class. + /// Constant names are case-sensitive, so the source constant name is the key. + pub constant_attribute_names: HashMap>, + /// Literal class-constant-attribute args aligned with `constant_attribute_names`. + pub constant_attribute_args: HashMap>>>, /// Trait names used directly by this class declaration, preserving source order. pub used_traits: Vec, + /// Trait method aliases declared directly by this class, as `(alias, Trait::method)`. + pub trait_aliases: Vec<(String, String)>, pub properties: Vec<(String, PhpType)>, pub property_offsets: HashMap, pub property_declaring_classes: HashMap, @@ -160,6 +301,13 @@ pub struct ClassInfo { /// use their `property_visibilities` entry for writes too. pub property_set_visibilities: HashMap, pub declared_properties: HashSet, + /// Per-layout-slot typed-declaration flags for instance properties. + /// + /// The name-keyed `declared_properties` map describes the property currently + /// visible by name in this class. This vector follows `properties` by index + /// so hidden private parent slots keep their typed-property initialization + /// metadata when a child declares a same-named property. + pub property_declared_slots: Vec, pub final_properties: HashSet, pub readonly_properties: HashSet, pub reference_properties: HashSet, @@ -170,6 +318,13 @@ pub struct ClassInfo { /// caller). The object allocates a cell per such property at construction and releases /// it on destruction. pub owned_reference_properties: HashSet, + pub promoted_properties: HashSet, + /// Per-layout-slot by-reference flags for instance properties. + /// + /// The name-keyed `reference_properties` map describes the currently + /// visible property by name. Runtime GC descriptors need the original slot + /// flag even when a private parent slot is shadowed by a child property. + pub property_reference_slots: Vec, pub abstract_properties: HashSet, pub abstract_property_hooks: HashMap, pub static_properties: Vec<(String, PhpType)>, @@ -204,6 +359,64 @@ pub struct ClassInfo { pub constructor_param_to_prop: Vec>, } +impl ClassInfo { + /// Resolves the layout index of the property visible by name on this class. + /// + /// The result follows `property_offsets` when present so private parent + /// slots shadowed by child declarations do not win merely because they occur + /// earlier in the physical object layout. + pub fn visible_property_index(&self, property: &str) -> Option { + self.property_offsets + .get(property) + .and_then(|offset| property_index_from_offset(*offset, self.properties.len())) + .or_else(|| { + self.properties + .iter() + .rposition(|(name, _)| name == property) + }) + } + + /// Returns the property tuple visible by name on this class. + pub fn visible_property(&self, property: &str) -> Option<(usize, &(String, PhpType))> { + let index = self.visible_property_index(property)?; + self.properties.get(index).map(|entry| (index, entry)) + } + + /// Returns whether one physical property slot has a declared PHP type. + pub fn property_slot_is_declared(&self, index: usize, property: &str) -> bool { + self.property_declared_slots + .get(index) + .copied() + .unwrap_or_else(|| self.declared_properties.contains(property)) + } + + /// Returns whether the property visible by name has a declared PHP type. + pub fn visible_property_is_declared(&self, property: &str) -> bool { + self.visible_property(property) + .is_some_and(|(index, (name, _))| self.property_slot_is_declared(index, name)) + } + + /// Returns whether one physical property slot stores a by-reference cell. + pub fn property_slot_is_reference(&self, index: usize, property: &str) -> bool { + self.property_reference_slots + .get(index) + .copied() + .unwrap_or_else(|| self.reference_properties.contains(property)) + } + +} + +/// Converts a property offset into a `properties` vector index when it points +/// at a normal object-property slot. +fn property_index_from_offset(offset: usize, property_count: usize) -> Option { + let payload_offset = offset.checked_sub(8)?; + if payload_offset % 16 != 0 { + return None; + } + let index = payload_offset / 16; + (index < property_count).then_some(index) +} + /// Enum case value, either an integer or a string (PHP 8.1+ backed enums). #[derive(Debug, Clone, PartialEq)] pub enum EnumCaseValue { @@ -217,6 +430,8 @@ pub enum EnumCaseValue { pub struct EnumCaseInfo { pub name: String, pub value: Option, + pub attribute_names: Vec, + pub attribute_args: Vec>>, } /// Enum metadata for a resolved backed enum declaration (PHP 8.1+). diff --git a/src/types/signatures.rs b/src/types/signatures.rs index f9f93cfa62..2c75d1ed4c 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -9,7 +9,7 @@ //! Key details: //! - Builtin signatures must match PHP so named arguments, first-class callables, and mutation semantics stay coherent. -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::{AttributeGroup, Expr, ExprKind, TypeExpr}; use crate::span::Span; use super::PhpType; @@ -22,6 +22,8 @@ use super::PhpType; /// named arguments, callable aliases, and mutation semantics. pub struct FunctionSig { pub params: Vec<(String, PhpType)>, + pub param_type_exprs: Vec>, + pub param_attributes: Vec>, pub defaults: Vec>, pub return_type: PhpType, pub declared_return: bool, @@ -59,13 +61,37 @@ pub(crate) fn callable_wrapper_sig(sig: &FunctionSig) -> FunctionSig { } } + let variadic_index = wrapper_sig.params.len(); + let variadic_type_expr = if wrapper_sig.param_type_exprs.len() > variadic_index { + wrapper_sig.param_type_exprs.remove(variadic_index) + } else { + None + }; + let variadic_attributes = if wrapper_sig.param_attributes.len() > variadic_index { + wrapper_sig.param_attributes.remove(variadic_index) + } else { + Vec::new() + }; + let variadic_ref = if wrapper_sig.ref_params.len() > variadic_index { + wrapper_sig.ref_params.remove(variadic_index) + } else { + false + }; + let variadic_declared = if wrapper_sig.declared_params.len() > variadic_index { + wrapper_sig.declared_params.remove(variadic_index) + } else { + false + }; + wrapper_sig.params.push(( variadic_name.clone(), PhpType::Array(Box::new(PhpType::Mixed)), )); wrapper_sig.defaults.push(None); - wrapper_sig.ref_params.push(false); - wrapper_sig.declared_params.push(false); + wrapper_sig.ref_params.push(variadic_ref); + wrapper_sig.declared_params.push(variadic_declared); + wrapper_sig.param_type_exprs.push(variadic_type_expr); + wrapper_sig.param_attributes.push(variadic_attributes); wrapper_sig } @@ -99,6 +125,8 @@ pub(crate) fn builtin_call_sig(name: &str) -> Option { /// longer needed. pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { match name { + "eval" => Some(fixed(&["code"])), + "time" | "phpversion" | "json_last_error" | "json_last_error_msg" | "pi" | "ptr_null" | "getcwd" | "sys_get_temp_dir" | "tmpfile" | "hash_algos" | "date_default_timezone_get" => Some(fixed(&[])), @@ -127,8 +155,9 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { Some(fixed(&["text"])) } - "intval" | "floatval" | "boolval" | "gettype" | "is_bool" | "is_null" - | "is_float" | "is_int" | "is_iterable" | "is_string" | "is_numeric" + "intval" | "floatval" | "boolval" | "strval" | "gettype" | "is_bool" + | "is_null" | "is_float" | "is_double" | "is_real" | "is_int" | "is_integer" + | "is_long" | "is_iterable" | "is_string" | "is_numeric" | "is_array" | "is_object" | "is_scalar" | "empty" => { Some(fixed(&["value"])) @@ -167,6 +196,14 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { 1, vec![bool_lit(true)], )), + "method_exists" => Some(with_return( + fixed(&["object_or_class", "method"]), + PhpType::Bool, + )), + "property_exists" => Some(with_return( + fixed(&["object_or_class", "property"]), + PhpType::Bool, + )), "iterator_to_array" => Some(optional( &["iterator", "preserve_keys"], 1, @@ -702,6 +739,8 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { match name { "strlen" => Some(FunctionSig { params: vec![("string".to_string(), PhpType::Str)], + param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -719,6 +758,8 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { value: Box::new(PhpType::Mixed), }, )], + param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -730,6 +771,8 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { }), "buffer_len" => Some(FunctionSig { params: vec![("buffer".to_string(), PhpType::Buffer(Box::new(PhpType::Int)))], + param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -773,14 +816,20 @@ fn general_first_class_callable_builtin_sig(name: &str) -> Option { &[PhpType::Mixed], PhpType::Float, )), + "strval" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Str, + )), // NOTE: is_array/is_object/is_scalar are intentionally NOT first-class callable. // No runtime callable wrapper is emitted for these three predicates, so listing // them here would emit an undefined `_fn_is_*` invoker reference in any program // using dynamic string callbacks. // Direct calls are fully supported; first-class/string-callback use is not (yet). - "boolval" | "is_bool" | "is_null" | "is_float" | "is_int" | "is_iterable" - | "is_string" | "is_numeric" | "is_nan" | "is_finite" | "is_infinite" - | "ctype_alpha" | "ctype_digit" | "ctype_alnum" | "ctype_space" => { + "boolval" | "is_bool" | "is_null" | "is_float" | "is_double" | "is_real" + | "is_int" | "is_integer" | "is_long" | "is_iterable" | "is_string" + | "is_numeric" | "is_nan" | "is_finite" | "is_infinite" | "ctype_alpha" + | "ctype_digit" | "ctype_alnum" | "ctype_space" => { Some(typed_first_class_builtin_sig(name, &[PhpType::Mixed], PhpType::Bool)) } "defined" => Some(typed_first_class_builtin_sig( @@ -788,6 +837,9 @@ fn general_first_class_callable_builtin_sig(name: &str) -> Option { &[PhpType::Str], PhpType::Bool, )), + "method_exists" | "property_exists" => { + return_typed_first_class_builtin_sig(name, PhpType::Bool) + } "gettype" => Some(typed_first_class_builtin_sig( name, &[PhpType::Mixed], @@ -1112,6 +1164,13 @@ fn variadic(regular_params: &[&str], variadic_name: &str) -> FunctionSig { make_sig(¶ms, defaults, Some(variadic_name)) } +/// Sets the declared return type for a signature while preserving its parameter contract. +fn with_return(mut sig: FunctionSig, return_type: PhpType) -> FunctionSig { + sig.return_type = return_type; + sig.declared_return = true; + sig +} + /// Marks the first parameter of a signature as by-reference. fn first_param_ref(mut sig: FunctionSig) -> FunctionSig { if let Some(first_ref) = sig.ref_params.first_mut() { @@ -1130,6 +1189,8 @@ fn make_sig(params: &[&str], defaults: Vec>, variadic: Option<&str> .iter() .map(|name| ((*name).to_string(), PhpType::Mixed)) .collect(), + param_type_exprs: vec![None; params.len()], + param_attributes: vec![Vec::new(); params.len()], defaults, return_type: PhpType::Mixed, declared_return: false, @@ -1169,6 +1230,8 @@ mod tests { fn variadic_sig(params: Vec<(String, PhpType)>) -> FunctionSig { FunctionSig { defaults: vec![None; params.len()], + param_type_exprs: vec![None; params.len()], + param_attributes: vec![Vec::new(); params.len()], return_type: PhpType::Mixed, declared_return: false, by_ref_return: false, diff --git a/src/types/traits.rs b/src/types/traits.rs index c9d9a5dd59..6cf479eef8 100644 --- a/src/types/traits.rs +++ b/src/types/traits.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, Program, StmtKind, TraitUse, + ClassConst, ClassMethod, ClassProperty, Program, StmtKind, TraitAdaptation, TraitUse, }; use crate::span::Span; @@ -27,6 +27,7 @@ mod validation; /// schema building, type checking, and codegen. pub struct FlattenedClass { pub name: String, + pub span: Span, pub extends: Option, pub implements: Vec, pub is_abstract: bool, @@ -37,6 +38,7 @@ pub struct FlattenedClass { pub attributes: Vec, pub constants: Vec, pub used_traits: Vec, + pub trait_aliases: Vec<(String, String)>, } #[derive(Clone)] @@ -66,16 +68,22 @@ struct ImportedMethod { decl: ClassMethod, } -/// Scans `program` for all traits and classes, validates direct member uniqueness, -/// expands trait uses for each class, merges imported vs. local members, and returns -/// a vector of `FlattenedClass` with any composition errors collected. +/// Scans `program` for all traits, classes, and enums, validates direct member uniqueness, +/// expands trait uses for each class-like declaration, and returns flattened metadata with +/// any composition errors collected. /// /// Trait declarations are stored in `trait_map` for later expansion. -/// Classes with traits are processed in program order; each class's trait uses are -/// resolved recursively, then merged with the class's own members. +/// Class-like declarations with traits are processed in program order; each declaration's trait +/// uses are resolved recursively, then merged with the declaration's own members. /// Circular trait composition and duplicate declarations are reported as errors. -/// Returns `([FlattenedClass], Vec)`. -pub fn flatten_classes(program: &Program) -> (Vec, Vec) { +/// Returns `(flattened_classes, flattened_enums, errors)`. +pub fn flatten_classes( + program: &Program, +) -> ( + Vec, + HashMap, + Vec, +) { let mut trait_map = HashMap::new(); let mut trait_keys = HashSet::new(); let mut class_like_keys = HashSet::new(); @@ -128,92 +136,224 @@ pub fn flatten_classes(program: &Program) -> (Vec, Vec result, - Err(error) => { - errors.extend(error.flatten()); - continue; - } - }; - let merged_props = match merge::merge_properties( - &imported_props, properties, - stmt.span, - &format!("class {}", name), - true, - ) { - Ok(props) => props, - Err(error) => { + methods, + constants, + } => { + if let Err(error) = + validation::validate_direct_members(properties, methods, stmt.span, name) + { errors.extend(error.flatten()); continue; } - }; - let merged_methods = match merge::merge_methods( - imported_methods, + let (imported_props, imported_methods) = match expand::resolve_trait_uses( + trait_uses, + &trait_map, + &mut cache, + &mut stack, + &format!("class {}", name), + stmt.span, + ) { + Ok(result) => result, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let merged_props = match merge::merge_properties( + &imported_props, + properties, + stmt.span, + &format!("class {}", name), + true, + ) { + Ok(props) => props, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let merged_methods = match merge::merge_methods( + imported_methods, + methods, + stmt.span, + &format!("class {}", name), + ) { + Ok(methods) => methods, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let (merged_props, merged_methods) = + crate::magic_constants::bind_trait_class_constants( + merged_props, + merged_methods, + name, + ); + flattened.push(FlattenedClass { + name: name.clone(), + span: stmt.span, + extends: extends.as_ref().map(|name| name.as_str().to_string()), + implements: implements.iter().map(|name| name.as_str().to_string()).collect(), + is_abstract: *is_abstract, + is_final: *is_final, + is_readonly_class: *is_readonly_class, + properties: merged_props, + methods: merged_methods, + attributes: stmt.attributes.clone(), + constants: constants.clone(), + used_traits: used_trait_names(trait_uses), + trait_aliases: used_trait_aliases(trait_uses, &trait_map), + }); + } + StmtKind::EnumDecl { + name, + implements, + trait_uses, methods, - stmt.span, - &format!("class {}", name), - ) { - Ok(methods) => methods, - Err(error) => { + constants, + .. + } => { + if let Err(error) = + validation::validate_direct_members(&[], methods, stmt.span, name) + { errors.extend(error.flatten()); continue; } - }; - let (merged_props, merged_methods) = - crate::magic_constants::bind_trait_class_constants( - merged_props, - merged_methods, - name, - ); - flattened.push(FlattenedClass { - name: name.clone(), - extends: extends.as_ref().map(|name| name.as_str().to_string()), - implements: implements.iter().map(|name| name.as_str().to_string()).collect(), - is_abstract: *is_abstract, - is_final: *is_final, - is_readonly_class: *is_readonly_class, - properties: merged_props, - methods: merged_methods, - attributes: stmt.attributes.clone(), - constants: constants.clone(), - used_traits: trait_uses - .iter() - .flat_map(|use_decl| { - use_decl - .trait_names + let (imported_props, imported_methods) = match expand::resolve_trait_uses( + trait_uses, + &trait_map, + &mut cache, + &mut stack, + &format!("enum {}", name), + stmt.span, + ) { + Ok(result) => result, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + if let Some(property) = imported_props.first() { + errors.push(CompileError::new( + property.span, + "Enums cannot use traits with properties", + )); + continue; + } + let merged_methods = match merge::merge_methods( + imported_methods, + methods, + stmt.span, + &format!("enum {}", name), + ) { + Ok(methods) => methods, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let (_merged_props, merged_methods) = + crate::magic_constants::bind_trait_class_constants( + Vec::new(), + merged_methods, + name, + ); + flattened_enums.insert( + name.clone(), + FlattenedClass { + name: name.clone(), + span: stmt.span, + extends: None, + implements: implements .iter() .map(|name| name.as_str().to_string()) - }) - .collect(), - }); + .collect(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: Vec::new(), + methods: merged_methods, + attributes: stmt.attributes.clone(), + constants: constants.clone(), + used_traits: used_trait_names(trait_uses), + trait_aliases: used_trait_aliases(trait_uses, &trait_map), + }, + ); + } + _ => {} } } - (flattened, errors) + (flattened, flattened_enums, errors) +} + +/// Returns the direct trait names from a group of trait-use declarations. +fn used_trait_names(trait_uses: &[TraitUse]) -> Vec { + trait_uses + .iter() + .flat_map(|use_decl| { + use_decl + .trait_names + .iter() + .map(|name| name.as_str().to_string()) + }) + .collect() +} + +/// Returns direct trait aliases in PHP's `alias => Trait::method` reflection format. +fn used_trait_aliases( + trait_uses: &[TraitUse], + trait_map: &HashMap, +) -> Vec<(String, String)> { + trait_uses + .iter() + .flat_map(|use_decl| { + use_decl.adaptations.iter().filter_map(|adaptation| { + let TraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + return None; + }; + let source_trait = trait_name + .as_ref() + .map(|name| name.as_str().to_string()) + .or_else(|| trait_alias_source_trait(use_decl, method, trait_map))?; + Some((alias.clone(), format!("{source_trait}::{method}"))) + }) + }) + .collect() +} + +/// Resolves the direct trait that supplies one unqualified alias adaptation target. +fn trait_alias_source_trait( + trait_use: &TraitUse, + method: &str, + trait_map: &HashMap, +) -> Option { + let method_key = php_symbol_key(method); + trait_use.trait_names.iter().find_map(|trait_name| { + let trait_info = trait_map.get(trait_name.as_str())?; + trait_info + .methods + .iter() + .any(|candidate| php_symbol_key(&candidate.name) == method_key) + .then(|| trait_name.as_str().to_string()) + }) } diff --git a/src/types/warnings/expr_reads.rs b/src/types/warnings/expr_reads.rs index d6def10c74..96b1ebe85d 100644 --- a/src/types/warnings/expr_reads.rs +++ b/src/types/warnings/expr_reads.rs @@ -44,6 +44,7 @@ pub(super) fn collect_expr_reads( | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -103,6 +104,17 @@ pub(super) fn collect_expr_reads( collect_expr_reads(arg, scope, warnings); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_expr_reads(object, scope, warnings); + collect_expr_reads(method, scope, warnings); + for arg in args { + collect_expr_reads(arg, scope, warnings); + } + } ExprKind::NewDynamic { name_expr, args } => { collect_expr_reads(name_expr, scope, warnings); for arg in args { diff --git a/src/tz_prelude/detect.rs b/src/tz_prelude/detect.rs index 082b4aa99d..4693863b4a 100644 --- a/src/tz_prelude/detect.rs +++ b/src/tz_prelude/detect.rs @@ -39,6 +39,16 @@ fn name_is_tz_fn(name: &Name) -> bool { }) } +/// Returns whether a function name is PHP's `eval` construct. +/// +/// Runtime eval can call the introspection aliases from a string that this +/// prelude detector cannot inspect statically, so an `eval(...)` call is treated +/// as a potential introspection use. +fn name_is_eval_fn(name: &Name) -> bool { + name.last_segment() + .is_some_and(|segment| segment.eq_ignore_ascii_case("eval")) +} + /// Returns whether a method name is one of the three introspection methods, /// compared case-insensitively as PHP method names are. fn method_is_tz(method: &str) -> bool { @@ -131,7 +141,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { | ExprKind::MagicConstant(_) => false, ExprKind::FunctionCall { name, args } => { - name_is_tz_fn(name) || args.iter().any(expr_refs_tz) + name_is_eval_fn(name) || name_is_tz_fn(name) || args.iter().any(expr_refs_tz) } ExprKind::MethodCall { object, @@ -143,6 +153,11 @@ fn expr_refs_tz(expr: &Expr) -> bool { method, args, } => method_is_tz(method) || expr_refs_tz(object) || args.iter().any(expr_refs_tz), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_tz(object) || expr_refs_tz(method) || args.iter().any(expr_refs_tz), ExprKind::StaticMethodCall { method, args, .. } => { method_is_tz(method) || args.iter().any(expr_refs_tz) } @@ -156,6 +171,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -455,6 +471,15 @@ mod tests { ))); } + /// A runtime `eval(...)` call is detected because the eval fragment can call + /// the introspection aliases dynamically. + #[test] + fn detects_eval_call() { + assert!(program_uses_tz_introspection(&parse( + r#" bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_ve(object) || args.iter().any(expr_refs_ve) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_ve(object) || expr_refs_ve(method) || args.iter().any(expr_refs_ve), ExprKind::StaticMethodCall { args, .. } => args.iter().any(expr_refs_ve), ExprKind::FirstClassCallable(target) => callable_target_refs_ve(target), @@ -158,6 +163,7 @@ fn expr_refs_ve(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs new file mode 100644 index 0000000000..e859e42c26 --- /dev/null +++ b/tests/builtin_parity_tests.rs @@ -0,0 +1,811 @@ +//! Purpose: +//! Integration tests for builtin catalog parity between static elephc and +//! elephc-magician's eval interpreter. +//! +//! Called from: +//! - `cargo test --test builtin_parity_tests` through Rust's test harness. +//! +//! Key details: +//! - Static builtin names and signatures are read from compiler metadata APIs. +//! - Eval builtin existence and signature shape are read from magician metadata APIs. + +use std::collections::BTreeSet; + +const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( + "../crates/elephc-magician/src/interpreter/expressions.rs" +)]; + +const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[include_str!( + "../crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs" +)]; + +/// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. +const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ + "get_called_class", + "get_class_methods", + "get_class_vars", + "get_object_vars", +]; + +/// Static-only registered builtins exist in the compiler before magician/eval has runtime support. +const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ + "array_all", + "array_any", + "array_diff_assoc", + "array_find", + "array_intersect_assoc", + "array_is_list", + "array_key_first", + "array_key_last", + "array_merge_recursive", + "array_multisort", + "array_replace", + "array_replace_recursive", + "array_udiff", + "array_uintersect", + "array_walk_recursive", + "serialize", + "unserialize", + "zval_free", + "zval_pack", + "zval_type", + "zval_unpack", +]; + +/// Eval supports these PHP optional parameters before the static backend does. +const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ + "array_reverse", + "array_splice", + "nl2br", + "preg_match", + "print_r", +]; + +/// Eval supports extra optional by-reference parameters before the static backend does. +const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["is_callable", "preg_match_all"]; + +/// Eval supports variadic behavior before the static backend does. Empty: the +/// static `var_dump` signature is variadic, so its former entry became an exact +/// shape match; keep the slice for the next genuine variadic extension. +const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[]; + +/// Builtins migrated to magician's declarative eval registry. +const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ + "abs", + "acos", + "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", + "array_flip", + "array_intersect", + "array_intersect_key", + "array_key_exists", + "array_keys", + "array_map", + "array_merge", + "array_pad", + "array_pop", + "array_product", + "array_push", + "array_rand", + "array_reduce", + "array_reverse", + "array_search", + "array_shift", + "array_slice", + "array_splice", + "array_sum", + "array_unique", + "array_unshift", + "array_values", + "array_walk", + "arsort", + "asin", + "asort", + "atan", + "atan2", + "basename", + "base64_decode", + "base64_encode", + "bin2hex", + "boolval", + "buffer_free", + "buffer_len", + "buffer_new", + "call_user_func", + "call_user_func_array", + "ceil", + "checkdate", + "chdir", + "chgrp", + "chmod", + "chown", + "class_alias", + "class_attribute_args", + "class_attribute_names", + "class_exists", + "class_get_attributes", + "class_implements", + "class_parents", + "class_uses", + "closedir", + "chr", + "chop", + "clearstatcache", + "clamp", + "cos", + "cosh", + "copy", + "count", + "crc32", + "ctype_alnum", + "ctype_alpha", + "ctype_digit", + "ctype_space", + "date", + "date_default_timezone_get", + "date_default_timezone_set", + "define", + "defined", + "deg2rad", + "die", + "dirname", + "disk_free_space", + "disk_total_space", + "exec", + "exit", + "empty", + "enum_exists", + "explode", + "exp", + "fdiv", + "file", + "file_exists", + "file_get_contents", + "file_put_contents", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", + "fclose", + "fdatasync", + "feof", + "fflush", + "fgetcsv", + "fgetc", + "fgets", + "floatval", + "floor", + "flock", + "fmod", + "fnmatch", + "fopen", + "fprintf", + "fpassthru", + "fputcsv", + "fread", + "fscanf", + "fseek", + "fsockopen", + "fstat", + "ftruncate", + "fsync", + "ftell", + "fwrite", + "function_exists", + "getdate", + "get_called_class", + "get_class", + "get_class_methods", + "get_class_vars", + "getcwd", + "get_declared_classes", + "get_declared_interfaces", + "get_declared_traits", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "get_object_vars", + "get_parent_class", + "get_resource_id", + "get_resource_type", + "getservbyname", + "getservbyport", + "gettype", + "gmdate", + "gmmktime", + "glob", + "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", + "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", + "header", + "hex2bin", + "html_entity_decode", + "htmlentities", + "htmlspecialchars", + "hrtime", + "http_response_code", + "hypot", + "implode", + "inet_ntop", + "inet_pton", + "interface_exists", + "intdiv", + "intval", + "is_a", + "is_array", + "is_bool", + "is_callable", + "is_dir", + "is_double", + "is_executable", + "is_file", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_link", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_readable", + "is_real", + "is_resource", + "is_scalar", + "is_string", + "is_subclass_of", + "in_array", + "isset", + "is_writable", + "is_writeable", + "ip2long", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", + "iterator_apply", + "iterator_count", + "iterator_to_array", + "krsort", + "ksort", + "lchgrp", + "lchown", + "lcfirst", + "link", + "linkinfo", + "localtime", + "log", + "log10", + "log2", + "long2ip", + "lstat", + "ltrim", + "max", + "method_exists", + "microtime", + "md5", + "min", + "mkdir", + "mktime", + "mt_rand", + "natcasesort", + "natsort", + "nl2br", + "number_format", + "opendir", + "ord", + "pathinfo", + "pclose", + "passthru", + "pfsockopen", + "pi", + "php_uname", + "phpversion", + "popen", + "pow", + "property_exists", + "print_r", + "printf", + "ptr", + "ptr_get", + "ptr_is_null", + "ptr_null", + "ptr_offset", + "ptr_read8", + "ptr_read16", + "ptr_read32", + "ptr_read_string", + "ptr_set", + "ptr_sizeof", + "ptr_write8", + "ptr_write16", + "ptr_write32", + "ptr_write_string", + "putenv", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", + "rad2deg", + "rand", + "random_int", + "range", + "rawurldecode", + "rawurlencode", + "readdir", + "readfile", + "readline", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", + "rename", + "round", + "rsort", + "rtrim", + "rewind", + "rewinddir", + "rmdir", + "scandir", + "sha1", + "shell_exec", + "settype", + "shuffle", + "sin", + "sinh", + "sleep", + "sort", + "sqrt", + "sprintf", + "spl_autoload", + "spl_autoload_call", + "spl_autoload_extensions", + "spl_autoload_functions", + "spl_autoload_register", + "spl_autoload_unregister", + "spl_classes", + "spl_object_hash", + "spl_object_id", + "sscanf", + "stat", + "stream_bucket_append", + "stream_bucket_make_writeable", + "stream_bucket_new", + "stream_bucket_prepend", + "stream_context_create", + "stream_context_get_default", + "stream_context_get_options", + "stream_context_get_params", + "stream_context_set_default", + "stream_context_set_option", + "stream_context_set_params", + "stream_copy_to_stream", + "stream_filter_append", + "stream_filter_prepend", + "stream_filter_register", + "stream_filter_remove", + "stream_get_contents", + "stream_get_filters", + "stream_get_line", + "stream_get_meta_data", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", + "stream_isatty", + "stream_select", + "stream_socket_accept", + "stream_socket_client", + "stream_socket_enable_crypto", + "stream_socket_get_name", + "stream_socket_pair", + "stream_socket_recvfrom", + "stream_socket_sendto", + "stream_socket_server", + "stream_socket_shutdown", + "str_contains", + "str_ends_with", + "str_ireplace", + "str_pad", + "str_replace", + "str_split", + "str_starts_with", + "strcasecmp", + "strcmp", + "strlen", + "str_repeat", + "strrev", + "strpos", + "strrpos", + "strstr", + "strtolower", + "stripslashes", + "strtoupper", + "strtotime", + "substr", + "substr_replace", + "strval", + "stream_resolve_include_path", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", + "stream_supports_lock", + "stream_wrapper_register", + "stream_wrapper_restore", + "stream_wrapper_unregister", + "system", + "symlink", + "sys_get_temp_dir", + "tan", + "tanh", + "tempnam", + "time", + "tmpfile", + "touch", + "trait_exists", + "trim", + "uasort", + "uksort", + "ucfirst", + "ucwords", + "umask", + "unlink", + "unset", + "urldecode", + "urlencode", + "usleep", + "usort", + "var_dump", + "vfprintf", + "vprintf", + "vsprintf", + "wordwrap", +]; + +/// Verifies every static builtin is visible through eval's function lookup. +#[test] +fn static_php_visible_builtins_are_visible_to_eval() { + let missing = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_exists(name)) + .collect::>(); + + assert!( + missing.is_empty(), + "static builtins missing from eval function lookup: {missing:?}" + ); +} + +/// Verifies every static builtin appears in eval's direct and dynamic dispatch sources. +#[test] +fn static_php_visible_builtins_have_eval_dispatch_literals() { + let direct_dispatch_names = php_symbol_string_literals(EVAL_DIRECT_DISPATCH_SOURCES); + let dynamic_dispatch_names = php_symbol_string_literals(EVAL_DYNAMIC_DISPATCH_SOURCES); + + let missing_direct = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name)) + .filter(|name| !direct_dispatch_names.contains(*name)) + .collect::>(); + let missing_dynamic = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name)) + .filter(|name| !dynamic_dispatch_names.contains(*name)) + .collect::>(); + + assert!( + missing_direct.is_empty(), + "static builtins missing from eval direct dispatcher literals: {missing_direct:?}" + ); + assert!( + missing_dynamic.is_empty(), + "static builtins missing from eval dynamic dispatcher literals: {missing_dynamic:?}" + ); +} + +/// Verifies migrated builtins are backed by magician's declarative registry. +#[test] +fn migrated_eval_builtins_are_registry_declared() { + for name in EVAL_DECLARATIVE_REGISTRY_BUILTINS { + assert!( + elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name), + "{name} should be declared through the eval builtin registry" + ); + } +} + +/// Extracts lowercase PHP-symbol string literals from Rust source snippets. +fn php_symbol_string_literals(sources: &[&str]) -> BTreeSet { + let mut literals = BTreeSet::new(); + for source in sources { + collect_php_symbol_string_literals(source, &mut literals); + } + literals +} + +/// Adds simple double-quoted PHP-symbol literals from one Rust source string. +fn collect_php_symbol_string_literals(source: &str, literals: &mut BTreeSet) { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + if bytes[index] != b'"' { + index += 1; + continue; + } + + index += 1; + let mut literal = String::new(); + let mut escaped = false; + while index < bytes.len() { + let byte = bytes[index]; + index += 1; + if escaped { + literal.push(byte as char); + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + break; + } else { + literal.push(byte as char); + } + } + + if is_php_symbol_literal(&literal) { + literals.insert(literal); + } + } +} + +/// Returns whether a string literal can be a lowercase PHP builtin symbol. +fn is_php_symbol_literal(literal: &str) -> bool { + !literal.is_empty() + && literal + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + +/// Verifies eval has signature metadata for each shared static builtin. +#[test] +fn shared_builtin_signature_shape_matches_static_signatures() { + let mut missing_static_signature = Vec::new(); + let mut missing_eval_signature = Vec::new(); + let mut mismatched_signatures = Vec::new(); + + for name in elephc::builtin_metadata::php_visible_builtin_names() { + if STATIC_ONLY_REGISTRY_BUILTINS.contains(name) { + continue; + } + let Some(static_meta) = elephc::builtin_metadata::builtin_signature_metadata(name) else { + missing_static_signature.push(*name); + continue; + }; + let Some(eval_meta) = elephc_magician::builtin_metadata::builtin_signature_metadata(name) else { + missing_eval_signature.push(*name); + continue; + }; + if EVAL_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_signature_extends_static_signature(name, &static_meta, &eval_meta); + continue; + } + if EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_by_ref_signature_extends_static_signature(name, &static_meta, &eval_meta); + continue; + } + if EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_variadic_signature_extends_static_signature( + name, + &static_meta, + &eval_meta, + ); + continue; + } + + if static_meta.params != eval_meta.params + || static_meta.required_param_count != eval_meta.required_param_count + || static_meta.default_param_count != eval_meta.default_param_count + || static_meta.variadic != eval_meta.variadic + || static_meta.by_ref_params != eval_meta.by_ref_params + { + mismatched_signatures.push((*name, static_meta, eval_meta)); + } + } + + assert!( + missing_static_signature.is_empty(), + "static catalog entries without signature metadata: {missing_static_signature:?}" + ); + assert!( + missing_eval_signature.is_empty(), + "shared builtins without eval parameter metadata: {missing_eval_signature:?}" + ); + assert!( + mismatched_signatures.is_empty(), + "shared builtin signature-shape mismatches: {mismatched_signatures:#?}" + ); +} + +/// Documents compiler-visible builtins whose eval support has not landed yet. +#[test] +fn static_only_registry_builtins_remain_documented_until_eval_support_lands() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + + for name in STATIC_ONLY_REGISTRY_BUILTINS { + assert!( + static_names.contains(name), + "{name} is no longer compiler-visible; remove it from the static-only allowlist" + ); + assert!( + !elephc_magician::builtin_metadata::php_visible_builtin_exists(name), + "{name} is now eval-visible; remove it from the static-only allowlist" + ); + } +} + +/// Verifies a documented eval signature extension keeps the static prefix contract. +fn assert_eval_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval extension must preserve required parameter count" + ); + assert_eq!( + static_meta.variadic, eval_meta.variadic, + "{name} eval extension must not change variadic behavior" + ); + assert_eq!( + static_meta.by_ref_params, eval_meta.by_ref_params, + "{name} eval extension must preserve by-reference parameters" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval extension must not remove defaults" + ); +} + +/// Verifies a documented eval by-reference extension keeps the static prefix contract. +fn assert_eval_by_ref_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval by-ref extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval by-ref extension must preserve required parameter count" + ); + assert_eq!( + static_meta.variadic, eval_meta.variadic, + "{name} eval by-ref extension must not change variadic behavior" + ); + assert!( + eval_meta.by_ref_params.starts_with(&static_meta.by_ref_params), + "{name} eval by-ref extension must preserve static by-reference prefix" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval by-ref extension must not remove defaults" + ); +} + +/// Verifies a documented eval variadic extension keeps the static prefix contract. +fn assert_eval_variadic_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval variadic extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval variadic extension must preserve required parameter count" + ); + assert!( + static_meta.variadic.is_none() && eval_meta.variadic.is_some(), + "{name} eval variadic extension must add, not remove, variadic behavior" + ); + assert_eq!( + static_meta.by_ref_params, eval_meta.by_ref_params, + "{name} eval variadic extension must preserve by-reference parameters" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval variadic extension must not remove defaults" + ); +} + +/// Documents the current eval-only reflection builtins so the drift is explicit. +#[test] +fn eval_only_reflection_builtins_remain_visible_until_static_catalog_catches_up() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + + for name in EVAL_ONLY_REFLECTION_BUILTINS { + assert!( + !static_names.contains(name), + "{name} moved into the static catalog; remove it from the eval-only allowlist" + ); + assert!( + elephc_magician::builtin_metadata::php_visible_builtin_exists(name), + "{name} should stay visible to eval while it is documented as eval-only" + ); + } +} + +/// Verifies magician does not expose unexpected builtin names outside the static catalog. +#[test] +fn eval_php_visible_builtins_are_static_or_documented_eval_only() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + let eval_only = EVAL_ONLY_REFLECTION_BUILTINS + .iter() + .copied() + .collect::>(); + let unexpected = elephc_magician::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !static_names.contains(name) && !eval_only.contains(name)) + .collect::>(); + + assert!( + unexpected.is_empty(), + "eval exposes builtins outside the static catalog and eval-only allowlist: {unexpected:?}" + ); +} diff --git a/tests/codegen/callables/state_and_variadics.rs b/tests/codegen/callables/state_and_variadics.rs index aceea83b3b..124dc2c1f6 100644 --- a/tests/codegen/callables/state_and_variadics.rs +++ b/tests/codegen/callables/state_and_variadics.rs @@ -286,6 +286,34 @@ echo $x; assert_eq!(out, "15"); } +/// Verifies by-reference variadic function and method element assignments mutate caller variables. +#[test] +fn test_by_ref_variadic_function_and_method_element_writeback() { + let out = compile_and_run( + r#"m($c, $d); +echo $c . ":" . $d; +"#, + ); + assert_eq!(out, "A-f:B-g|C-m:D-n"); +} + // --- Variadic functions --- /// Verifies a variadic function collects exactly three positional arguments into the rest array. diff --git a/tests/codegen/casts_and_constants/predicates.rs b/tests/codegen/casts_and_constants/predicates.rs index be6a55bce6..7c597b4a14 100644 --- a/tests/codegen/casts_and_constants/predicates.rs +++ b/tests/codegen/casts_and_constants/predicates.rs @@ -72,6 +72,72 @@ fn test_is_numeric_string() { assert_eq!(out, ""); } +/// Verifies PHP scalar predicate aliases and container/object predicates compile as builtin calls. +#[test] +fn test_type_predicate_aliases_array_and_object() { + let out = compile_and_run( + r#" 1]) ? "h" : "_"; +echo is_object($object) ? "o" : "_"; +echo is_object([1]) ? "bad" : "_"; +"#, + ); + assert_eq!(out, "ildraho_"); +} + +/// Verifies `is_array()` inspects boxed Mixed JSON payload tags for array values. +#[test] +fn test_is_array_recognizes_arrays_inside_mixed_array() { + let out = compile_and_run( + r#" 1, "b" => 2]) + + count(["1" => "a", 1 => "b", "01" => "c"]) + + count([1, [2, 3], ["x" => 4]]) + + count([true => "yes", 1 => "one", false => "no", 0 => "zero"]) + + count([null => "empty", "" => "blank", "name" => "Ada"]) + + count([2.0 => "two", 2 => "int", -2.0 => "minus", -2 => "intminus"]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array count should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array count should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array count should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array count should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array count should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "16"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies runtime `count()` calls on eval-created local arrays use EIR AOT. +#[test] +fn test_literal_eval_local_array_count_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_local_array_count_aot"); + let source = r#" 1, "b" => 2]; return count($items) + count($map);'); +echo ":" . count($items) . ":" . count($map); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval runtime count on local arrays should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval runtime count on local arrays should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "runtime count on eval-created arrays should flush created locals through scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "scope-only runtime count on local arrays should not create an eval bridge context:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "scope-only runtime count on local arrays should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only runtime count on local arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "5:3:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `count()` can consume caller-scope arrays through direct-param EIR AOT. +#[test] +fn test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_count_scope_read_array_aot"); + let source = r#" 1, "b" => 2]; +echo eval('return count($items) . ":" . count($map);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "count over caller-scope arrays should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "count over caller-scope arrays should not reference eval runtime helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "count over caller-scope arrays should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "count over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "3:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named `count()` with default mode can use direct-param EIR AOT. +#[test] +fn test_literal_eval_count_named_default_mode_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_count_named_default_mode_aot"); + let source = r#" null]) ? "Y" : "bad") + . (array_key_exists("missing", ["name" => 1]) ? "bad" : "N") + . (array_key_exists("1", [1 => "one"]) ? "I" : "bad") + . (array_key_exists(2, ["0" => "a", "01" => "b", 2 => "c"]) ? "K" : "bad") + . (array_key_exists(0, ["x", "y"]) ? "Z" : "bad") + . (array_key_exists(2, ["x", "y"]) ? "bad" : "O") + . (array_key_exists(true, [1 => "yes"]) ? "T" : "bad") + . (array_key_exists(false, [0 => "no"]) ? "F" : "bad") + . (array_key_exists(null, ["" => "empty"]) ? "E" : "bad") + . (array_key_exists(2.0, [2.0 => "two"]) ? "D" : "bad") + . (array_key_exists(-2.0, [-2.0 => "minus"]) ? "M" : "bad");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array_key_exists should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array_key_exists should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array_key_exists should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array_key_exists should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array_key_exists should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "YNIKZOTFEDM"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `array_key_exists()` can inspect caller-scope arrays through direct-param AOT. +#[test] +fn test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_array_aot"); + let source = r#" null, "age" => 42]; +echo eval('return (array_key_exists(1, $items) ? "I" : "bad") + . ":" . (array_key_exists(3, $items) ? "bad" : "N") + . ":" . (array_key_exists("name", $map) ? "A" : "bad") + . ":" . (array_key_exists("missing", $map) ? "bad" : "M");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "array_key_exists over caller-scope arrays should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "array_key_exists over caller-scope arrays should not reference eval runtime helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "array_key_exists over caller-scope arrays should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "array_key_exists over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "I:N:A:M"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies caller-scope `array_key_exists()` supports null and integral float keys in EIR AOT. +#[test] +fn test_literal_eval_array_key_exists_scope_read_null_and_float_keys_use_eir_aot() { + let dir = + make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_null_float_keys_aot"); + let source = r#" "empty", -2 => "minus", 2 => "two"]; +echo eval('return (array_key_exists(1.0, $items) ? "F" : "bad") + . ":" . (array_key_exists(null, $map) ? "N" : "bad") + . ":" . (array_key_exists(-2.0, $map) ? "M" : "bad");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "array_key_exists with null/integral float keys should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "array_key_exists with null/integral float keys should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "array_key_exists with null/integral float keys should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "array_key_exists with null/integral float keys should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "F:N:M"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named args for EIR-safe runtime builtins stay on the eval AOT path. +#[test] +fn test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_named_runtime_builtins_aot"); + let source = r#" "Ada"]; +echo eval('return count(value: $items) + . ":" . (boolval(value: $flag) ? "B" : "bad") + . ":" . (array_key_exists(array: $map, key: "name") ? "Y" : "bad") + . ":" . (array_key_exists(key: "missing", array: $map) ? "bad" : "N");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "named runtime builtins should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "named runtime builtins should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "named runtime builtins should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "named runtime builtins should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:B:Y:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static spread args for EIR-safe runtime builtins stay on the eval AOT path. +#[test] +fn test_literal_eval_static_spread_runtime_builtins_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_spread_runtime_builtins_aot"); + let source = r#" "Ada"]; +echo eval('return count(...["value" => $items]) + . ":" . (boolval(...["value" => $flag]) ? "B" : "bad") + . ":" . (array_key_exists(...["array" => $map, "key" => "name"]) ? "Y" : "bad") + . ":" . (array_key_exists(...["key" => "missing", "array" => $map]) ? "bad" : "N");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "static-spread runtime builtins should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static-spread runtime builtins should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static-spread runtime builtins should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static-spread runtime builtins should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:B:Y:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies dynamic spread args for runtime builtins keep the eval bridge fallback. +#[test] +fn test_literal_eval_dynamic_spread_runtime_builtin_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_dynamic_spread_runtime_builtin_bridge"); + let source = r#" $items]; +echo eval('return count(...$args);'); +"#; + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("__elephc_eval_execute"), + "dynamic-spread runtime builtin should keep the interpreter bridge fallback:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "dynamic-spread runtime builtin should link elephc_magician for fallback: {required_libraries:?}" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `array_key_exists()` on a caller scalar keeps the bridge fallback. +#[test] +fn test_literal_eval_array_key_exists_scope_read_scalar_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_scalar_bridge"); + let source = r#" "Ada"]["name"];'); +echo ":"; +echo eval('return isset(["name" => "Ada"]["name"]) ? "Y" : "bad";'); +echo ":"; +echo eval('return isset(["name" => null]["name"]) ? "bad" : "N";'); +echo ":"; +echo eval('return isset(["name" => "Ada"]["missing"]) ? "bad" : "M";'); +echo ":"; +echo eval('return empty(["name" => ""]["name"]) ? "E" : "bad";'); +echo ":"; +echo eval('return empty(["name" => "Ada"]["name"]) ? "bad" : "V";'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:Ada:Y:N:M:E:V"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static array literals returned from eval lower through EIR AOT. +#[test] +fn test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_literal_return_aot"); + let source = r#" "L", "right" => "R"];'); +echo $map["left"] . $map["right"]; +echo ":"; +$rows = eval('return [[10, 20], ["name" => "Ada"]];'); +echo $rows[0][1] . ":" . $rows[1]["name"]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array returns should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array returns should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array returns should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array returns should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array returns should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "ab:LR:20:Ada"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static associative array auto keys can lower through eval EIR AOT. +#[test] +fn test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_next_key_aot"); + let source = r#" "two", "tail"][3];'); +echo ":"; +echo eval('return [-2 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return ["2" => "two", "tail"][3];'); +echo ":"; +echo eval('return ["02" => "two", "tail"][0];'); +echo ":"; +echo eval('return [true => "yes", "tail"][2];'); +echo ":"; +echo eval('return [false => "no", "tail"][1];'); +echo ":"; +echo eval('return [null => "empty"][""];'); +echo ":"; +echo eval('return [null => "empty", "tail"][0];'); +echo ":"; +echo eval('return [2.0 => "two", "tail"][3];'); +echo ":"; +echo eval('return [-2.0 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return array(2 => "two", "tail")[3];'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static array next-key reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static array next-key reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array next-key reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array next-key reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array next-key reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!( + out, + "tail:tail:tail:tail:tail:tail:empty:tail:tail:tail:tail" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies fractional float array keys stay on the bridge because PHP emits a precision warning. +#[test] +fn test_eval_static_array_fractional_float_key_uses_bridge_fallback() { + let dir = make_cli_test_dir("elephc_eval_static_array_fractional_float_key_bridge"); + let source = r#" "two", "tail"][3];'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT fallback"), + "fractional float array key should keep the explicit literal eval fallback marker:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_execute"), + "fractional float array key should execute through the bridge:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "fractional float array key fallback should link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "tail"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static array writes into eval scope lower through EIR AOT. +#[test] +fn test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_scope_write_aot"); + let source = r#" "Ada"];'); +echo $map["name"]; +echo ":"; +eval('$legacy = array("x", "y");'); +echo $legacy[0] . $legacy[1]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static array scope writes should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "static array scope writes should flush through eval scope set:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static array scope writes should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "static array scope writes should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only static array eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "b:Ada:xy"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named/static-spread builtin args normalize before literal eval AOT folding. +#[test] +fn test_literal_eval_static_builtin_named_args_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_builtin_named_args_aot"); + let source = r#" "cd"]) + + count(value: [1, 2, 3]) + + (array_key_exists(array: ["name" => null], key: "name") ? 10 : 0) + + (str_contains(needle: "x", haystack: "xyz") ? 20 : 0) + + strlen(str_repeat(times: 3, string: "q")) + + strlen(substr(length: 2, string: "abcdef", offset: 1));'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static builtin named args should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static builtin named args should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static builtin named args should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static builtin named args should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static builtin named args should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "42"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies foldable static builtins also work in direct local-scalar statement bodies. +#[test] +fn test_literal_eval_static_scalar_builtins_in_local_body_use_aot() { + let dir = make_cli_test_dir("elephc_literal_eval_static_scalar_builtins_local_body_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + " "B", "left" => "A"]) + . ":" . join_static_spread(...["left" => "C", "right" => "D", "bang" => true]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static function static-spread args should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static function static-spread args should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static-spread function eval should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static-spread function eval should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static-spread function eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.:C:D!"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies dynamic spread args for scalar user functions keep the eval bridge fallback. +#[test] +fn test_literal_eval_static_user_function_dynamic_spread_args_keep_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_static_function_dynamic_spread_args_bridge"); + let source = r#" "A", "right" => "B"]; +echo eval('return join_dynamic_spread(...$args);'); +"#; + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("__elephc_eval_execute"), + "dynamic-spread static function call should keep the interpreter bridge fallback:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "dynamic-spread static function call should link elephc_magician for fallback: {required_libraries:?}" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static user-function scalar defaults are accepted by literal eval EIR AOT. +#[test] +fn test_literal_eval_static_user_function_defaults_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_function_defaults_aot"); + let source = r#" $s]) + . ":" . call_user_func("strtoupper", "az");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "static call_user_func builtin callbacks should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static call_user_func builtin callbacks should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static call_user_func builtin callbacks should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static call_user_func builtin callbacks should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "4:4:AZ"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static `call_user_func*()` user callbacks can use literal eval EIR AOT. +#[test] +fn test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_call_user_func_user_function_aot"); + let source = r#" "D", "left" => "C", "bang" => true]) + . "|" . call_user_func(eval_cuf_join(...), "E", "F", true) + . "|" . call_user_func_array(eval_cuf_join(...), ["right" => "H", "left" => "G"]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static call_user_func user callbacks should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static call_user_func user callbacks should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static call_user_func user callbacks should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static call_user_func user callbacks should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static call_user_func user callbacks should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.|C:D!|E:F!|G:H."); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static method callbacks can use literal eval EIR AOT. +#[test] +fn test_literal_eval_static_method_callbacks_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_method_callbacks_aot"); + let source = r#" "F", "left" => "E"]) + . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]) + . "|" . call_user_func([EvalAotStaticMethodCallbackBox::class, "join"], "G", "H") + . "|" . call_user_func_array([EvalAotStaticMethodCallbackBox::class, "inc"], [9]) + . "|" . call_user_func(EvalAotStaticMethodCallbackBox::join(...), "I", "J", true) + . "|" . call_user_func_array(EvalAotStaticMethodCallbackBox::join(...), ["right" => "L", "left" => "K"]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static method callbacks should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static method callbacks should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static method callback eval should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static method callback eval should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static method callback eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10|I:J!|K:L."); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static method callbacks without declared scalar signatures keep the bridge fallback. +#[test] +fn test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_static_method_callback_untyped_bridge"); + let source = r#"= 2, + "null/fallthrough literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "null/fallthrough literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only null/fallthrough literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only null/fallthrough literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only null/fallthrough literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "N:body:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies ternary expressions inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_ternary_expressions_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_ternary_eir_aot"); + let source = r#"= 2, + "ternary literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "ternary literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only ternary literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only ternary literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only ternary literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "yes:fallback:len"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies null coalesce inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_null_coalesce_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_null_coalesce_eir_aot"); + let source = r#"= 4, + "null coalesce literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "null coalesce caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "null coalesce literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only null coalesce literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only null coalesce literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only null coalesce literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "literal:set:caller:missing:nullcaller"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies match expressions inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_match_expression_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_match_eir_aot"); + let source = r#" "int", "1" => "string", default => "other" };'); +echo ":"; +$x = 3; +echo eval('return match ($x) { 1, 2 => "small", 3 => "three", default => "other" };'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm + .matches("eval literal AOT compiled EIR function") + .count() + >= 2, + "match literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "match literal eval caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "match literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only match literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only match literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only match literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "string:three"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies strict equality operators inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_strict_equality_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_strict_equality_eir_aot"); + let source = r#"= 2, + "strict-equality literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "strict-equality caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "strict-equality literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only strict-equality literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only strict-equality literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only strict-equality literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "S:D:same"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies logical xor inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_logical_xor_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_logical_xor_eir_aot"); + let source = r#"= 2, + "logical xor literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "logical xor caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "logical xor literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only logical xor literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only logical xor literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only logical xor literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "T:F"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies scalar casts inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_scalar_casts_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_scalar_casts_eir_aot"); + let source = r#"= 3, + "scalar-cast literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "scalar-cast caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "scalar-cast literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only scalar-cast literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only scalar-cast literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only scalar-cast literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "42:7:F:1.25:42"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies division and exponentiation inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_division_and_pow_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_division_pow_eir_aot"); + let source = r#"= 2, + "division and exponentiation literal evals should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "division and exponentiation literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "native-only division/pow literal evals should not create an eval context:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "division and exponentiation literal evals should not emit the eval bridge runtime:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "division and exponentiation literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "4.5:512"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies compound division/modulo inside literal eval uses direct local sync. +#[test] +fn test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_division_modulo_assign_eir_aot"); + let source = r#"> 2); +$x = 6; $x &= 3; echo ":" . $x; +$x = 4; $x |= 1; echo "," . $x; +$x = 7; $x ^= 3; echo "," . $x; +$x = 1; $x <<= 5; echo "," . $x; +$x = 64; $x >>= 3; echo "," . $x;'); +echo ":" . $x; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled local scalar with direct local sync"), + "bitwise/shift literal eval should use local-scalar AOT with direct sync:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_scope_set"), + "bitwise/shift literal eval should not write through eval scope:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "bitwise/shift literal eval should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "bitwise/shift literal eval should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "bitwise/shift literal eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "1:7:6:-1:16:-4:2,5,4,32,8:8"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies spaceship comparisons inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_spaceship_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_spaceship_eir_aot"); + let source = r#" 2;'); +echo ":"; +echo eval('return 2.5 <=> 2.5;'); +echo ":"; +$a = 12; +echo eval('return $a <=> 10;'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm + .matches("eval literal AOT compiled EIR function") + .count() + >= 3, + "spaceship literal evals should use EIR AOT:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "spaceship caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "spaceship literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only spaceship literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only spaceship literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only spaceship literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "-1:0:1"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies float literal eval returns can lower through an internal EIR AOT function. +#[test] +fn test_literal_eval_float_return_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_float_return_eir_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + "i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } + public function valid(): bool { return $this->i < 0; } + public function rewind(): void { $this->i = 0; } +} +$items = [1, 2]; +$iterator = new EvalAotDirectIterator(); +$n = 42; +echo eval('return (is_iterable($items) ? "A" : "bad") . + (is_iterable($iterator) ? "I" : "bad") . + (is_iterable($n) ? "bad" : "N");'); +"#, + &dir, + 8_388_608, + false, + false, + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "is_iterable over caller-scope values should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "is_iterable over caller-scope values should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "is_iterable over caller-scope values should not allocate an eval bridge context:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "is_iterable over caller-scope values should not emit the interpreter bridge:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "is_iterable over caller-scope values should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "AIN"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `is_numeric()` and `is_resource()` use direct-param EIR AOT for scope reads. +#[test] +fn test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_numeric_resource_probe_scope_read_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + r#" 0; --$k) { echo $k; }'); +echo ":" . $i . ":" . $j . ":" . $k; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled local scalar with direct local sync"), + "literal eval local inc/dec should use local-scalar AOT with direct sync:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_scope_set"), + "literal eval local inc/dec should not write through eval scope:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval local inc/dec should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "literal eval local inc/dec should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "literal eval local inc/dec should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "1012321:1:3:0"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies supported `switch` statements inside literal eval lower through local-scalar AOT. +#[test] +fn test_literal_eval_switch_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_switch_aot"); + let source = r#" 1, "b" => 2] as $key => $value) { echo $key . ":" . $value . ";"; }'); +echo ":" . $key . ":" . $value; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with scope reads"), + "static foreach eval should use the scope-aware EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "static foreach eval should synchronize loop variables through eval scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static foreach eval should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "static foreach eval should not emit the eval execute runtime bridge:\n{runtime_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "static foreach eval should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only static foreach eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "123:3|a:1;b:2;:b:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static empty foreach in eval uses AOT without publishing loop locals. +#[test] +fn test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers() { + let dir = make_cli_test_dir("elephc_literal_eval_static_empty_foreach_aot"); + let source = r#" 10, "y" => 20]; +$key = "old-key"; +$value = 0; +eval('foreach ($pairs as $key => $value) { echo $key . ":" . $value . ";"; }'); +echo ":" . $key . ":" . $value . "|"; +$empty = []; +$kept = "keep"; +eval('foreach ($empty as $kept) { echo "bad"; }'); +echo $kept; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with scope reads"), + "foreach over caller-scope arrays should use the scope-aware EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_get"), + "foreach over caller-scope arrays should read the source through eval scope helpers:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "foreach over caller-scope arrays should synchronize loop variables through eval scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "foreach over caller-scope arrays should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "foreach over caller-scope arrays should not emit the eval execute runtime bridge:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only foreach over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "ab:b|x:10;y:20;:y:20|keep"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies foreach over a caller scalar keeps the bridge fallback. +#[test] +fn test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_foreach_scope_scalar_bridge"); + let source = r#" "z"]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +echo function_exists("print_r");'); +"#, + ); + assert_eq!(out, "x::Array\n(\n [0] => 1\n [1] => 2\n)\n:1z:call:spread:1"); +} + +/// Verifies eval `var_dump()` writes PHP-style diagnostics and returns null. +#[test] +fn test_eval_dispatches_var_dump_builtin_call() { + let out = compile_and_run( + r#" true]); +$call = call_user_func("var_dump", 3.5); +$spread = call_user_func_array("var_dump", ["value" => "z"]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +echo function_exists("var_dump");'); +"#, + ); + assert_eq!( + out, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:1", + ) + ); +} + +/// Verifies eval `var_dump()` prints eval-declared and generated object class names. +#[test] +fn test_eval_var_dump_prints_object_class_names() { + let out = compile_and_run( + r#"> 2);'); +echo ":"; +eval('$x = 6; $x &= 3; echo $x; echo ","; $x = 4; $x |= 1; echo $x; echo ","; $x = 7; $x ^= 3; echo $x; echo ","; $x = 1; $x <<= 5; echo $x; echo ","; $x = 64; $x >>= 3; echo $x;'); +"#, + ); + assert_eq!(out, "1:7:6:-1:16:-4:2,5,4,32,8"); +} + +/// Verifies the eval bridge routes concatenation through runtime string helpers. +#[test] +fn test_eval_scalar_concat_executes_through_bridge() { + let out = compile_and_run(" 3; echo 4 >= 4; echo 5 != 6; echo 7 == 7;'); +"#, + ); + assert_eq!(out, "111111"); +} + +/// Verifies eval spaceship comparisons return boxed -1/0/1 integers. +#[test] +fn test_eval_spaceship_executes() { + let out = compile_and_run( + r#" 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;'); +"#, + ); + assert_eq!(out, "-1:0:1"); +} + +/// Verifies loose scalar equality in eval handles strings and null/empty-string rules. +#[test] +fn test_eval_scalar_loose_equality_executes_through_bridge() { + let out = compile_and_run( + r#" 0; --$k) { echo $k; }'); +"#, + ); + assert_eq!(out, "1:012:321"); +} + +/// Verifies eval if/else branches use PHP truthiness and update the caller scope. +#[test] +fn test_eval_if_else_updates_scope() { + let out = compile_and_run( + r#" "int", "1" => "string", default => "other" }; +echo ":"; +echo match (3) { 1, 2 => missing(), default => "fallback" };'); +"#, + ); + assert_eq!(out, "string:fallback"); +} + +/// Verifies break and continue control a loop interpreted inside eval. +#[test] +fn test_eval_break_and_continue_control_loop() { + let out = compile_and_run( + r#" $item) { echo $key . ":" . $item . ";"; }'); +echo "|" . $key . ":" . $item; +"#, + ); + assert_eq!(out, "0:10;1:20;|1:20"); +} + +/// Verifies eval foreach can iterate an indexed array from the caller scope. +#[test] +fn test_eval_foreach_reads_scope_array() { + let out = compile_and_run( + r#"i = 0; } + public function valid(): bool { echo "valid" . $this->i . ":"; return $this->i < 2; } + public function current(): mixed { echo "current" . $this->i . ":"; return "v" . $this->i; } + public function key(): mixed { echo "key" . $this->i . ":"; return "k" . $this->i; } + public function next(): void { echo "next" . $this->i . ":"; $this->i = $this->i + 1; } +} +class EvalAotForeachAggregate implements IteratorAggregate { + public function getIterator(): Traversable { echo "agg:"; return new EvalAotForeachIterator(); } +} +eval('foreach (new EvalAotForeachIterator() as $key => $item) { + echo $key . "=" . $item . ":"; + if ($item === "v0") { continue; } + break; +} +echo "|"; +foreach (new EvalAotForeachAggregate() as $item) { + echo $item . ":"; +}'); +"#, + ); + assert_eq!( + out, + "rewind:valid0:current0:key0:k0=v0:next0:valid1:current1:key1:k1=v1:|agg:rewind:valid0:current0:v0:next0:valid1:current1:v1:next1:valid2:" + ); +} + +/// Verifies value-only foreach loops inside eval iterate associative array values. +#[test] +fn test_eval_foreach_iterates_assoc_values() { + let out = compile_and_run( + r#" 1, "b" => 2] as $item) { echo $item; }'); +"#, + ); + assert_eq!(out, "12"); +} + +/// Verifies key-value foreach loops inside eval expose associative keys in insertion order. +#[test] +fn test_eval_foreach_iterates_assoc_keys_and_values() { + let out = compile_and_run( + r#" 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }'); +echo "|" . $key . ":" . $item; +"#, + ); + assert_eq!(out, "a:1;b:2;|b:2"); +} + +/// Verifies eval indexed-array literals and reads execute through Mixed array helpers. +#[test] +fn test_eval_indexed_array_literal_and_read() { + let out = compile_and_run(" "Ada",)["name"];'); +echo ":"; +$rows = eval('return ARRAY(array(10, 20), array("name" => "Ada"));'); +echo $rows[0][1] . ":" . $rows[1]["name"]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "legacy array syntax static reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "legacy array syntax static reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only legacy array syntax static reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only legacy array syntax static reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only legacy array syntax static reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "b:Ada:20:Ada"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies legacy `array(...)` next-key assignment can read the local array through EIR AOT. +#[test] +fn test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers() { + let dir = make_cli_test_dir("elephc_eval_legacy_array_next_key_aot_scope"); + let source = r#" "two", "tail",); echo $items[3];'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "legacy array next-key eval assignment should use EIR AOT:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "legacy array next-key eval assignment should write through eval scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "legacy array next-key eval assignment should not execute through the bridge:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "legacy array next-key eval assignment should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only legacy array next-key eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "tail"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies eval indexed-array writes mutate an array visible to native code. +#[test] +fn test_eval_indexed_array_write_is_visible_after_eval() { + let out = compile_and_run( + r#" "Ada"]; $items[] = "Grace"; return $items[0];'); +echo ":"; +echo eval('$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];'); +echo ":"; +echo eval('$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];'); +"#, + ); + assert_eq!(out, "Grace:tail:tail"); +} + +/// Verifies eval can read a native Mixed array through runtime array helpers. +#[test] +fn test_eval_reads_native_mixed_array() { + let out = compile_and_run( + r#" "Ada"]; +eval('echo $items["name"];'); +"#, + ); + assert_eq!(out, "Ada"); +} + +/// Verifies eval can write string-keyed native associative arrays through Mixed helpers. +#[test] +fn test_eval_writes_native_assoc_array_string_key() { + let out = compile_and_run( + r#" "Ada"]; +eval('$items["name"] = "Grace";'); +echo $items["name"]; +"#, + ); + assert_eq!(out, "Grace"); +} + +/// Verifies eval can create and read associative array literals with string keys. +#[test] +fn test_eval_assoc_array_literal_and_string_key_read() { + let out = compile_and_run(r#" "Ada"]["name"];');"#); + assert_eq!(out, "Ada"); +} + +/// Verifies eval associative-array literals use PHP's next automatic key. +#[test] +fn test_eval_assoc_array_literal_unkeyed_entries_use_next_key() { + let out = compile_and_run( + r#" "Ada", "Grace"][0];'); +echo ":"; +echo eval('return [2 => "two", "tail"][3];'); +echo ":"; +echo eval('return [-2 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return ["2" => "two", "tail"][3];'); +echo ":"; +echo eval('return ["02" => "two", "tail"][0];'); +echo ":"; +echo eval('return [null => "empty"][""];'); +echo ":"; +echo eval('return [null => "empty", "tail"][0];'); +echo ":"; +echo eval('return [true => "yes", "tail"][2];'); +echo ":"; +echo eval('return [false => "no", "tail"][1];'); +echo ":"; +echo eval('return [2.7 => "two", "tail"][3];'); +"#, + ); + assert_eq!(out, "Grace:tail:tail:tail:tail:empty:tail:tail:tail:tail"); +} + +/// Verifies eval-created associative arrays remain visible to native code. +#[test] +fn test_eval_created_assoc_array_is_visible_after_eval() { + let out = compile_and_run( + r#" "Ada"];'); +echo $items["name"]; +"#, + ); + assert_eq!(out, "Ada"); +} + +/// Verifies nested eval calls reuse the materialized caller scope. +#[test] +fn test_eval_nested_eval_uses_same_scope() { + let out = compile_and_run( + r#" [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +echo defined("COUNT_RECURSIVE") ? "C" : "bad";'); +"#, + ); + assert_eq!(out, "4:2:3:6:2:3:C"); +} + +/// Verifies eval can dispatch raw pointer and buffer extension builtins through the bridge. +#[test] +fn test_eval_dispatches_raw_memory_builtins() { + let out = compile_and_run( + r#" $payload, "value" => 4660]); +echo ptr_read16($payload) . ":"; +ptr_write32($payload, 305419896); +echo ptr_read32($payload) . ":"; +$written = ptr_write_string($payload, "GET /"); +echo $written . ":" . ptr_read_string($payload, $written) . ":"; +echo strlen(ptr_read_string($payload, 0)); +buffer_free($buf); +echo ":" . (ptr_is_null($buf) ? "freed" : "live"); +return ":" . function_exists("ptr_read16") . is_callable("ptr_write_string") . function_exists("buffer_new");'); +"#, + ); + assert_eq!(out, "4:123456789:255,1:4660:305419896:5:GET /:0:freed:111"); +} + +/// Verifies eval `count()` dispatches through `Countable` for generated/AOT objects. +#[test] +fn test_eval_counts_aot_countable_objects() { + let out = compile_and_run( + r#"n = $n; } + public function count(): int { echo "count:"; return $this->n; } +} +$bag = new EvalAotCountableBag(5); +eval('echo count($bag); echo ":"; echo count($bag, COUNT_RECURSIVE); echo ":"; echo call_user_func_array("count", ["value" => $bag]);'); +"#, + ); + assert_eq!(out, "count:5:count:5:count:5"); +} + +/// Verifies eval dispatches `ArrayAccess` reads, writes, append, probes, and unset on AOT objects. +#[test] +fn test_eval_dispatches_aot_array_access_objects() { + let out = compile_and_run( + r#" 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . chr(128) . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode(["k" . chr(128) => "v" . chr(128)], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; +echo function_exists("json_encode");'); +"#, + ); + assert_eq!( + out, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:1"# + ); +} + +/// Verifies eval `json_decode()` materializes scalar, indexed, and associative values. +#[test] +fn test_eval_dispatches_json_decode_builtin_call() { + let out = compile_and_run( + r#" "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +$badJson = "\"a" . chr(128) . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . chr(128) . "\":\"v" . chr(128) . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . chr(128) . "\":\"v" . chr(128) . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; +echo function_exists("json_decode");'); +"#, + ); + assert_eq!( + out, + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:1" + ); +} + +/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. +#[test] +fn test_eval_dispatches_json_decode_stdclass_default() { + let out = compile_and_run( + r#"a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":";'); +$object = eval('return json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}");'); +echo gettype($object) . ":" . $object->a . ":" . $object->b->c; +"#, + ); + assert_eq!(out, "1:x:2:3:y:object:1:x"); +} + +/// Verifies eval `json_encode()` serializes stdClass dynamic properties. +#[test] +fn test_eval_dispatches_json_encode_stdclass_object() { + let out = compile_and_run( + r#"a = 7; +echo json_encode($empty);'); +"#, + ); + assert_eq!( + out, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); +} + +/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. +#[test] +fn test_eval_dispatches_json_last_error_builtin_calls() { + let out = compile_and_run( + r#"getCode() . ":" . (str_contains($e->getMessage(), "Syntax error") ? "syntax" : "bad") . ":"; +} +try { + eval('json_encode(INF, JSON_THROW_ON_ERROR);'); + echo "bad"; +} catch (Throwable $e) { + echo "encode:" . get_class($e) . ":" . $e->getCode() . ":" . $e->getMessage() . ":"; +} +eval('echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":";'); +eval('$json = chr(34) . "a" . chr(128) . "b" . chr(34); echo json_decode($json, true, 512, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) . ":";'); +"#, + ); + assert_eq!( + out, + "inner:outer:JsonException:4:syntax:encode:JsonException:7:Inf and NaN cannot be JSON encoded:0:ab:" + ); +} + +/// Verifies eval `json_validate()` validates JSON syntax, depth, and dynamic calls. +#[test] +fn test_eval_dispatches_json_validate_builtin_call() { + let out = compile_and_run( + r#" "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; +echo function_exists("json_validate");'); +"#, + ); + assert_eq!(out, "Y:N:D:C:A:I:0:S:4:1"); +} + +/// Verifies eval direct builtin calls bind named arguments and spread arrays. +#[test] +fn test_eval_dispatches_named_and_spread_builtin_calls() { + let out = compile_and_run( + r#" 1], key: "name") ? "Y" : "N"); +echo ":" . round(precision: 1, num: 3.14); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N");'); +"#, + ); + assert_eq!(out, "4:Y:3.1:Y"); +} + +/// Verifies eval `ord()` returns the first byte and dispatches dynamically. +#[test] +fn test_eval_dispatches_ord_builtin_call() { + let out = compile_and_run( + r#" 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); echo function_exists("array_product");'); +"#, + ); + assert_eq!(out, "6:24:0:1:7:7:10:11"); +} + +/// Verifies eval `array_map()` applies callbacks and preserves source keys. +#[test] +fn test_eval_dispatches_array_map_builtin_call() { + let out = compile_and_run( + r#" "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +echo function_exists("array_map");'); +"#, + ); + assert_eq!(out, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:1"); +} + +/// Verifies eval `array_reduce()` folds values through a string callback. +#[test] +fn test_eval_dispatches_array_reduce_builtin_call() { + let out = compile_and_run( + r#" [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +echo function_exists("array_reduce");'); +"#, + ); + assert_eq!(out, "16:9:ab:13:9:9:1"); +} + +/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. +#[test] +fn test_eval_dispatches_array_walk_builtin_call() { + let out = compile_and_run( + r#" 2, "b" => 3]; +echo array_walk($walk, "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +echo function_exists("array_walk");'); +"#, + ); + assert_eq!(out, "a=2;b=3;T:0=4;1=5;z=6;1"); +} + +/// Verifies eval `array_pop()` and `array_shift()` mutate writable lvalue arguments. +#[test] +fn test_eval_dispatches_array_pop_shift_builtin_calls() { + let out = compile_and_run( + r#" 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +class EvalArrayPopShiftPropertyBox { + public array $items = ["p", "q"]; + public static array $staticItems = ["s", "t"]; +} +$box = new EvalArrayPopShiftPropertyBox(); +echo array_pop($box->items) . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$name = "items"; +echo array_push($box->{$name}, "r") . ":" . $box->items[1] . ":"; +echo array_shift(EvalArrayPopShiftPropertyBox::$staticItems) . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":"; +$class = "EvalArrayPopShiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "u") . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":" . EvalArrayPopShiftPropertyBox::$staticItems[1] . ":"; +echo function_exists("array_pop") && function_exists("array_shift");'); +"#, + ); + assert_eq!(out, "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:1"); +} + +/// Verifies eval `array_push()` and `array_unshift()` mutate writable lvalue arguments. +#[test] +fn test_eval_dispatches_array_push_unshift_builtin_calls() { + let out = compile_and_run( + r#" 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +class EvalArrayPushUnshiftPropertyBox { + public array $items = ["p"]; + public static array $staticItems = ["s"]; +} +$box = new EvalArrayPushUnshiftPropertyBox(); +echo array_push($box->items, "q", "r") . ":" . $box->items[2] . ":"; +$name = "items"; +echo array_unshift($box->{$name}, "o") . ":" . $box->items[0] . ":" . $box->items[3] . ":"; +echo array_push(EvalArrayPushUnshiftPropertyBox::$staticItems, "t") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[1] . ":"; +$class = "EvalArrayPushUnshiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "r") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[0] . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[2] . ":"; +echo function_exists("array_push") && function_exists("array_unshift");'); +"#, + ); + assert_eq!( + out, + "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:3:r:4:o:r:2:t:3:r:t:1" + ); +} + +/// Verifies first-class eval builtin callables preserve ref-like writeback targets. +#[test] +fn test_eval_first_class_ref_like_builtin_callables_write_back_lvalues() { + let out = compile_and_run( + r#"items, "b") . ":" . $box->items[1] . ":"; +$rsort = rsort(...); +echo $rsort(EvalFirstClassRefLikeBuiltinBox::$staticItems) . ":" . EvalFirstClassRefLikeBuiltinBox::$staticItems[0] . EvalFirstClassRefLikeBuiltinBox::$staticItems[1];'); +"#, + ); + assert_eq!(out, "3:2:2:1:1,2,3:1:integer:42:2:b:1:21"); +} + +/// Verifies eval `call_user_func_array()` preserves ref-like builtin writeback aliases. +#[test] +fn test_eval_call_user_func_array_ref_like_builtin_callables_write_back_lvalues() { + let out = compile_and_run( + r#"items]) . ":" . implode(",", $box->items) . ":"; +$rsort = rsort(...); +echo call_user_func_array($rsort, [&EvalCallArrayRefLikeBuiltinBox::$staticItems]) . ":" . implode(",", EvalCallArrayRefLikeBuiltinBox::$staticItems) . ":"; +$set = settype(...); +echo call_user_func_array($set, ["var" => &$box->value, "type" => "integer"]) . ":" . gettype($box->value) . ":" . $box->value . ":"; +$string = "settype"; +echo call_user_func_array($string, [&EvalCallArrayRefLikeBuiltinBox::$staticValue, "bool"]) . ":" . gettype(EvalCallArrayRefLikeBuiltinBox::$staticValue) . ":" . (EvalCallArrayRefLikeBuiltinBox::$staticValue ? "true" : "false") . ":"; +$push = array_push(...); +echo call_user_func_array($push, [&$box->items, 4]) . ":" . $box->items[3];'); +"#, + ); + assert_eq!(out, "1:1,2,3:1:2,1:1:integer:42:1:boolean:false:4:4"); +} + +/// Verifies eval `array_splice()` mutates writable lvalue arguments. +#[test] +fn test_eval_dispatches_array_splice_builtin_call() { + let out = compile_and_run( + r#" 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +class EvalArraySplicePropertyBox { + public array $items = ["a", "b", "c"]; + public static array $staticItems = ["x", "y", "z"]; +} +$box = new EvalArraySplicePropertyBox(); +$propRemoved = array_splice($box->items, 1, 1, ["B"]); +echo count($propRemoved) . ":" . $propRemoved[0] . ":" . $box->items[1] . ":" . $box->items[2] . ":"; +$name = "items"; +$dynRemoved = array_splice($box->{$name}, 0, 1); +echo $dynRemoved[0] . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$staticRemoved = array_splice(EvalArraySplicePropertyBox::$staticItems, 1, 1); +echo $staticRemoved[0] . ":" . count(EvalArraySplicePropertyBox::$staticItems) . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +$class = "EvalArraySplicePropertyBox"; +$staticName = "staticItems"; +$dynStaticRemoved = array_splice($class::${$staticName}, 0, 1, ["w"]); +echo $dynStaticRemoved[0] . ":" . EvalArraySplicePropertyBox::$staticItems[0] . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +echo function_exists("array_splice");'); +"#, + ); + assert_eq!( + out, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1:b:B:c:a:2:B:y:2:z:x:w:z:1" + ); +} + +/// Verifies eval `sort()` and `rsort()` mutate writable lvalue arguments. +#[test] +fn test_eval_dispatches_sort_builtin_calls() { + let out = compile_and_run( + r#" 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +class EvalSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b", "a"]; +} +$box = new EvalSortPropertyBox(); +echo sort($box->items) . ":" . $box->items[0] . $box->items[1] . $box->items[2] . ":"; +$name = "items"; +echo rsort($box->{$name}) . ":" . $box->items[0] . ":" . $box->items[2] . ":"; +echo sort(EvalSortPropertyBox::$staticItems) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +$class = "EvalSortPropertyBox"; +$staticName = "staticItems"; +echo rsort($class::${$staticName}) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +echo function_exists("sort") && function_exists("rsort");'); +"#, + ); + assert_eq!( + out, + "1:123:1:cherry:apple:123:1:312:1:1:3:1:123:1:3:1:1:ab:1:ba:1" + ); +} + +/// Verifies eval key-preserving sort builtins mutate writable lvalue arguments. +#[test] +fn test_eval_dispatches_key_preserving_sort_builtin_calls() { + let out = compile_and_run( + r#" 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +class EvalKeySortPropertyBox { + public array $items = ["x" => 2, "y" => 1]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalKeySortPropertyBox(); +echo asort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +$name = "items"; +echo arsort($box->{$name}) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +echo ksort(EvalKeySortPropertyBox::$staticItems) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +$class = "EvalKeySortPropertyBox"; +$staticName = "staticItems"; +echo krsort($class::${$staticName}) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +echo function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");'); +"#, + ); + assert_eq!( + out, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1:y1x2:1:x2y1:1:a2b1:1:b1a2:1" + ); +} + +/// Verifies eval natural sort builtins preserve keys and use natural string order. +#[test] +fn test_eval_dispatches_natural_sort_builtin_calls() { + let out = compile_and_run( + r#" $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +class EvalNaturalSortPropertyBox { + public array $items = ["img10", "img2", "img1"]; + public static array $staticItems = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +} +$box = new EvalNaturalSortPropertyBox(); +echo natsort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$class = "EvalNaturalSortPropertyBox"; +$staticName = "staticItems"; +echo natcasesort($class::${$staticName}) . ":"; +foreach (EvalNaturalSortPropertyBox::$staticItems as $key => $value) { echo $key . $value . ";"; } +echo ":"; +echo function_exists("natsort") && function_exists("natcasesort");'); +"#, + ); + assert_eq!( + out, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1" + ); +} + +/// Verifies eval `shuffle()` reindexes writable array lvalues. +#[test] +fn test_eval_dispatches_shuffle_builtin_call() { + let out = compile_and_run( + r#" 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +class EvalShufflePropertyBox { + public array $items = ["x" => 1, "y" => 2]; + public static array $staticItems = ["a" => 3, "b" => 4]; +} +$box = new EvalShufflePropertyBox(); +echo shuffle($box->items) . ":" . (isset($box->items["x"]) ? "bad" : "prop") . ":" . count($box->items) . ":" . array_sum($box->items) . ":"; +$class = "EvalShufflePropertyBox"; +$staticName = "staticItems"; +echo shuffle($class::${$staticName}) . ":" . (isset(EvalShufflePropertyBox::$staticItems["a"]) ? "bad" : "static") . ":" . count(EvalShufflePropertyBox::$staticItems) . ":" . array_sum(EvalShufflePropertyBox::$staticItems) . ":"; +echo function_exists("shuffle");'); +"#, + ); + assert_eq!(out, "1:reindexed:2:3:1:12:1:prop:2:3:1:static:2:7:1"); +} + +/// Verifies eval user-comparator sort builtins call callbacks and mutate writable lvalues. +#[test] +fn test_eval_dispatches_user_sort_builtin_calls() { + let out = compile_and_run( + r#" $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +function eval_sort_quiet_cmp($left, $right) { return $left <=> $right; } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +class EvalUserSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalUserSortPropertyBox(); +echo usort($box->items, "eval_sort_quiet_cmp") . ":"; +foreach ($box->items as $value) { echo $value; } +echo ":"; +$name = "items"; +echo usort($box->{$name}, "eval_sort_quiet_cmp") . ":" . $box->items[0] . $box->items[2] . ":"; +$class = "EvalUserSortPropertyBox"; +$staticName = "staticItems"; +echo uksort($class::${$staticName}, "eval_key_cmp") . ":"; +foreach (EvalUserSortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +echo function_exists("usort") && function_exists("uasort") && function_exists("uksort");'); +"#, + ); + assert_eq!(out, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:1"); +} + +/// Verifies eval iterator array helpers dispatch through direct and dynamic calls. +#[test] +fn test_eval_dispatches_iterator_array_builtin_calls() { + let out = compile_and_run( + r#" 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +echo function_exists("iterator_count") && function_exists("iterator_to_array");'); +"#, + ); + assert_eq!(out, "2:12:reindexed:12:2:12:1"); +} + +/// Verifies eval `iterator_apply()` drives AOT Iterator objects through eval callbacks. +#[test] +fn test_eval_dispatches_iterator_apply_object_builtin() { + let out = compile_and_run( + r#"i = 0; $this->end = $end; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < $this->end; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +eval('function eval_apply_label($prefix) { echo $prefix; return true; } +$r = new EvalApplyRange(2); +echo iterator_apply($r, "eval_apply_label", ["prefix" => "E"]) . ":"; +echo call_user_func("iterator_apply", $r, "eval_apply_label", ["C"]);'); +"#, + ); + assert_eq!(out, "EE2:CC2"); +} + +/// Verifies eval `array_filter()` removes falsey values and preserves source keys. +#[test] +fn test_eval_dispatches_array_filter_builtin_call() { + let out = compile_and_run( + r#" 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; +echo function_exists("array_filter");'); +"#, + ); + assert_eq!(out, "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:1"); +} + +/// Verifies eval `array_combine()` supports PHP key conversions and callable dispatch. +#[test] +fn test_eval_dispatches_array_combine_builtin_call() { + let out = compile_and_run( + r#" "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +echo function_exists("array_column");'); +"#, + ); + assert_eq!(out, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:1"); +} + +/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. +#[test] +fn test_eval_dispatches_array_shape_builtin_calls() { + let out = compile_and_run( + r#" 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +echo function_exists("array_merge");'); +"#, + ); + assert_eq!(out, "4:1234:2:1:3:9:x:y:3:8:10:1"); +} + +/// Verifies eval `array_diff()` and `array_intersect()` preserve left keys and compare by string value. +#[test] +fn test_eval_dispatches_array_value_set_builtin_calls() { + let out = compile_and_run( + r#" 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +echo function_exists("array_diff"); echo function_exists("array_intersect");'); +"#, + ); + assert_eq!(out, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:11"); +} + +/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. +#[test] +fn test_eval_dispatches_array_key_set_builtin_calls() { + let out = compile_and_run( + r#" 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +echo function_exists("array_diff_key"); echo function_exists("array_intersect_key");'); +"#, + ); + assert_eq!(out, "2:2:3:no-a:2:2:3:2:1030:1:8:11"); +} + +/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. +#[test] +fn test_eval_dispatches_range_builtin_call() { + let out = compile_and_run( + r#"= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +echo function_exists("array_rand");'); +"#, + ); + assert_eq!(out, "idx:assoc:named:call:spread:1"); +} + +/// Verifies eval random builtins produce values in their PHP-visible ranges. +#[test] +fn test_eval_dispatches_rand_builtin_calls() { + let out = compile_and_run( + r#"= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +echo function_exists("rand"); echo function_exists("mt_rand"); echo function_exists("random_int");'); +"#, + ); + assert_eq!( + out, + "plain:range:same:swap:call:spread:random:random-call:random-spread:111" + ); +} + +/// Verifies eval `spl_classes()` exposes the same static SPL class list as native code. +#[test] +fn test_eval_dispatches_spl_classes_builtin_calls() { + let out = compile_and_run( + r#"push("a"); +$list->push("b"); +echo count($list) . ":" . $list->bottom() . ":" . $list->top() . ":"; +foreach ($list as $value) { + echo $value; +} +$stack = new SplStack(); +$stack->push("s"); +echo ":" . count($stack) . $stack->pop(); +$queue = new SplQueue(); +$queue->enqueue("q"); +echo ":" . count($queue) . $queue->dequeue(); +$emptyFixed = new SplFixedArray(); +echo ":" . $emptyFixed->getSize(); +$fixed = new SplFixedArray(2); +$fixed->offsetSet(0, "x"); +echo ":" . $fixed->getSize() . $fixed->offsetGet(0); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:a:b:ab:1s:1q:0:2x"); +} + +/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. +#[test] +fn test_eval_dispatches_array_fill_builtin_calls() { + let out = compile_and_run( + r#" "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +echo function_exists("array_flip");'); +"#, + ); + assert_eq!(out, "c:b:d:e:4:k:left:n:1"); +} + +/// Verifies eval `array_unique()` preserves keys and supports callable dispatch. +#[test] +fn test_eval_dispatches_array_unique_builtin_call() { + let out = compile_and_run( + r#" "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +echo function_exists("array_unique");'); +"#, + ); + assert_eq!(out, "3:ab2:2:ab:2:1:F:v:1:qr:st:1"); +} + +/// Verifies eval array projection builtins return indexed key/value arrays. +#[test] +fn test_eval_dispatches_array_projection_builtin_calls() { + let out = compile_and_run( + r#" 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); echo function_exists("array_values");'); +"#, + ); + assert_eq!(out, "10:20:a:b:0:z:8:11"); +} + +/// Verifies eval `array_reverse()` supports key rules, named args, and callable dispatch. +#[test] +fn test_eval_dispatches_array_reverse_builtin_call() { + let out = compile_and_run( + r#" "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +echo function_exists("array_reverse");'); +"#, + ); + assert_eq!(out, "321:cba:cba:yx:54:76:1"); +} + +/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. +#[test] +fn test_eval_dispatches_array_key_exists_builtin_calls() { + let out = compile_and_run( + r#" null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; +echo ":"; echo function_exists("array_key_exists");'); +"#, + ); + assert_eq!(out, "Y:N:Y:N:Y:Y:1"); +} + +/// Verifies eval array search builtins return booleans or matching keys. +#[test] +fn test_eval_dispatches_array_search_builtin_calls() { + let out = compile_and_run( + r#" "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); echo function_exists("array_search");'); +"#, + ); + assert_eq!(out, "Y:N:1:name:F:C:k:11"); +} + +/// Verifies eval ASCII case-conversion builtins work directly and by callable dispatch. +#[test] +fn test_eval_dispatches_string_case_builtin_calls() { + let out = compile_and_run( + r#" "a-b", "separators" => "-"]); +echo ":"; echo function_exists("ucwords");'); +"#, + ); + assert_eq!(out, "Hello World:Hello-World:Hello\tWorld:A B:A-B:1"); +} + +/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. +#[test] +fn test_eval_dispatches_wordwrap_builtin_call() { + let out = compile_and_run( + r#""); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; echo function_exists("wordwrap");'); +"#, + ); + assert_eq!( + out, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:1" + ); +} + +/// Verifies eval `strrev()` reverses byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_strrev_builtin_call() { + let out = compile_and_run( + r#" 321]); +echo ":"; echo function_exists("chr");'); +"#, + ); + assert_eq!(out, "A:00:ff:A:1"); +} + +/// Verifies eval `str_repeat()` repeats byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_str_repeat_builtin_call() { + let out = compile_and_run( + r#" "z", "times" => 3]); +echo ":"; echo function_exists("str_repeat");'); +"#, + ); + assert_eq!(out, "hahaha:0:abab:zzz:1"); +} + +/// Verifies eval `substr()` slices byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_substr_builtin_call() { + let out = compile_and_run( + r#" "abcdef", "offset" => -4, "length" => 2]); +echo ":"; echo function_exists("substr");'); +"#, + ); + assert_eq!(out, "cdef:bcde:ef:cd:cd:1"); +} + +/// Verifies eval `substr_replace()` replaces selected byte ranges through callable paths. +#[test] +fn test_eval_dispatches_substr_replace_builtin_call() { + let out = compile_and_run( + r#" "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); +echo ":"; echo function_exists("substr_replace");'); +"#, + ); + assert_eq!(out, "hello PHP:aXf:abcdX:abcdefX:Xcdef:1"); +} + +/// Verifies eval `nl2br()` preserves newline bytes while inserting HTML breaks. +#[test] +fn test_eval_dispatches_nl2br_builtin_call() { + let out = compile_and_run( + r#" "a\n\rb", "use_xhtml" => false])); +echo ":"; echo function_exists("nl2br");'); +"#, + ); + assert_eq!( + out, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:1" + ); +} + +/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. +#[test] +fn test_eval_dispatches_explode_implode_builtin_calls() { + let out = compile_and_run( + r#" "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +echo ":"; echo function_exists("implode");'); +"#, + ); + assert_eq!(out, "3:a:b::a|b|:x-2-1-:n:p/q:1:1"); +} + +/// Verifies eval `str_split()` builds indexed chunk arrays. +#[test] +fn test_eval_dispatches_str_split_builtin_call() { + let out = compile_and_run( + r#" "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; +echo ":"; echo function_exists("str_split");'); +"#, + ); + assert_eq!(out, "3:abc:ab-cd:0:xy-z:pqr-s:1"); +} + +/// Verifies eval `str_pad()` supports all PHP pad modes and callable dispatch. +#[test] +fn test_eval_dispatches_str_pad_builtin_call() { + let out = compile_and_run( + r#" "x", "length" => 3, "pad_string" => "."]); +echo ":"; echo function_exists("str_pad");'); +"#, + ); + assert_eq!(out, "[hi ]:[___hi]:[abxaba]:00042:x..:1"); +} + +/// Verifies eval string replacement builtins support direct and callable dispatch. +#[test] +fn test_eval_dispatches_string_replace_builtin_calls() { + let out = compile_and_run( + r#" "x", "replace" => "Y", "subject" => "xX"]); +echo ":"; echo function_exists("str_replace"); +echo ":"; echo function_exists("str_ireplace");'); +"#, + ); + assert_eq!(out, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1:1"); +} + +/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. +#[test] +fn test_eval_dispatches_html_entity_builtin_calls() { + let out = compile_and_run( + r#"\"Hi\" & \'bye\'"); echo ":"; +echo htmlentities(string: "
"); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); +echo ":"; echo function_exists("htmlspecialchars"); +echo ":"; echo function_exists("htmlentities"); +echo ":"; echo function_exists("html_entity_decode");'); +"#, + ); + assert_eq!( + out, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":1:1:1" + ); +} + +/// Verifies eval URL codec builtins encode, decode, and dispatch as callables. +#[test] +fn test_eval_dispatches_url_codec_builtin_calls() { + let out = compile_and_run( + r#" "x%2By%zz"]); +echo ":"; echo function_exists("urlencode"); +echo ":"; echo function_exists("rawurlencode"); +echo ":"; echo function_exists("urldecode"); +echo ":"; echo function_exists("rawurldecode");'); +"#, + ); + assert_eq!( + out, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:1:1:1:1" + ); +} + +/// Verifies eval `ctype_*` predicates inspect ASCII byte classes. +#[test] +fn test_eval_dispatches_ctype_builtin_calls() { + let out = compile_and_run( + r#" " x"]) ? "bad" : "not-space"; +echo ":"; echo function_exists("ctype_alpha"); +echo ":"; echo function_exists("ctype_digit"); +echo ":"; echo function_exists("ctype_alnum"); +echo ":"; echo function_exists("ctype_space");'); +"#, + ); + assert_eq!(out, "A:D:N:S:empty:not-digit:not-space:1:1:1:1"); +} + +/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. +#[test] +fn test_eval_dispatches_crc32_builtin_call() { + let out = compile_and_run( + r#" "The quick brown fox jumps over the lazy dog"]); +echo ":"; echo function_exists("crc32");'); +"#, + ); + assert_eq!(out, "0:3421780262:907060870:1095738169:1"); +} + +/// Verifies eval `hash_algos()` exposes the native supported hash algorithm list. +#[test] +fn test_eval_dispatches_hash_algos_builtin_call() { + let out = compile_and_run( + r#" "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +file_put_contents("eval-hash-file.txt", "abc"); +echo hash_file("sha256", "eval-hash-file.txt"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "eval-hash-file.txt", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "eval-hash-file.txt"]); echo ":"; +echo hash_file("sha256", "eval-hash-file.txt.missing") === false ? "missing" : "bad"; echo ":"; +unlink("eval-hash-file.txt"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); echo function_exists("hash_hmac");'); +"#, + ); + assert_eq!( + out, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "11111" + ) + ); +} + +/// Verifies eval zero-argument system builtins match native runtime conventions. +#[test] +fn test_eval_dispatches_zero_arg_system_builtin_calls() { + let out = compile_and_run( + r#" 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +echo function_exists("sys_get_temp_dir");'); +"#, + ); + assert_eq!( + out, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:1111", + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_VERSION") + ) + ); +} + +/// Verifies eval `date()` formats timestamps and `mktime()` creates them. +#[test] +fn test_eval_dispatches_date_mktime_builtin_calls() { + let out = compile_and_run( + r#" 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); echo function_exists("mktime");'); +"#, + ); + assert_eq!( + out, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:11" + ); +} + +/// Verifies eval function probes recognize the DateTime/calendar aliases that static elephc +/// desugars before codegen. +#[test] +fn test_eval_function_probes_date_procedural_aliases() { + let out = compile_and_run( + r#" "2024-01-02"]); +echo ":" . date("Y-m-d", $spread) . ":"; +echo function_exists("strtotime");'); +"#, + ); + assert_eq!( + out, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:1" + ); +} + +/// Verifies eval `microtime()` returns a plausible floating timestamp by all call paths. +#[test] +fn test_eval_dispatches_microtime_builtin_call() { + let out = compile_and_run( + r#" 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; echo function_exists("microtime");'); +"#, + ); + assert_eq!(out, "now:named:call:array:1"); +} + +/// Verifies eval realpath-cache builtins expose elephc's empty-cache convention. +#[test] +fn test_eval_dispatches_realpath_cache_builtin_calls() { + let out = compile_and_run( + r#" "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +echo function_exists("putenv");'); +"#, + ); + assert_eq!(out, "direct:named:named:set:spread:empty:11"); +} + +/// Verifies eval sleep builtins dispatch through direct, named, and callable paths. +#[test] +fn test_eval_dispatches_sleep_builtin_calls() { + let out = compile_and_run( + r#" 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +echo function_exists("usleep");'); +"#, + ); + assert_eq!(out, "0:0:u:0:null:11"); +} + +/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. +#[test] +fn test_eval_dispatches_php_uname_builtin_call() { + let out = compile_and_run( + r#" 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +echo function_exists("php_uname");'); +"#, + ); + assert_eq!( + out, + "all:same:sys:node:release:version:machine:call:spread:1" + ); +} + +/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. +#[test] +fn test_eval_dispatches_gethostbyname_builtin_call() { + let out = compile_and_run( + r#" "not a host"]) . ":"; +echo function_exists("gethostbyname");'); +"#, + ); + assert_eq!(out, "127.0.0.1:not a host:127.0.0.1:not a host:1"); +} + +/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. +#[test] +fn test_eval_dispatches_gethostname_builtin_call() { + let out = compile_and_run( + r#" 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +echo function_exists("gethostname");'); +"#, + ); + assert_eq!(out, "host:call:spread:1"); +} + +/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. +#[test] +fn test_eval_dispatches_gethostbyaddr_builtin_call() { + let out = compile_and_run( + r#" 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +echo function_exists("gethostbyaddr");'); +"#, + ); + assert_eq!(out, "direct:named:false:call:spread:1"); +} + +/// Verifies eval protocol and service database lookups dispatch dynamically. +#[test] +fn test_eval_dispatches_protocol_service_builtin_calls() { + let out = compile_and_run( + r#" 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); echo function_exists("getservbyport");'); +"#, + ); + assert_eq!( + out, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:1111" + ); +} + +/// Verifies eval stream introspection builtins return native-compatible static lists. +#[test] +fn test_eval_dispatches_stream_introspection_builtin_calls() { + let out = compile_and_run( + r#" $tmp]) ? "calllock" : "bad"; echo ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); echo function_exists("stream_get_filters"); +echo function_exists("stream_is_local"); echo function_exists("stream_supports_lock");'); +"#, + ); + assert_eq!( + out, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:local:lock:calllocal:calllock:11111" + ); +} + +/// Verifies eval IPv4 conversion builtins handle integer, string, and raw-byte forms. +#[test] +fn test_eval_dispatches_ip_conversion_builtin_calls() { + let out = compile_and_run( + r#" "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); echo function_exists("inet_ntop");'); +"#, + ); + assert_eq!( + out, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:1111" + ); +} + +/// Verifies eval `basename()` and `dirname()` preserve static path edge-case behavior. +#[test] +fn test_eval_dispatches_path_component_builtin_calls() { + let out = compile_and_run( + r#" "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); echo function_exists("dirname");'); +"#, + ); + assert_eq!( + out, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:11" + ); +} + +/// Verifies eval `realpath()` returns strings for existing paths and false for missing paths. +#[test] +fn test_eval_dispatches_realpath_builtin_call() { + let out = compile_and_run( + r#" "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; echo function_exists("realpath");'); +"#, + ); + assert_eq!(out, "resolved:false:call:array-false:1"); +} + +/// Verifies eval `stream_resolve_include_path()` dispatches directly and dynamically. +#[test] +fn test_eval_dispatches_stream_resolve_include_path_builtin_call() { + let out = compile_and_run( + r#" "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; echo function_exists("stream_resolve_include_path");'); +"#, + ); + assert_eq!(out, "resolved:false:call:array-false:1"); +} + +/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. +#[test] +fn test_eval_dispatches_preg_builtin_calls() { + let out = compile_and_run( + r#" "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +class EvalPregMatchesBox { + public array $matches = []; + public static array $staticMatches = []; +} +$box = new EvalPregMatchesBox(); +preg_match("/([a-z]+)([0-9]+)/", "ab12", $box->matches); +echo $box->matches[0] . ":" . $box->matches[1] . ":" . $box->matches[2] . ":"; +$name = "matches"; +preg_match_all("/([a-z])([0-9])/", "a1 b2", $box->{$name}, PREG_SET_ORDER); +echo count($box->matches) . ":" . $box->matches[1][0] . ":" . $box->matches[1][2] . ":"; +preg_match("/([A-Z]+)/", "ID", EvalPregMatchesBox::$staticMatches); +echo EvalPregMatchesBox::$staticMatches[0] . ":"; +$class = "EvalPregMatchesBox"; +$staticName = "staticMatches"; +preg_match_all("/([a-z])/", "xy", $class::${$staticName}); +echo count(EvalPregMatchesBox::$staticMatches[0]) . ":" . EvalPregMatchesBox::$staticMatches[0][1] . ":"; +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");'); +"#, + ); + assert_eq!( + out, + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:ab12:ab:12:2:b2:2:ID:2:y:1" + ); +} + +/// Verifies eval `preg_replace_callback()` accepts general callable forms. +#[test] +fn test_eval_preg_replace_callback_accepts_general_callables() { + let out = compile_and_run( + r#"prefix = $prefix; } + public function wrap($matches) { return $this->prefix . $matches[0]; } + public static function wrapStatic($matches) { return "S" . $matches[0]; } +} +$box = new EvalPregCallbackBox("O"); +echo preg_replace_callback("/[A-Z]/", [$box, "wrap"], "AB") . ":"; +echo preg_replace_callback("/[0-9]/", "EvalPregCallbackBox::wrapStatic", "12") . ":"; +echo preg_replace_callback("/[C]/", ["EvalPregCallbackBox", "wrapStatic"], "CC") . ":"; +$first = $box->wrap(...); +echo preg_replace_callback("/[a-z]/", $first, "xy") . ":"; +$static = EvalPregCallbackBox::wrapStatic(...); +return preg_replace_callback("/[m]/", $static, "mm");'); +"#, + ); + assert_eq!(out, "OAOB:S1S2:SCSC:OxOy:SmSm"); +} + +/// Verifies dynamic eval preg callables write by-reference `$matches` arrays. +#[test] +fn test_eval_dynamic_preg_callables_write_matches_by_ref() { + let out = compile_and_run( + r#" "/y/", "subject" => "y", "matches" => $named]) . ":" . $named[0] . "|"; +$flagged = ["old"]; +echo call_user_func("preg_match_all", "/([a-z])/", "ab", $flagged, PREG_SET_ORDER) . ":" . $flagged[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:old|2:old|1:old|2:old"); + for warning in [ + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. +#[test] +fn test_eval_dispatches_fnmatch_builtin_call() { + let out = compile_and_run( + r#" "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD");'); +"#, + ); + assert_eq!( + out, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); +} + +/// Verifies eval `basename()` and `dirname()` support defaults, named call arrays, and function probes. +#[test] +fn test_eval_dispatches_basename_dirname_builtin_calls() { + let out = compile_and_run( + r#" "/a/b/c", "levels" => 2]) . ":"; +echo function_exists("basename") && function_exists("dirname");'); +"#, + ); + assert_eq!(out, "syslog:/usr/local:file.txt:/a:1"); +} + +/// Verifies eval `pathinfo()` supports arrays, component flags, constants, and callables. +#[test] +fn test_eval_dispatches_pathinfo_builtin_call() { + let out = compile_and_run( + r#" "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL");'); +"#, + ); + assert_eq!( + out, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); +} + +/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. +#[test] +fn test_eval_dispatches_filesystem_builtin_calls() { + let out = compile_and_run( + r#" "eval-fs.txt"]) . ":"; +echo unlink("eval-fs.txt") ? "unlinked" : "bad"; echo ":"; +echo file_exists("eval-fs.txt") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); echo function_exists("unlink");'); +"#, + ); + assert_eq!( + out, + "5:hello:exists:file:dir:readable:writable:writeable:5:call-exists:5:unlinked:gone:1111111111" + ); +} + +/// Verifies dynamic eval `flock()` callables write the by-reference `$would_block` output. +#[test] +fn test_eval_dynamic_flock_callables_write_would_block_by_ref() { + let out = compile_and_run( + r#" 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-magician") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; +echo ":"; echo function_exists("disk_free_space"); echo function_exists("disk_total_space");'); +"#, + ); + assert_eq!(out, "free:total:ordered:missing:call:spread:11"); +} + +/// Verifies eval stat metadata builtins return scalar metadata and dispatch dynamically. +#[test] +fn test_eval_dispatches_stat_metadata_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "mtime" : "bad"; echo ":"; +echo fileatime(filename: "eval-stat.txt") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("eval-stat.txt") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("eval-stat.txt") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("eval-stat.txt") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("eval-stat.txt") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("eval-stat.txt") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("eval-stat.txt") . ":"; +echo filetype(".") . ":"; +echo is_executable("/bin/sh") ? "exec" : "bad"; echo ":"; +echo is_link("eval-stat.txt") ? "bad" : "notlink"; echo ":"; +echo fileatime("missing-stat.txt") === false ? "missing-atime" : "bad"; echo ":"; +echo filetype("missing-stat.txt") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("missing-stat.txt") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "eval-stat.txt") . ":"; +echo call_user_func_array("fileinode", ["filename" => "eval-stat.txt"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +unlink("eval-stat.txt");'); +"#, + ); + assert_eq!( + out, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:exec:notlink:missing-atime:missing-type:missing-mtime:file:callinode:1111111111" + ); +} + +/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. +#[test] +fn test_eval_dispatches_stat_array_builtin_calls() { + let out = compile_and_run( + r#" "eval-lstat-array.txt"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("eval-lstat-array.txt") && unlink("eval-stat-array.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +'); +"#, + ); + assert_eq!(out, "stat:mode:lstat:missing:callstat:calllstat:cleanup:11"); +} + +/// Verifies eval path operation builtins mutate local filesystem state. +#[test] +fn test_eval_dispatches_path_operation_builtin_calls() { + let out = compile_and_run( + r#"= 0 ? "linkinfo" : "bad"; echo ":"; +echo link("eval-op-src.txt", "eval-op-hard.txt") ? "hardlink" : "bad"; echo ":"; +echo readlink("eval-op-src.txt") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("eval-op-missing.txt") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo chdir("eval-op-dir") ? "chdir" : "bad"; echo ":"; +echo getcwd() !== "" ? "cwd" : "bad"; echo ":"; +chdir(".."); +echo clearstatcache(true, "eval-op-src.txt") === null ? "cache" : "bad"; echo ":"; +echo unlink("eval-op-link.txt") && unlink("eval-op-hard.txt") && unlink("eval-op-moved.txt") && unlink("eval-op-src.txt") && rmdir("eval-op-dir") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "eval-op-call-dir") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "eval-op-call-dir"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +'); +"#, + ); + assert_eq!( + out, + "mkdir:copy:rename:symlink:readlink:linkinfo:hardlink:readlink-false:linkinfo-missing:chdir:cwd:cache:cleanup:callmkdir:callrmdir:111111111" + ); +} + +/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. +#[test] +fn test_eval_dispatches_file_listing_builtin_calls() { + let out = compile_and_run( + r#" "eval-list-dir"]); +echo count($call_scan) . ":"; +echo unlink("eval-list-dir/a.txt") && unlink("eval-list-dir/b.txt") && rmdir("eval-list-dir") && unlink("eval-lines.txt") && unlink("eval-empty.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +'); +"#, + ); + assert_eq!( + out, + "2:line0:line1:[]0:missing-readfile:4:scan:callfile:4:cleanup:111" + ); +} + +/// Verifies eval directory resource builtins dispatch directly and dynamically. +#[test] +fn test_eval_dispatches_directory_resource_builtin_calls() { + let out = compile_and_run( + r#" $call]) === null ? "callclose" : "bad"; echo ":"; +echo unlink("eval-dir-handle/a.txt") && rmdir("eval-dir-handle") ? "cleanup" : "bad"; echo ":"; +echo function_exists("opendir"); echo function_exists("readdir"); echo function_exists("rewinddir"); echo function_exists("closedir"); +'); +"#, + ); + assert_eq!(out, "iter:callread:callrewind:callclose:cleanup:1111"); +} + +/// Verifies eval `glob()` expands local patterns and dispatches dynamically. +#[test] +fn test_eval_dispatches_glob_builtin_calls() { + let out = compile_and_run( + r#" "eval-glob-dir/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("eval-glob-dir/.hidden.txt"); +unlink("eval-glob-dir/c.txt"); +unlink("eval-glob-dir/b.log"); +unlink("eval-glob-dir/a.txt"); +echo rmdir("eval-glob-dir") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +'); +"#, + ); + assert_eq!( + out, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); +} + +/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. +#[test] +fn test_eval_dispatches_file_modify_builtin_calls() { + let out = compile_and_run( + r#" "eval-missing-call-group.txt", "group" => 1000]) ? "bad" : "callchgrp-false"; echo ":"; +$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "evc"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "evc") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("eval-mod.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("chown"); echo function_exists("chgrp"); +echo function_exists("lchown"); echo function_exists("lchgrp"); echo function_exists("tempnam"); +echo function_exists("umask"); +'); +"#, + ); + assert_eq!( + out, + "chmod:mode:chmod-false:chown-false:chgrp-false:lchown-false:lchgrp-false:tempnam:umask:probe:callchmod:callchown-false:callchgrp-false:calltempnam:cleanup:1111111" + ); +} + +/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. +#[test] +fn test_eval_dispatches_touch_builtin_calls() { + let out = compile_and_run( + r#" "eval-touch-stamped.txt", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("eval-touch-created.txt") && unlink("eval-touch-stamped.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +'); +"#, + ); + assert_eq!( + out, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); +} + +/// Verifies eval process-pipe and temporary stream builtins dispatch dynamically. +#[test] +fn test_eval_dispatches_process_pipe_and_tmpfile_builtin_calls() { + let out = compile_and_run( + r#" "printf q", "mode" => "r"]); +echo fread($callPipe, 1) . ":"; +echo call_user_func("pclose", $callPipe) . ":"; +echo function_exists("tmpfile"); echo function_exists("popen"); echo function_exists("pclose");'); +"#, + ); + assert_eq!(out, "tmpfile:3:abc:xyz:0:calltmp:q:0:111"); +} + +/// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_bin2hex_builtin_call() { + let out = compile_and_run( + r#" "ok"]); +echo ":"; echo function_exists("bin2hex");'); +"#, + ); + assert_eq!(out, "417a:410a:5c6e:415c71:410b1b0c:213f:6f6b:1"); +} + +/// Verifies eval `hex2bin()` decodes hex strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_hex2bin_builtin_call() { + let out = compile_and_run( + r#" "6f6b"]); +echo ":"; echo function_exists("hex2bin");'); +"#, + ); + assert_eq!(out, "Az:410a:!?:ok:1"); +} + +/// Verifies eval `addslashes()` and `stripslashes()` use PHP byte escaping semantics. +#[test] +fn test_eval_dispatches_slash_escape_builtin_calls() { + let out = compile_and_run( + r#" ""]); +echo ":"; echo function_exists("base64_encode");'); +"#, + ); + assert_eq!(out, "SGVsbG8=:SGk=:VGVzdCAxMjMh::1"); +} + +/// Verifies eval `base64_decode()` decodes byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_base64_decode_builtin_call() { + let out = compile_and_run( + r#" ""]); +echo ":"; echo function_exists("base64_decode");'); +"#, + ); + assert_eq!(out, "Hello:Hi:Test 123!::1"); +} + +/// Verifies eval `str_contains()` supports direct and callable byte-string search. +#[test] +fn test_eval_dispatches_str_contains_builtin_call() { + let out = compile_and_run( + r#" "abcabc", "needle" => "bc", "before_needle" => true]); +echo ":"; echo function_exists("strstr");'); +"#, + ); + assert_eq!(out, "@example.com:hel:F:hello:bcabc:a:1"); +} + +/// Verifies eval string boundary builtins support direct and callable byte-string checks. +#[test] +fn test_eval_dispatches_string_boundary_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); echo function_exists("hash_equals");'); +"#, + ); + assert_eq!(out, "0:lt:0:gt:ci:heq:hlen:hneq:111"); +} + +/// Verifies eval trim-like builtins strip default and explicit masks. +#[test] +fn test_eval_dispatches_trim_like_builtin_calls() { + let out = compile_and_run( + r#"i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } + public function valid(): bool { return $this->i < 0; } + public function rewind(): void { $this->i = 0; } +} +$iterator = new EvalAotPredicateIterator(); +eval('echo is_int(1); echo is_integer(1); echo is_long(1); +echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); +echo is_string("x"); echo is_bool(false); echo is_null(null); +echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable($iterator) ? "I" : "bad"; +echo is_iterable(1) ? "bad" : "T"; +echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; +echo is_resource(1) ? "bad" : "R"; +$object = json_decode("{}"); +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; +echo is_resource($h) ? "H" : "bad"; +echo ":"; +echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func("is_iterable", $iterator) ? "C" : "bad"; +echo call_user_func_array("is_iterable", ["value" => $iterator]) ? "D" : "bad"; +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; +echo call_user_func("is_resource", $h); +echo call_user_func_array("is_resource", [$h]); +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; +echo call_user_func("is_nan", fdiv(0, 0)) ? "N" : "bad"; +echo call_user_func_array("is_finite", [42]) ? "F" : "bad"; +echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); +echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_iterable"); echo function_exists("is_infinite");'); +"#, + ); + assert_eq!( + out, + "1111111111111ITok11111NBROoNIiFfH:1111CDt11OoNF11111111" + ); +} + +/// Verifies eval resource introspection builtins inspect boxed runtime resources. +#[test] +fn test_eval_dispatches_resource_introspection_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "id" : "bad"; +echo ":"; echo call_user_func("get_resource_type", $h); +echo ":"; echo call_user_func_array("get_resource_id", ["resource" => $h]) > 0 ? "id" : "bad"; +echo ":"; echo function_exists("get_resource_type"); echo function_exists("get_resource_id");'); +"#, + ); + assert_eq!(out, "stream:id:stream:id:11"); +} + +/// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. +#[test] +fn test_eval_dispatches_cast_builtin_calls() { + let out = compile_and_run( + r#"name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalStringableBox(); +echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +echo $box->accepts($box);'); +"#, + ); + assert_eq!( + out, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:typed:box:Ada" + ); +} + +/// Verifies eval-declared objects support nullsafe property reads and method calls. +#[test] +fn test_eval_declared_nullsafe_property_and_method_access() { + let out = compile_and_run( + r#"name . ":" . $value; + } +} + +class EvalNullsafeUser { + public $profile = null; +} + +function eval_nullsafe_side() { + echo "bad"; + return "side"; +} + +$with = new EvalNullsafeUser(); +$with->profile = new EvalNullsafeProfile(); +$without = new EvalNullsafeUser(); + +echo $with->profile?->name ?? "none"; echo "|"; +echo $without->profile?->name ?? "none"; echo "|"; +echo $with?->profile?->label("ok") ?? "none"; echo "|"; +echo $without?->profile?->label(eval_nullsafe_side()) ?? "none";'); +"#, + ); + assert_eq!(out, "Ada|none|method:Ada:ok|none"); +} + +/// Verifies eval-declared objects support runtime-name property reads and method calls. +#[test] +fn test_eval_declared_dynamic_property_and_method_access() { + let out = compile_and_run( + r#"name . ":" . $value; + } +} + +class EvalDynamicMemberUser { + public $profile = null; +} + +function eval_dynamic_member_name() { + echo "name:"; + return "profile"; +} + +function eval_dynamic_member_method() { + echo "methodName:"; + return "label"; +} + +function eval_dynamic_member_bad() { + echo "bad"; + return "profile"; +} + +$with = new EvalDynamicMemberUser(); +$with->profile = new EvalDynamicMemberProfile(); +$missing = null; +$name = "profile"; +$method = "label"; + +echo $with->{$name}->name; echo "|"; +echo $with?->{eval_dynamic_member_name()}?->name ?? "none"; echo "|"; +echo $with->{$name}->$method("ok"); echo "|"; +echo $with->{$name}->{eval_dynamic_member_method()}("yes"); echo "|"; +echo $missing?->{eval_dynamic_member_bad()} ?? "none"; echo "|"; +echo $missing?->{eval_dynamic_member_bad()}(eval_dynamic_member_bad()) ?? "none";'); +"#, + ); + assert_eq!( + out, + "Ada|name:Ada|call:Ada:ok|methodName:call:Ada:yes|none|none" + ); +} + +/// Verifies eval-declared objects support runtime-name property writes and probes. +#[test] +fn test_eval_declared_dynamic_property_write_unset_isset_empty() { + let out = compile_and_run( + r#"{eval_dynamic_write_name()} = eval_dynamic_write_value(); +echo $box->name; echo "|"; +echo isset($box->{$name}) ? "set" : "bad"; echo "|"; +echo empty($box->{$blank}) ? "empty" : "bad"; echo "|"; +unset($box->{$name}); +echo isset($box->{$name}) ? "bad" : "unset"; echo "|"; +$missing = null; +echo isset($missing?->{eval_dynamic_write_bad()}) ? "bad" : "nullset"; echo "|"; +echo empty($missing?->{eval_dynamic_write_bad()}) ? "nullempty" : "bad";'); +"#, + ); + assert_eq!(out, "name:value:Ada|set|empty|unset|nullset|nullempty"); +} + +/// Verifies eval-declared object properties can bind to local variables by reference. +#[test] +fn test_eval_declared_property_reference_binding() { + let out = compile_and_run( + r#"value =& $source; +$source = "B"; +echo $box->value . "|"; +$box->value = "C"; +echo $source . "|"; + +$name = "other"; +$dynamic = "D"; +$box->{$name} =& $dynamic; +$dynamic = "E"; +echo $box->other . "|"; +$box->{$name} = "F"; +echo $dynamic;'); +"#, + ); + assert_eq!(out, "B|C|E|F"); +} + +/// Verifies eval object property increment/decrement works with named and dynamic properties. +#[test] +fn test_eval_object_property_inc_dec_statements() { + let out = compile_and_run_capture( + r#"count++; +++$box->count; +$name = "count"; +$box->{$name}++; +--$box->{$name}; +++$box->{$name}; +$box->{eval_property_inc_name()}++; +--$box->{eval_property_inc_name()}; +$i = 0; +for (; $i < 3; $box->count++) { + $i++; +} +echo $box->count; echo "|"; +$aot = new EvalAotPropertyIncDec(); +$aot->count++; +++$aot->count; +$aot->count--; +--$aot->count; +echo $aot->count;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|n|7|10"); +} + +/// Verifies eval object property compound assignments work for named and dynamic properties. +#[test] +fn test_eval_object_property_compound_assignment_statements() { + let out = compile_and_run_capture( + r#"count += 2; +$box->count *= 3; +$box->label .= "y"; +$name = "count"; +$box->{$name} -= 1; +$box->{eval_property_compound_name()} += eval_property_compound_rhs(); +echo $box->count; echo ":"; echo $box->label; echo "|"; +$aot = new EvalAotPropertyCompoundAssign(); +$aot->count += 5; +$aot->count %= 6; +$aot->label .= "b"; +echo $aot->count; echo ":"; echo $aot->label;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|r|12:xy|3:ab"); +} + +/// Verifies eval object property array writes and appends update property storage. +#[test] +fn test_eval_object_property_array_write_statements() { + let out = compile_and_run_capture( + r#"items[0] = "a"; +$box->items[] = "b"; +$box->items[0] .= "A"; +$name = "dyn"; +$box->{$name}[1] = "x"; +$box->{$name}[] = "y"; +$box->{eval_property_array_name()}[] = "z"; +echo $box->items[0] . ":" . $box->items[1] . ":"; +echo $box->dyn[1] . ":" . $box->dyn[2] . ":" . $box->dyn[3] . "|"; +$aot = new EvalAotPropertyArrayWrite(); +$aot->items[0] = "m"; +$aot->items[] = "n"; +$aot->items[0] .= "M"; +echo $aot->items[0] . ":" . $aot->items[1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|aA:b:x:y:z|mM:n"); +} + +/// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. +#[test] +fn test_eval_aot_tostring_string_contexts() { + let out = compile_and_run( + r#"name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalAotStringableBox(); +eval('echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +echo $box->accepts($box);'); +"#, + ); + assert_eq!( + out, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:typed:box:Ada" + ); +} + +/// Verifies eval-declared objects without `__toString()` throw catchable PHP errors in string contexts. +#[test] +fn test_eval_declared_object_string_context_without_tostring_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Object of class EvalPlainStringContext could not be converted to string" + ); +} + +/// Verifies AOT objects stringified from eval throw PHP's catchable conversion error. +#[test] +fn test_eval_aot_object_string_context_without_tostring_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Object of class EvalAotPlainStringContext could not be converted to string" + ); +} + +/// Verifies eval `settype()` mutates direct variables and supports named arguments. +#[test] +fn test_eval_dispatches_settype_builtin_call() { + let out = compile_and_run( + r#" "6"]; +echo settype($items["k"], "integer") ? gettype($items["k"]) . ":" . $items["k"] : "bad"; +echo ":"; +$box = new EvalAotSettypeBox(); +echo settype($box->value, "string") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +$name = "value"; +echo settype($box->{$name}, "integer") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +echo settype(EvalAotSettypeBox::$staticValue, "string") ? gettype(EvalAotSettypeBox::$staticValue) . ":" . EvalAotSettypeBox::$staticValue : "bad"; +echo ":"; +$class = "EvalAotSettypeBox"; +$staticName = "staticValue"; +echo settype($class::${$staticName}, "bool") ? gettype(EvalAotSettypeBox::$staticValue) . ":" . (EvalAotSettypeBox::$staticValue ? "true" : "false") : "bad"; +echo ":"; +echo function_exists("settype");'); +"#, + ); + assert_eq!( + out, + "string:42:boolean:false:integer:6:string:8:integer:8:string:9:boolean:true:1" + ); +} + +/// Verifies eval SPL object identity builtins inspect AOT object cells. +#[test] +fn test_eval_dispatches_spl_object_identity_builtin_calls() { + let out = compile_and_run( + r#" $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); echo function_exists("spl_object_hash");'); +"#, + ); + assert_eq!(out, "stable:unique:hash:call:array:11"); +} + +/// Verifies eval `gettype()` maps boxed Mixed runtime tags to PHP type names. +#[test] +fn test_eval_dispatches_gettype_builtin_call() { + let out = compile_and_run( + r#" 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +echo ":"; echo function_exists("gettype");'); +"#, + ); + assert_eq!( + out, + "integer:double:string:boolean:NULL:array:array:boolean:NULL:1" + ); +} + +/// Verifies eval `get_class()` resolves stdClass and AOT object runtime names. +#[test] +fn test_eval_dispatches_get_class_builtin_call() { + let out = compile_and_run( + r#" $probe]) . ":"; +echo function_exists("get_class");'); +"#, + ); + assert_eq!( + out, + "stdClass:EvalClassNameProbe:stdClass:EvalClassNameProbe:1" + ); +} + +/// Verifies eval no-arg `get_class()` and `get_parent_class()` use method class scope. +#[test] +fn test_eval_dispatches_no_arg_class_name_builtins() { + let out = compile_and_run_capture( + r#"inherited() . ":"; +echo $child->inheritedCallable() . ":"; +echo $child->own() . ":"; +try { + get_class(); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo get_parent_class();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalNoArgRuntimeBase:EvalNoArgRuntimeParent:EvalNoArgRuntimeBase:EvalNoArgRuntimeParent:EvalNoArgRuntimeChild:EvalNoArgRuntimeBase:Error:get_class() without arguments must be called from within a class:" + ); +} + +/// Verifies eval `get_parent_class()` resolves AOT object and class-string parents. +#[test] +fn test_eval_dispatches_get_parent_class_builtin_call() { + let out = compile_and_run( + r#" "EvalParentChild"]) . ":"; +echo function_exists("get_parent_class");'); +"#, + ); + + assert_eq!( + out, + "EvalParentBase:EvalParentBase:EvalParentBase:EvalParentBase:EvalParentBase:1" + ); +} + +/// Verifies eval `define()` and `defined()` share dynamic constant names across fragments. +#[test] +fn test_eval_define_and_defined_dynamic_constants() { + let out = compile_and_run_capture( + r#" "DynEvalConst"]) ? "Y" : "N";'); +echo eval('return function_exists("define") && function_exists("defined") ? "Y" : "N";'); +"#, + ); + assert!(out.success, "program failed: {}", out.stderr); + assert_eq!(out.stdout, "YY77NNYYYYY"); + assert!( + out.stderr + .contains("Warning: define(): Constant already defined"), + "expected duplicate eval define warning, got stderr={}", + out.stderr + ); +} + +/// Verifies eval can read predefined runtime constants and protect them from redefinition. +#[test] +fn test_eval_reads_predefined_runtime_constants() { + let out = compile_and_run_capture( + r#" 9000000000000000000 ? "int" : "bad") . ":" . + (defined("PHP_OS") ? "defined" : "bad") . ":" . + (defined("\\\\PHP_OS") ? "root" : "bad") . ":" . + (defined("php_os") ? "bad" : "case") . ":" . + (define("PHP_OS", "x") ? "bad" : "locked");'); +"#, + ); + assert!(out.success, "program failed: {}", out.stderr); + assert_eq!(out.stdout, "eol:os:/:int:defined:root:case:locked"); + assert!( + out.stderr + .contains("Warning: define(): Constant already defined"), + "expected predefined eval define warning, got stderr={}", + out.stderr + ); +} + +/// Verifies `@eval(...)` suppresses duplicate eval `define()` warnings. +#[test] +fn test_error_control_suppresses_duplicate_eval_define_warning() { + let out = compile_and_run_capture( + r#" 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); echo function_exists("hypot");'); +"#, + ); + assert_eq!( + out, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:1111" + ); +} + +/// Verifies eval `pow()` reuses exponentiation runtime hooks through direct and callable calls. +#[test] +fn test_eval_dispatches_pow_builtin_call() { + let out = compile_and_run( + r#" 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); +echo ":"; echo function_exists("number_format");'); +"#, + ); + assert_eq!( + out, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:1" + ); +} + +/// Verifies eval printf-family builtins format, print, and dispatch through callables. +#[test] +fn test_eval_dispatches_printf_family_builtin_calls() { + let out = compile_and_run( + r#" "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); echo is_callable("vprintf");'); +"#, + ); + assert_eq!( + out, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:1111" + ); +} + +/// Verifies eval `sscanf()` returns indexed string matches through direct and callable paths. +#[test] +fn test_eval_dispatches_sscanf_builtin_call() { + let out = compile_and_run( + r#" "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +echo function_exists("sscanf");'); +"#, + ); + assert_eq!(out, "John:1.5:30:-25:-2.5e3:ok:1"); +} + +/// Verifies eval `min()` and `max()` select numeric values directly and through callables. +#[test] +fn test_eval_dispatches_min_max_builtin_calls() { + let out = compile_and_run( + r#" 9, "min" => 0, "max" => 7]); +echo ":"; echo function_exists("clamp"); echo is_callable("clamp");'); +"#, + ); + assert_eq!(out, "5:10:0:2.5:5:0:7:11"); +} + +/// Verifies eval `pi()` returns the PHP math constant through direct and callable calls. +#[test] +fn test_eval_dispatches_pi_builtin_call() { + let out = compile_and_run( + r#" "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +];'); +eval('echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";'); +"#, + ); + assert_eq!(out, "1001100:01111011"); +} + +/// Verifies eval builtin dispatch can inspect arrays from the caller scope. +#[test] +fn test_eval_count_reads_scope_array() { + let out = compile_and_run( + r#" 0) { echo "D"; } else { echo "d"; } +echo ":"; +if (strlen(__FILE__) > strlen(__DIR__)) { echo "F"; } else { echo "f"; }'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval __FILE__/__DIR__ should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval __FILE__/__DIR__ should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only literal eval __FILE__/__DIR__ should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only literal eval __FILE__/__DIR__ should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only literal eval __FILE__/__DIR__ should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "D:F"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies eval scope magic constants are empty through EIR AOT even from namespaced methods. +#[test] +fn test_literal_eval_scope_magic_constants_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_scope_magic_aot"); + let source = r#"run(); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval scope magic constants should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval scope magic constants should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only literal eval scope magic constants should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only literal eval scope magic constants should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only literal eval scope magic constants should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "[||||]"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies eval trait methods expose PHP magic constants from the declaring trait. +#[test] +fn test_eval_trait_method_magic_constants_execute_through_bridge() { + let out = compile_and_run( + r#"aliasReport(); echo ":"; +echo Box::aliasStat();'); +"#, + ); + let expected = concat!( + "EvalBridgeTraitMagic|EvalBridgeTraitMagic\\Box|", + "EvalBridgeTraitMagic\\Inner|", + "EvalBridgeTraitMagic\\Inner::report|report:", + "EvalBridgeTraitMagic|EvalBridgeTraitMagic\\Box|", + "EvalBridgeTraitMagic\\Inner|", + "EvalBridgeTraitMagic\\Inner::stat|stat" + ); + + assert_eq!(out, expected); +} + +/// Verifies eval trait member defaults expose PHP magic constants through the bridge. +#[test] +fn test_eval_trait_member_default_magic_constants_execute_through_bridge() { + let out = compile_and_run( + r#"getDefaultProperties(); +echo Base::C . "|" . Base::T . "|"; +echo Child::C . "|" . Child::T . "|"; +echo $object->p . "|" . $object->pt . "|"; +echo Child::$sp . "|" . Child::$st . "|"; +echo $traitProps["p"] . "|" . $traitProps["pt"] . ":"; +$direct = new Direct(); +echo Direct::C . "|" . Direct::T . "|" . $direct->p . "|" . $direct->pt . "|" . Direct::$sp . "|" . Direct::$st;'); +"#, + ); + let expected = concat!( + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Inner|EvalBridgeDefaultMagic\\Inner:", + "EvalBridgeDefaultMagic\\Direct||", + "EvalBridgeDefaultMagic\\Direct||", + "EvalBridgeDefaultMagic\\Direct|" + ); + + assert_eq!(out, expected); +} + +/// Verifies eval-declared functions persist across eval calls in the same generated context. +#[test] +fn test_eval_declared_function_persists_across_eval_calls() { + let out = compile_and_run( + r#" 0 ? "Y" : "N");'); +"#, + ); + assert_eq!(out, "1:1:Y"); +} + +/// Verifies top-level eval can replace `$argc` after the eval barrier widens it. +#[test] +fn test_eval_top_level_can_replace_argc_type() { + let out = compile_and_run( + r#" 0 ? "Y" : "N");'); +} +show_eval_process_args(); +"#, + ); + assert_eq!(out, "1:1:Y"); +} + +/// Verifies functions declared by eval from a namespace are registered globally. +#[test] +fn test_eval_declared_function_in_namespace_is_global() { + let out = compile_and_run( + r#" 2, 'x' => 1]); +$args = ['y' => 3, 'x' => 2]; +echo ":" . call_user_func_array('dyn_eval_cufa', $args); +"#, + ); + assert_eq!(out, "12:23"); +} + +/// Verifies `call_user_func()` inside eval can dispatch to an eval-declared function. +#[test] +fn test_eval_fragment_call_user_func_dispatches_eval_declared_function() { + let out = compile_and_run( + r#" 2, "x" => 1]);'); +"#, + ); + assert_eq!(out, "12"); +} + +/// Verifies `call_user_func_array()` inside eval dispatches to supported builtins. +#[test] +fn test_eval_fragment_call_user_func_array_dispatches_builtin() { + let out = compile_and_run( + r#" "R", "left" => "L"]);'); +"#, + ); + assert_eq!(out, "L:R"); +} + +/// Verifies eval fragments can call AOT user functions registered in the eval context. +#[test] +fn test_eval_fragment_can_call_native_user_function() { + let out = compile_and_run( + r#"name = $name; + } +} + +function native_eval_ref_array_heap(array &$items): string { + $items = ["A", "B"]; + return "array"; +} + +function native_eval_ref_iterable_heap(iterable &$items): string { + $items = ["left" => "L", "right" => "R"]; + return "iter"; +} + +function native_eval_ref_object_heap(NativeEvalRefBox &$box): string { + $box = new NativeEvalRefBox($box->name . "!"); + return $box->name; +} + +echo eval('$items = [1]; +$first = native_eval_ref_array_heap($items); +$fn = "native_eval_ref_array_heap"; +$fn($items); +native_eval_ref_array_heap(items: $items); +$iter = [0]; +$iterFirst = native_eval_ref_iterable_heap($iter); +$box = new NativeEvalRefBox("start"); +$objectFirst = native_eval_ref_object_heap($box); +native_eval_ref_object_heap(box: $box); +return $first . ":" . $items[0] . ":" . $items[1] . ":" . $iterFirst . ":" . $iter["right"] . ":" . $objectFirst . ":" . $box->name;'); +"#, + ); + assert_eq!(out, "array:A:B:iter:R:start!:start!!"); +} + +/// Verifies eval can dispatch generated/AOT variadic functions through the native bridge. +#[test] +fn test_eval_fragment_can_call_native_variadic_user_function() { + let out = compile_and_run( + r#" 0 ? $items[0] : "-"); +} + +echo eval('$fn = "native_eval_variadic_collect"; +return native_eval_variadic_collect("H", "A", "B") . "|" + . native_eval_variadic_default(head: "N") . "|" + . native_eval_variadic_default("P", "Q") . "|" + . $fn("V", "X", "Y") . "|" + . call_user_func("native_eval_variadic_collect", "C", "M", "N");'); +"#, + ); + assert_eq!(out, "H:2:A:B|N:0:-|P:1:Q|V:2:X:Y|C:2:M:N"); +} + +/// Verifies eval fragments called from methods can mutate public properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_public_property() { + let out = compile_and_run( + r#"x = $this->x + 1;'); + } +} + +$box = new EvalPropBox(); +$box->bump(); +echo $box->x; +"#, + ); + assert_eq!(out, "2"); +} + +/// Verifies eval fragments inherit native method scope for private AOT property access. +#[test] +fn test_eval_fragment_can_mutate_this_private_property_from_declaring_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +$box = new EvalPrivatePropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments outside the declaring scope cannot read private AOT properties. +#[test] +fn test_eval_fragment_rejects_private_property_outside_declaring_scope() { + let out = compile_and_run( + r#"x; +} catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Cannot access private property EvalPrivatePropOutsideBox::$x" + ); +} + +/// Verifies eval fragments can access inherited protected AOT properties from child scopes. +#[test] +fn test_eval_fragment_can_mutate_protected_aot_property_from_child_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +$box = new EvalProtectedPropChild(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments allow parent scopes to access child-declared protected AOT properties. +#[test] +fn test_eval_fragment_can_mutate_child_protected_aot_property_from_parent_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +class EvalParentScopeProtectedPropChild extends EvalParentScopeProtectedPropBase { + protected int $x = 1; +} + +$box = new EvalParentScopeProtectedPropChild(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT properties between sibling class scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_property_from_sibling_scope() { + let out = compile_and_run( + r#"x; + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); + } +} + +$right = new EvalProtectedPropRight(); +$right->run(); +"#, + ); + assert_eq!( + out, + "Error:Cannot access protected property EvalProtectedPropLeft::$x" + ); +} + +/// Verifies eval fragments can read and write public nullable-int AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_nullable_int_property() { + let out = compile_and_run( + r#"count === null) ? "N" : "n"; + $this->count = 7; + $out = $out . ":" . (($this->count === 7) ? "I7" : "bad"); + $this->count = "42"; + $out = $out . ":" . (($this->count === 42) ? "I42" : "bad"); + $this->count = null; + return $out . ":" . (($this->count === null) ? "N" : "bad");'); + } +} + +$box = new EvalNullableIntPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "N:I7:I42:N"); +} + +/// Verifies eval fragments can read and write public nullable scalar AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_nullable_scalar_properties() { + let out = compile_and_run( + r#"name === null && $this->flag === null && $this->ratio === null) ? "N" : "bad"; + $this->name = "Ada"; + $this->flag = true; + $this->ratio = 2.5; + $out = $out . ":" . (($this->name === "Ada" && $this->flag === true && $this->ratio === 2.5) ? "set" : "bad"); + $this->name = null; + $this->flag = null; + $this->ratio = null; + return $out . ":" . (($this->name === null && $this->flag === null && $this->ratio === null) ? "N" : "bad");'); + } +} + +$box = new EvalNullableScalarPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "N:set:N"); +} + +/// Verifies eval fragments can replace public array AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_array_property() { + let out = compile_and_run( + r#"items = [3, 4]; + return count($this->items) . ":" . $this->items[0] . ":" . $this->items[1];'); + } +} + +$box = new EvalArrayPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "2:3:4"); +} + +/// Verifies eval fragments can replace public object AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_object_property() { + let out = compile_and_run( + r#"n = $n; + } +} + +class EvalObjectPropBox { + public EvalObjectPropValue $value; + + public function __construct() { + $this->value = new EvalObjectPropValue(1); + } + + public function run(): void { + echo eval('$this->value = new EvalObjectPropValue(7); + $value = $this->value; + return $value->n;'); + } +} + +$box = new EvalObjectPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies eval fragments can read and write public nullable-int AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_aot_nullable_int_static_property() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT static properties outside the declaring scope. +#[test] +fn test_eval_fragment_rejects_private_aot_static_property_outside_declaring_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +$box = new EvalPrivateStaticPropChild(); +$box->run(); +"#, + ); + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateStaticPropBase::$x" + ); +} + +/// Verifies eval fragments can access inherited protected AOT static properties from child scopes. +#[test] +fn test_eval_fragment_can_mutate_protected_aot_static_property_from_child_method() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments use AOT private parent static slots when child classes shadow them. +#[test] +fn test_eval_fragment_uses_aot_private_parent_static_property_shadowing_scope() { + let out = compile_and_run_capture( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT static properties between sibling class scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_static_property_from_sibling_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +$right = new EvalProtectedStaticPropRight(); +$right->run(); +"#, + ); + assert_eq!( + out, + "Error:Cannot access protected property EvalProtectedStaticPropLeft::$x" + ); +} + +/// Verifies eval fragments throw Error for invalid AOT static property access. +#[test] +fn test_eval_fragment_invalid_aot_static_property_access_throws_error() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + EvalInvalidAotStaticPropBox::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidAotStaticPropBox::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalMissingAotStaticPropBox::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Access to undeclared static property EvalInvalidAotStaticPropBox::$missing|\ +Error:Access to undeclared static property EvalInvalidAotStaticPropBox::$instance|\ +Error:Typed static property EvalInvalidAotStaticPropBox::$typed must not be accessed before initialization|\ +Error:Class \"EvalMissingAotStaticPropBox\" not found" + ); +} + +/// Verifies eval fragments can replace public array AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_aot_array_static_property() { + let out = compile_and_run( + r#"n = $n; + } +} + +class EvalObjectStaticPropBox { + public static EvalObjectStaticPropValue $value; +} + +EvalObjectStaticPropBox::$value = new EvalObjectStaticPropValue(1); + +echo eval('EvalObjectStaticPropBox::$value = new EvalObjectStaticPropValue(8); + $value = EvalObjectStaticPropBox::$value; + return $value->n;'); +"#, + ); + assert_eq!(out, "8"); +} + +/// Verifies eval keeps PHP property names case-sensitive while parsing keywords case-insensitively. +#[test] +fn test_eval_fragment_preserves_this_property_case() { + let out = compile_and_run( + r#"camelName;'); + } +} + +$box = new EvalCasePropBox(); +$box->read(); +"#, + ); + assert_eq!(out, "42"); +} + +/// Verifies eval fragments can call public zero-argument AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_zero_arg_method() { + let out = compile_and_run( + r#"x + 1; + } + + public function run(): void { + echo eval('return $this->answer();'); + } +} + +$box = new EvalMethodBox(); +$box->run(); +"#, + ); + assert_eq!(out, "42"); +} + +/// Verifies eval fragments can call private AOT instance methods from the declaring scope. +#[test] +fn test_eval_fragment_can_call_private_aot_method_from_declaring_scope() { + let out = compile_and_run( + r#"secret(3);'); + } +} + +(new EvalPrivateAotMethodBox())->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval callable arrays can call private AOT instance methods from the declaring scope. +#[test] +fn test_eval_fragment_callable_array_can_call_private_aot_method_from_declaring_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT instance methods from child scopes. +#[test] +fn test_eval_fragment_rejects_private_aot_method_from_child_scope() { + let out = compile_and_run( + r#"secret(3); + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); + } +} + +(new EvalPrivateAotMethodChild())->run(); +"#, + ); + assert_eq!( + out, + "Error:Call to private method EvalPrivateAotMethodBase::secret() from scope EvalPrivateAotMethodChild" + ); +} + +/// Verifies eval fragments dispatch private AOT parent methods on child receivers. +#[test] +fn test_eval_fragment_dispatches_aot_private_parent_method_on_child_receiver() { + let out = compile_and_run_capture( + r#"secret();'); + } +} +class EvalPrivateAotParentReceiverChild extends EvalPrivateAotParentReceiverBase {} +$object = new EvalPrivateAotParentReceiverChild(); +eval('echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "p"); +} + +/// Verifies eval fragments dispatch AOT private parent methods shadowed by child methods. +#[test] +fn test_eval_fragment_dispatches_aot_private_parent_method_shadowing_scope() { + let out = compile_and_run_capture( + r#"secret();'); + } + public function parentCallback() { + return eval('$cb = [$this, "secret"]; return call_user_func($cb);'); + } + public static function parentStaticView() { + return eval('return self::staticSecret();'); + } +} +class EvalPrivateAotMethodShadowChild extends EvalPrivateAotMethodShadowParent { + public function secret() { return "c"; } + public static function staticSecret() { return "cs"; } + public function childView() { + return eval('return $this->secret();'); + } + public function childCallback() { + return eval('$cb = [$this, "secret"]; return call_user_func($cb);'); + } + public static function childStaticView() { + return eval('return self::staticSecret();'); + } +} +$object = new EvalPrivateAotMethodShadowChild(); +eval('echo $object->secret(); echo "|"; +echo $object->childView(); echo "|"; +echo $object->parentView(); echo "|"; +echo $object->childCallback(); echo "|"; +echo $object->parentCallback(); echo "|"; +echo EvalPrivateAotMethodShadowChild::staticSecret(); echo "|"; +echo EvalPrivateAotMethodShadowChild::childStaticView(); echo "|"; +echo EvalPrivateAotMethodShadowChild::parentStaticView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "c|c|p|c|p|cs|cs|ps"); +} + +/// Verifies eval fragments can call inherited protected AOT methods from child scopes. +#[test] +fn test_eval_fragment_can_call_protected_aot_method_from_child_scope() { + let out = compile_and_run( + r#"add(3);'); + } +} + +(new EvalProtectedAotMethodChild())->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT instance methods between sibling scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_method_from_sibling_scope() { + let out = compile_and_run( + r#"add(3); + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); + } +} + +(new EvalProtectedAotMethodRight())->run(); +"#, + ); + assert_eq!( + out, + "Error:Call to protected method EvalProtectedAotMethodLeft::add() from scope EvalProtectedAotMethodRight" + ); +} + +/// Verifies eval fragments pass one scalar argument to public AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_one_arg_method() { + let out = compile_and_run( + r#"x + $amount; + } + + public function run(): void { + echo eval('return $this->add(9);'); + } +} + +$box = new EvalMethodArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50"); +} + +/// Verifies eval fragments pass two scalar arguments to public AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_two_arg_method() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(9, "!");'); + } +} + +$box = new EvalMethodTwoArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50!"); +} + +/// Verifies eval fragments pass more than two fixed scalar arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_this_public_many_arg_method() { + let out = compile_and_run( + r#"x + $a + $b + $c) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(1, 2, 3, "!");'); + } +} + +$box = new EvalMethodManyArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "16!"); +} + +/// Verifies eval fragments pass AOT method arguments that overflow onto the caller stack. +#[test] +fn test_eval_fragment_can_call_this_public_method_with_stack_string_arg() { + let out = compile_and_run( + r#"join4("A", "B", "C", "D");'); + } +} + +$box = new EvalMethodStackStringArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "ABCD"); +} + +/// Verifies eval fragments pass boxed Mixed values to public AOT methods. +#[test] +fn test_eval_fragment_can_call_this_public_mixed_arg_method() { + let out = compile_and_run( + r#"identity("mixed-ok");'); + } +} + +$box = new EvalMethodMixedArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "mixed-ok"); +} + +/// Verifies eval fragments can pass object-typed arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_aot_method_with_object_arg() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalMethodObjectArgBox { + public function describe(EvalMethodObjectArgItem $item): string { + return $item->name; + } + + public static function describeStatic(EvalMethodObjectArgItem $item): string { + return $item->name . "!"; + } + + public function run() { + $item = new EvalMethodObjectArgItem("Obj"); + return eval('return $this->describe($item) . ":" . EvalMethodObjectArgBox::describeStatic($item);'); + } +} + +echo (new EvalMethodObjectArgBox())->run(); +"#, + ); + assert_eq!(out, "Obj:Obj!"); +} + +/// Verifies eval fragments can pass array-typed arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_aot_method_with_array_arg() { + let out = compile_and_run( + r#"countItems([1, 2, 3]) . ":" . EvalMethodArrayArgBox::countStatic([4, 5]);'); + } +} + +echo (new EvalMethodArrayArgBox())->run(); +"#, + ); + assert_eq!(out, "3:2"); +} + +/// Verifies eval fragments can pass iterable arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_iterable_parameters() { + let out = compile_and_run( + r#"i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): mixed { return "I" . $this->i; } + public function key(): mixed { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} + +class EvalAotIterableParamBox { + public string $label; + + public function __construct(iterable $items) { + $this->label = self::join($items); + } + + public function describe(iterable $items): string { + return self::join($items); + } + + public static function describeStatic(iterable $items): string { + return self::join($items); + } + + private static function join(iterable $items): string { + $out = ""; + foreach ($items as $item) { + $out .= $item; + } + return $out; + } +} + +echo eval('$box = new EvalAotIterableParamBox(["C", "D"]); +$fromIterator = new EvalAotIterableParamBox(new EvalAotIterableParamIterator()); +return $box->describe(["A", "B"]) . ":" . EvalAotIterableParamBox::describeStatic(new EvalAotIterableParamIterator()) . ":" . $box->label . ":" . $fromIterator->label;'); +"#, + ); + assert_eq!(out, "AB:I0I1:CD:I0I1"); +} + +/// Verifies eval fragments can pass nullable-int arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_parameters() { + let out = compile_and_run( + r#"label = self::format($count); + } + + public function describe(?int $count): string { + return self::format($count); + } + + public static function describeStatic(?int $count = null): string { + return self::format($count); + } + + private static function format(?int $count): string { + return $count === null ? "N" : "I" . $count; + } +} + +echo eval('$defaulted = new EvalAotNullableIntParamBox(); +$fromInt = new EvalAotNullableIntParamBox(7); +return $defaulted->label . ":" . $fromInt->label . ":" . $fromInt->describe(null) . ":" . $fromInt->describe("42") . ":" . EvalAotNullableIntParamBox::describeStatic() . ":" . EvalAotNullableIntParamBox::describeStatic(5);'); +"#, + ); + assert_eq!(out, "N:I7:N:I42:N:I5"); +} + +/// Verifies eval fragments can read nullable-int return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_returns() { + let out = compile_and_run( + r#"maybe(true) === 7 ? "I7" : "bad") . ":" . (is_null($this->maybe(false)) ? "N" : "bad") . ":" . (EvalAotNullableIntReturnBox::maybeStatic(true) === 11 ? "S11" : "bad") . ":" . (is_null(EvalAotNullableIntReturnBox::maybeStatic(false)) ? "SN" : "bad");'); + } +} + +echo (new EvalAotNullableIntReturnBox())->run(); +"#, + ); + assert_eq!(out, "I7:N:S11:SN"); +} + +/// Verifies eval fragments can pass nullable scalar arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_parameters() { + let out = compile_and_run( + r#"label = self::format($name, $flag, $ratio); + } + + public function describe(?string $name, ?bool $flag, ?float $ratio): string { + return self::format($name, $flag, $ratio); + } + + public static function describeStatic(?string $name = null, ?bool $flag = null, ?float $ratio = null): string { + return self::format($name, $flag, $ratio); + } + + private static function format(?string $name, ?bool $flag, ?float $ratio): string { + $namePart = $name === null ? "N" : (is_string($name) ? "S" . $name : "badName"); + $flagPart = $flag === null ? "N" : (is_bool($flag) ? ($flag ? "BT" : "BF") : "badFlag"); + $ratioPart = $ratio === null ? "N" : (is_float($ratio) ? "F" . $ratio : "badRatio"); + return $namePart . "/" . $flagPart . "/" . $ratioPart; + } +} + +echo eval('$defaulted = new EvalAotNullableScalarParamBox(); +$filled = new EvalAotNullableScalarParamBox("Ada", true, 2.5); +return $defaulted->label . ":" . $filled->label . ":" . $filled->describe(null, false, 3.5) . ":" . EvalAotNullableScalarParamBox::describeStatic("Bea", true, 4.5);'); +"#, + ); + assert_eq!(out, "N/N/N:SAda/BT/F2.5:N/BF/F3.5:SBea/BT/F4.5"); +} + +/// Verifies eval fragments can read nullable scalar return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_returns() { + let out = compile_and_run( + r#"maybeName(true) === "Ada" ? "S" : "bad") . ":" . (is_null($this->maybeName(false)) ? "SN" : "bad") . ":" . ($this->maybeFlag(true) === false ? "BF" : "bad") . ":" . (is_null($this->maybeFlag(false)) ? "BN" : "bad") . ":" . ($this->maybeRatio(true) === 1.5 ? "F15" : "bad") . ":" . (is_null($this->maybeRatio(false)) ? "FN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticName(true) === "Bea" ? "SS" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticName(false)) ? "SSN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticFlag(true) === true ? "SBT" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticFlag(false)) ? "SBN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticRatio(true) === 2.5 ? "SF25" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticRatio(false)) ? "SFN" : "bad");'); + } +} + +echo (new EvalAotNullableScalarReturnBox())->run(); +"#, + ); + assert_eq!(out, "S:SN:BF:BN:F15:FN:SS:SSN:SBT:SBN:SF25:SFN"); +} + +/// Verifies eval dispatch uses inherited AOT metadata for complex signatures. +#[test] +fn test_eval_fragment_dispatches_inherited_aot_complex_signatures() { + let out = compile_and_run( + r#"choose(suffix: "X") . ":" . $child->choose(value: 2) . ":" . EvalAotComplexChild::chooseStatic(suffix: "Y") . ":" . EvalAotComplexChild::chooseStatic(value: 3) . ":" . $child->both(new EvalAotComplexBoth());'); +"#, + ); + assert_eq!(out, "DX:12:DY:13:both"); +} + +/// Verifies eval fragments can read iterable return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { + let out = compile_and_run( + r#" "L", "right" => "R"]; + } + + public function run() { + return eval('$items = $this->items(); +$labels = EvalAotIterableReturnBox::labels(); +return is_iterable($items) . ":" . count($items) . ":" . $items[1] . ":" . is_iterable($labels) . ":" . $labels["right"];'); + } +} + +echo (new EvalAotIterableReturnBox())->run(); +"#, + ); + assert_eq!(out, "1:3:2:1:R"); +} + +/// Verifies eval fragments inherit lexical `self::` from an AOT instance method. +#[test] +fn test_eval_fragment_in_aot_method_resolves_self_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalAotScopeSelfBox:self"); +} + +/// Verifies eval fragments inherit late-static `static::` from an AOT instance method. +#[test] +fn test_eval_fragment_in_aot_method_resolves_late_static_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalAotScopeStaticBase:EvalAotScopeStaticChild:child"); +} + +/// Verifies eval classes keep late-static scope inside inherited AOT eval fragments. +#[test] +fn test_eval_declared_child_inherited_aot_eval_fragment_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceProbe() . "|"; +echo EvalAotEvalScopeChild::staticProbe();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotEvalScopeParent:EvalAotEvalScopeChild:EvalAotEvalScopeChild|\ +EvalAotEvalScopeParent:EvalAotEvalScopeChild:EvalAotEvalScopeChild" + ); +} + +/// Verifies eval classes keep late-static `static::class` in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_class_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceClass() . "|"; +echo EvalAotDirectScopeChild::staticClass();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotDirectScopeChild|EvalAotDirectScopeChild" + ); +} + +/// Verifies eval classes keep late-static `static::method()` in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_call_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceCall() . "|"; +echo EvalAotDirectStaticCallChild::staticCall();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "child|child"); +} + +/// Verifies eval classes keep late-static `static::$property` access in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_property_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceRead() . "|"; +echo EvalAotDirectStaticPropChild::staticRead() . "|"; +(new EvalAotDirectStaticPropChild())->instanceWrite("one"); +echo EvalAotDirectStaticPropChild::$value . ":" . EvalAotDirectStaticPropParent::$value . "|"; +EvalAotDirectStaticPropChild::staticWrite("two"); +echo EvalAotDirectStaticPropChild::$value . ":" . EvalAotDirectStaticPropParent::$value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "child|child|one:parent|two:parent"); +} + +/// Verifies eval fragments resolve `parent::` through AOT parent metadata. +#[test] +fn test_eval_fragment_in_aot_method_resolves_parent_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "parent"); +} + +/// Verifies eval fragments read generated/AOT class constants through inherited class scope. +#[test] +fn test_eval_fragment_reads_aot_class_constants() { + let out = compile_and_run( + r#"run(); +echo ":"; +echo eval('return EvalAotConstChild::PUBLIC_BASE . ":" . (EvalAotConstState::Ready === EvalAotConstState::Ready ? "case" : "bad");'); +"#, + ); + assert_eq!(out, "secret:base:10:4:case"); +} + +/// Verifies eval Reflection hides private AOT constants inherited from a parent class. +#[test] +fn test_eval_reflection_hides_private_inherited_aot_class_constants() { + let out = compile_and_run( + r#"getConstants(ReflectionClassConstant::IS_PRIVATE); +echo $ref->hasConstant("HIDDEN") ? "bad" : "ok"; +echo ":" . count($private); +echo ":" . $ref->getConstant("VISIBLE"); +return "";'); +"#, + ); + assert_eq!(out, "ok:0:visible"); +} + +/// Verifies eval rejects direct fetches of private AOT constants through child names. +#[test] +fn test_eval_rejects_private_inherited_aot_class_constant_fetch() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Undefined constant EvalAotPrivateConstFetchChild::HIDDEN" + ); +} + +/// Verifies eval ReflectionClass exposes generated/AOT class constants and reflectors. +#[test] +fn test_eval_reflection_aot_class_constants() { + let out = compile_and_run( + r#"hasConstant("OWN") ? "O" : "o"; +echo $ref->hasConstant("BASE") ? "B" : "b"; +echo $ref->hasConstant("IFACE_LIMIT") ? "I" : "i"; +echo $ref->hasConstant("own") ? "bad" : "z"; +$all = $ref->getConstants(); +$private = $ref->getConstants(ReflectionClassConstant::IS_PRIVATE); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); +$own = $ref->getReflectionConstant("OWN"); +$direct = new ReflectionClassConstant("EvalAotReflectConstChild", "SECRET"); +echo ":" . $ref->getConstant("OWN") . ":" . $ref->getConstant("SECRET") . ":" . $all["BASE"] . ":" . $all["IFACE_LIMIT"]; +echo ":" . count($private) . ":" . $private["SECRET"]; +echo ":" . count($final) . ":" . $own->getName() . ":" . $own->getValue() . ":" . ($own->isFinal() ? "F" : "f"); +echo ":" . $direct->getDeclaringClass()->getName() . ":" . $direct->getValue(); +$enum = new ReflectionClass("EvalAotReflectConstEnum"); +$case = $enum->getReflectionConstant("Ready"); +echo ":" . ($case->getValue() === EvalAotReflectConstEnum::Ready ? "case" : "bad") . ":" . $enum->getConstant("LEVEL"); +return "";'); +"#, + ); + assert_eq!( + out, + "OBIz:10:s:4:8:1:s:1:OWN:10:F:EvalAotReflectConstChild:s:case:7" + ); +} + +/// Verifies eval ReflectionClass materializes generated/AOT float constant arithmetic. +#[test] +fn test_eval_reflection_aot_float_constant_arithmetic() { + let out = compile_and_run_capture( + r#"getConstants(); +$power = $ref->getReflectionConstant("POWER"); +$negativePower = new ReflectionClassConstant("EvalAotReflectFloatConstTarget", "NEG_POWER"); +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("DIFF") . ":"; +echo $all["PRODUCT"] . ":"; +echo $ref->getConstant("QUOTIENT") . ":"; +echo $power->getValue() . ":"; +echo $negativePower->getValue(); +} catch (Throwable $e) { + echo "ERR:" . get_class($e) . ":" . $e->getMessage(); +} +return "";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "3.5:3.5:6:3.5:2.25:0.5"); +} + +/// Verifies eval fragments can unpack numeric arrays into public AOT method calls. +#[test] +fn test_eval_fragment_can_call_this_public_method_with_spread_args() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(...[9, "!"]);'); + } +} + +$box = new EvalMethodSpreadBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50!"); +} + +/// Verifies eval callable arrays dispatch public AOT methods through all dynamic call surfaces. +#[test] +fn test_eval_fragment_callable_array_dispatches_this_public_method() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('$cb = [$this, "label"]; +echo $cb(1, "a"); +echo ":"; +echo call_user_func($cb, 2, "b"); +echo ":"; +return call_user_func_array($cb, [3, "c"]);'); + } +} + +$box = new EvalCallableArrayBox(); +$box->run(); +"#, + ); + assert_eq!(out, "41a:42b:43c"); +} + +/// Verifies eval callable arrays bind named arguments for generated/AOT methods. +#[test] +fn test_eval_fragment_callable_array_dispatches_aot_method_with_named_args() { + let out = compile_and_run_capture( + r#" "D", "left" => "C"]) . ":" . + call_user_func_array($static, ["right" => "F", "left" => "E"]);'); + } +} + +echo (new EvalAotCallableArrayNamedBox())->run(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:CD:EF"); +} + +/// Verifies eval AOT callables preserve typed by-reference argument writeback. +#[test] +fn test_eval_fragment_aot_callables_write_back_typed_by_ref_args() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +class EvalAotCallableRefDriver { + public static function run(callable $callback, int &$value, int $delta) { + return $callback($value, $delta); + } +} + +echo eval('$box = new EvalAotCallableRefBox(5); +$method = [$box, "bump"]; +$value = "7"; +echo $method($value) . ":" . gettype($value) . ":" . $value . "|"; +$string = "EvalAotCallableRefBox::add"; +$staticValue = "3"; +echo $string($staticValue, 4) . ":" . gettype($staticValue) . ":" . $staticValue . "|"; +$first = EvalAotCallableRefBox::add(...); +$next = "2"; +echo $first($next, 6) . ":" . gettype($next) . ":" . $next . "|"; +$instanceFirst = $box->bump(...); +$instanceValue = "4"; +echo $instanceFirst($instanceValue) . ":" . gettype($instanceValue) . ":" . $instanceValue . "|"; +$invokable = new EvalAotCallableRefBox(10); +$invokableValue = "1"; +echo $invokable($invokableValue, 2) . ":" . gettype($invokableValue) . ":" . $invokableValue . "|"; +$invokableFirst = $invokable(...); +$firstValue = "2"; +echo $invokableFirst($firstValue, 3) . ":" . gettype($firstValue) . ":" . $firstValue . "|"; +$bridge = new EvalAotCallableRefBox(20); +$bridgeFirst = $bridge(...); +$bridgeValue = "5"; +return EvalAotCallableRefDriver::run($bridgeFirst, $bridgeValue, 1) . ":" . gettype($bridgeValue) . ":" . $bridgeValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:integer:12|7:integer:7|8:integer:8|9:integer:9|13:integer:13|15:integer:15|26:integer:26" + ); +} + +/// Verifies eval AOT callable by-reference writeback updates property lvalues. +#[test] +fn test_eval_fragment_aot_callables_write_back_by_ref_property_lvalues() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableRefLvalueBox(4); +$method = [$box, "bump"]; +echo $method($box->value) . ":" . $box->value . "|"; +$name = "value"; +echo $method($box->{$name}) . ":" . $box->value . "|"; +$string = "EvalAotCallableRefLvalueBox::add"; +echo $string(EvalAotCallableRefLvalueBox::$staticValue, 5) . ":" . EvalAotCallableRefLvalueBox::$staticValue . "|"; +$class = "EvalAotCallableRefLvalueBox"; +$staticName = "dynStaticValue"; +echo $string($class::${$staticName}, 6) . ":" . EvalAotCallableRefLvalueBox::$dynStaticValue . "|"; +$first = EvalAotCallableRefLvalueBox::add(...); +echo $first($box->value, 3) . ":" . $box->value . "|"; +$invokable = new EvalAotCallableRefLvalueBox(10); +echo $invokable($box->{$name}, 2) . ":" . $box->value . "|"; +$invokableFirst = $invokable(...); +echo $invokableFirst(EvalAotCallableRefLvalueBox::$staticValue, 1) . ":" . EvalAotCallableRefLvalueBox::$staticValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:5|9:9|7:7|9:9|12:12|24:24|18:18"); +} + +/// Verifies eval `call_user_func_array()` preserves AOT callable by-reference writeback. +#[test] +fn test_eval_call_user_func_array_aot_callables_write_back_by_ref_args() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallArrayRefBox(5); +$method = [$box, "bump"]; +$value = "7"; +echo call_user_func_array($method, [&$value]) . ":" . gettype($value) . ":" . $value . "|"; +$string = "EvalAotCallArrayRefBox::add"; +$staticValue = "3"; +echo call_user_func_array($string, [&$staticValue, 4]) . ":" . gettype($staticValue) . ":" . $staticValue . "|"; +$namedValue = "5"; +echo call_user_func_array($string, ["delta" => 2, "value" => &$namedValue]) . ":" . gettype($namedValue) . ":" . $namedValue . "|"; +$first = EvalAotCallArrayRefBox::add(...); +$next = "2"; +echo call_user_func_array($first, [&$next, 6]) . ":" . gettype($next) . ":" . $next . "|"; +$invokable = new EvalAotCallArrayRefBox(10); +$invokableValue = "1"; +echo call_user_func_array($invokable, [&$invokableValue, 2]) . ":" . gettype($invokableValue) . ":" . $invokableValue . "|"; +$invokableFirst = $invokable(...); +$firstValue = "2"; +echo call_user_func_array($invokableFirst, [&$firstValue, 3]) . ":" . gettype($firstValue) . ":" . $firstValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:integer:12|7:integer:7|7:integer:7|8:integer:8|13:integer:13|15:integer:15" + ); +} + +/// Verifies eval `call_user_func()` warns and passes eval-declared by-ref params by value. +#[test] +fn test_eval_call_user_func_eval_function_by_ref_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#"append(...); +$firstValue = "b"; +echo call_user_func($first, $firstValue) . ":" . $firstValue . "|"; +$staticFirst = EvalCufMethodRefBox::add(...); +$staticValue = 4; +echo call_user_func($staticFirst, $staticValue) . ":" . $staticValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "ax:a|5:3|qi:q|bx:b|6:4"); + for warning in [ + "EvalCufMethodRefBox::append(): Argument #1 ($value) must be passed by reference, value given", + "EvalCufMethodRefBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalCufMethodRefBox::__invoke(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies eval `call_user_func()` degrades AOT method by-ref params to by-value. +#[test] +fn test_eval_call_user_func_aot_method_by_ref_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#"bump(...); +$firstValue = 5; +echo call_user_func($first, $firstValue) . ":" . $firstValue . "|"; +$staticFirst = EvalAotCufMethodRefBox::add(...); +$staticFirstValue = 9; +echo call_user_func($staticFirst, $staticFirstValue, 1) . ":" . $staticFirstValue . "|"; +$invokable = new EvalAotCufMethodRefBox(); +$invokableValue = 6; +echo call_user_func($invokable, $invokableValue, 4) . ":" . $invokableValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:3|7:4|10:8|7:5|10:9|10:6"); + for warning in [ + "EvalAotCufMethodRefBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalAotCufMethodRefBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalAotCufMethodRefBox::__invoke(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies eval `ReflectionClass::newInstanceArgs()` preserves AOT constructor by-reference writeback. +#[test] +fn test_eval_reflection_new_instance_args_aot_constructor_writes_back_by_ref_args() { + let out = compile_and_run_capture( + r#"seen = $value; + } +} + +echo eval('$ref = new ReflectionClass("EvalAotReflectCtorArrayRefBox"); +$value = "3"; +$box = $ref->newInstanceArgs([&$value, 4]); +echo $box->seen . ":" . gettype($value) . ":" . $value . "|"; +$named = "5"; +$box = $ref->newInstanceArgs(["delta" => 2, "value" => &$named]); +echo $box->seen . ":" . gettype($named) . ":" . $named;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:integer:7|7:integer:7"); +} + +/// Verifies eval can pass runtime PHP callbacks to generated/AOT callable-typed methods. +#[test] +fn test_eval_fragment_passes_callable_args_to_aot_methods() { + let out = compile_and_run_capture( + r##"value = $callback("C"); + } + + public function apply(callable $callback) { + return $callback("M"); + } + + public static function applyStatic(callable $callback) { + return $callback("S"); + } +} + +echo eval('$box = new EvalAotCallableArgBox("eval_aot_callable_arg_suffix"); +return $box->value . ":" . + $box->apply("eval_aot_callable_arg_suffix") . ":" . + EvalAotCallableArgBox::applyStatic("eval_aot_callable_arg_suffix");'); +echo ":"; +echo eval('$static = [EvalAotCallableArgTarget::class, "suffix"]; +$box = new EvalAotCallableArgBox($static); +return $box->value . ":" . + $box->apply("EvalAotCallableArgTarget::suffix") . ":" . + EvalAotCallableArgBox::applyStatic($static);'); +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$instance = [$target, "instanceSuffix"]; +$box = new EvalAotCallableArgBox($instance); +return $box->value . ":" . + $box->apply($instance) . ":" . + EvalAotCallableArgBox::applyStatic($instance);'); +echo ":"; +echo eval('$invokable = new EvalAotCallableArgTarget(); +$box = new EvalAotCallableArgBox($invokable); +return $box->value . ":" . + $box->apply($invokable) . ":" . + EvalAotCallableArgBox::applyStatic($invokable);'); +echo ":"; +echo eval('$function = eval_aot_callable_arg_suffix(...); +$box = new EvalAotCallableArgBox($function); +return $box->value . ":" . + $box->apply($function) . ":" . + EvalAotCallableArgBox::applyStatic($function);'); +echo ":"; +echo eval('$static = EvalAotCallableArgTarget::suffix(...); +$box = new EvalAotCallableArgBox($static); +return $box->value . ":" . + $box->apply($static) . ":" . + EvalAotCallableArgBox::applyStatic($static);'); +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$instance = $target->instanceSuffix(...); +$box = new EvalAotCallableArgBox($instance); +return $box->value . ":" . + $box->apply($instance) . ":" . + EvalAotCallableArgBox::applyStatic($instance);'); +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$invokable = $target(...); +$box = new EvalAotCallableArgBox($invokable); +return $box->value . ":" . + $box->apply($invokable) . ":" . + EvalAotCallableArgBox::applyStatic($invokable);'); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#:C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#" + ); +} + +/// Verifies eval static calls and static callables dispatch public AOT static methods. +#[test] +fn test_eval_fragment_dispatches_aot_static_methods() { + let out = compile_and_run_capture( + r#"getMessage(); +} +"#, + ); + assert_eq!( + out, + "Non-static method EvalAotNonStaticStaticSyntaxBox::run() cannot be called statically" + ); +} + +/// Verifies eval fragments can call private AOT static methods from the declaring scope. +#[test] +fn test_eval_fragment_can_call_private_aot_static_method_from_declaring_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT static methods from child scopes. +#[test] +fn test_eval_fragment_rejects_private_aot_static_method_from_child_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +(new EvalPrivateAotStaticMethodChild())->run(); +"#, + ); + assert_eq!( + out, + "Error:Call to private method EvalPrivateAotStaticMethodBase::secret() from scope EvalPrivateAotStaticMethodChild" + ); +} + +/// Verifies eval fragments can call inherited protected AOT static methods from child scopes. +#[test] +fn test_eval_fragment_can_call_protected_aot_static_method_from_child_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval static method callables can call protected AOT methods from child scopes. +#[test] +fn test_eval_fragment_callable_can_call_protected_aot_static_method_from_child_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT static methods between sibling scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_static_method_from_sibling_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +(new EvalProtectedAotStaticMethodRight())->run(); +"#, + ); + assert_eq!( + out, + "Error:Call to protected method EvalProtectedAotStaticMethodLeft::add() from scope EvalProtectedAotStaticMethodRight" + ); +} + +/// Verifies eval static dispatch passes AOT static method arguments on the caller stack. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_stack_string_arg() { + let out = compile_and_run( + r#"join(right: "B", left: "A");'); + } + + public function join(string $left, string $right): string { + return $left . $right; + } +} + +echo (new EvalAotNamedMethodBox())->run(); +"#, + ); + assert_eq!(out, "AB"); +} + +/// Verifies eval dispatches generated/AOT instance methods with untyped by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#"mutate($value); +return $value;'); +"#, + ); + assert_eq!(out, "15"); +} + +/// Verifies eval dispatches generated/AOT instance methods with typed scalar by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_typed_by_ref_args() { + let out = compile_and_run( + r#"mutate($value, $flag); +return $value . ":" . ($flag ? "T" : "F");'); +"#, + ); + assert_eq!(out, "15:F"); +} + +/// Verifies eval writes nullable-int by-reference AOT method results back as boxed eval values. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_by_ref_arg() { + let out = compile_and_run( + r#"clear($value); +return $value === null ? "N" : "bad";'); +"#, + ); + assert_eq!(out, "N"); +} + +/// Verifies eval writes nullable scalar by-reference AOT method results back to eval variables. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_by_ref_args() { + let out = compile_and_run( + r#"mutate($name, $flag, $ratio); +$first = $name . ":" . ($flag ? "T" : "F") . ":" . $ratio; +$name = null; +$flag = null; +$ratio = null; +$box->mutate($name, $flag, $ratio); +return $first . ":" . $name . ":" . ($flag ? "T" : "F") . ":" . $ratio;'); +"#, + ); + assert_eq!(out, "eval-method:T:3:method:T:1.5"); +} + +/// Verifies eval dispatches generated/AOT instance methods with string by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_string_by_ref_arg() { + let out = compile_and_run( + r#"mutate($value); +return $value;'); +"#, + ); + assert_eq!(out, "eval-method"); +} + +/// Verifies eval dispatches generated/AOT instance methods with array by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_array_by_ref_arg() { + let out = compile_and_run( + r#"append($items); +$afterAppend = count($items) . ":" . $items[2]; +$box->replace($items); +return $afterAppend . ":" . count($items) . ":" . $items[0] . ":" . $items[1];'); +"#, + ); + assert_eq!(out, "3:3:2:4:5"); +} + +/// Verifies eval dispatches generated/AOT instance methods with object by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_object_by_ref_arg() { + let out = compile_and_run( + r#"value = 7; + } + + public function replace(EvalAotObjectRefMethodPayload &$payload): void { + $payload = new EvalAotObjectRefMethodPayload(); + $payload->value = 9; + } +} + +echo eval('$box = new EvalAotObjectRefMethodBox(); +$payload = new EvalAotObjectRefMethodPayload(); +$box->mutate($payload); +$afterMutate = $payload->value; +$box->replace($payload); +return $afterMutate . ":" . $payload->value;'); +"#, + ); + assert_eq!(out, "7:9"); +} + +/// Verifies eval dispatches generated/AOT instance methods with iterable by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#"replace($items); +return is_iterable($items) . ":" . count($items) . ":" . $items[0] . ":" . $items[1];'); +"#, + ); + assert_eq!(out, "1:2:4:5"); +} + +/// Verifies eval preserves string values passed through an untyped AOT method parameter. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { + let out = compile_and_run( + r#"relay("abc"); +return gettype($value) . ":" . $value;'); +"#, + ); + assert_eq!(out, "string:abc"); +} + +/// Verifies eval preserves array values passed through an untyped AOT method parameter. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_array_arg() { + let out = compile_and_run( + r#"relay([]); +return gettype($value) . ":" . (is_array($value) ? count($value) : 9);'); +"#, + ); + assert_eq!(out, "array:0"); +} + +/// Verifies eval binds named arguments before dispatching an AOT static method. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_named_args() { + let out = compile_and_run( + r#"value = 8; + } +} + +echo eval('$payload = new EvalAotObjectRefStaticPayload(); +EvalAotObjectRefStaticBox::mutate($payload); +return $payload->value;'); +"#, + ); + assert_eq!(out, "8"); +} + +/// Verifies eval dispatches generated/AOT static methods with iterable by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#" "static"]; + } +} + +echo eval('$items = [6, 7]; +EvalAotIterableRefStaticBox::replace($items); +return is_iterable($items) . ":" . $items["name"];'); +"#, + ); + assert_eq!(out, "1:static"); +} + +/// Verifies eval binds named arguments before dispatching an AOT constructor. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_named_args() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +echo eval('$box = new EvalDynamicNewNamedCtor(right: "F", left: "E"); return $box->label;'); +"#, + ); + assert_eq!(out, "EF"); +} + +/// Verifies eval object construction accepts runtime class-name variables. +#[test] +fn test_eval_dynamic_new_accepts_runtime_class_name() { + let out = compile_and_run( + r#"label = "aot:" . $label; + } +} + +eval('class EvalRuntimeNewTarget { + public string $label; + public function __construct(string $label) { + $this->label = "eval:" . $label; + } +} + +$evalClass = "EvalRuntimeNewTarget"; +$evalBox = new $evalClass("Ada"); +echo $evalBox->label; echo "|"; + +$aotClass = "EvalAotRuntimeNewTarget"; +$aotBox = new $aotClass("Turing"); +echo $aotBox->label; echo "|"; + +$prototype = new EvalRuntimeNewTarget("proto"); +$copy = new $prototype("Grace"); +echo $copy->label;'); +"#, + ); + assert_eq!(out, "eval:Ada|aot:Turing|eval:Grace"); +} + +/// Verifies eval object construction accepts parenthesized class expressions and optional constructor parentheses. +#[test] +fn test_eval_dynamic_new_accepts_expression_class_name() { + let out = compile_and_run( + r#"label = $label; + } +} + +function eval_expression_new_target() { + return "EvalExpressionNewTarget"; +} + +$direct = new (eval_expression_new_target())("Ada"); +$class = "EvalExpressionNewTarget"; +$withoutDynamicParens = new $class; +$withoutNamedParens = new EvalExpressionNewTarget; +return $direct->label . "|" . $withoutDynamicParens->label . "|" . $withoutNamedParens->label;'); +"#, + ); + assert_eq!(out, "Ada|default|default"); +} + +/// Verifies eval object construction passes object-typed arguments to AOT constructors. +#[test] +fn test_eval_dynamic_new_passes_object_arg_to_constructor() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalDynamicNewObjectArgTarget { + public string $label = ""; + public function __construct(EvalDynamicNewObjectArgSource $source) { + $this->label = $source->name; + } +} + +echo eval('$source = new EvalDynamicNewObjectArgSource("Ada"); +$box = new EvalDynamicNewObjectArgTarget($source); +return $box->label;'); +"#, + ); + assert_eq!(out, "Ada"); +} + +/// Verifies eval object construction passes array-typed arguments to AOT constructors. +#[test] +fn test_eval_dynamic_new_passes_array_arg_to_constructor() { + let out = compile_and_run( + r#"count = count($items); + } +} + +echo eval('$box = new EvalDynamicNewArrayArgTarget([1, 2, 3, 4]); +return $box->count;'); +"#, + ); + assert_eq!(out, "4"); +} + +/// Verifies eval-declared methods resolve `new self/static/parent` through the bridge. +#[test] +fn test_eval_declared_methods_construct_relative_class_names() { + let out = compile_and_run( + r#"label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { + public function parentFactory() { return new parent("parent"); } +} +$child = new EvalRelativeFactoryChild("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label;'); +"#, + ); + assert_eq!( + out, + "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" + ); +} + +/// Verifies eval-declared methods resolve `new self/static/parent` inside namespaces. +#[test] +fn test_eval_declared_methods_construct_namespaced_relative_class_names() { + let out = compile_and_run( + r#"label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class Child extends Base { + public function parentFactory() { return new parent("parent"); } +} +$child = new Child("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label;'); +"#, + ); + assert_eq!( + out, + "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" + ); +} + +/// Verifies eval supports PHP's legacy `var` public property marker through the bridge. +#[test] +fn test_eval_declared_legacy_var_properties() { + let out = compile_and_run( + r#"getDefaultProperties(); +echo $object->plain; echo ":"; +echo $plain->isPublic() ? "P" : "p"; echo ":"; +echo $plain->hasType() ? "T" : "t"; echo ":"; +echo $count->isPublic() ? "C" : "c"; echo ":"; +echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; +echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; +echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; +echo $object->label; echo ":"; +echo $label->isPublic() ? "L" : "l"; echo ":"; +echo $label->getType()->getName();'); +"#, + ); + assert_eq!(out, "p:P:t:C:int:N:null:trait:L:string"); +} + +/// Verifies eval supports PHP comma-separated instance, static, and trait properties. +#[test] +fn test_eval_declared_comma_separated_properties() { + let out = compile_and_run( + r#"a + $this->b + self::$s + self::$t; } +} +trait EvalMultiPropertyTrait { + public int $x = 5, $y = 6; +} +class EvalMultiPropertyTraitBox { + use EvalMultiPropertyTrait; + public function sum() { return $this->x + $this->y; } +} +$box = new EvalMultiPropertyBox(); +$traitBox = new EvalMultiPropertyTraitBox(); +echo $box->a . $box->b . ":"; +echo EvalMultiPropertyBox::$s . EvalMultiPropertyBox::$t . ":"; +echo $traitBox->x . $traitBox->y . ":"; +return $box->sum() + $traitBox->sum();'); +"#, + ); + assert_eq!(out, "12:34:56:21"); +} + +/// Verifies native callable probes can see functions declared by eval after the barrier. +#[test] +fn test_eval_declared_function_is_visible_to_callable_probes() { + let out = compile_and_run( + r#" false, "class" => "\EvalClassExistsProbe"]) ? "Y" : "N"; +echo class_exists(class: "MissingEvalClassExistsProbe", autoload: false) ? "Y" : "N";'); +"#, + ); + assert_eq!(out, "YYYYYN"); +} + +/// Verifies eval `get_declared_*()` exposes generated AOT class-like names. +#[test] +fn test_eval_get_declared_symbols_exposes_aot_metadata() { + let out = compile_and_run( + r#" false, "interface" => "\EvalInterfaceExistsProbe"]) ? "Y" : "N"; +echo function_exists("interface_exists");'); +"#, + ); + assert_eq!(out, "YYYNUBcYY1"); +} + +/// Verifies eval `trait_exists()` and `enum_exists()` probe generated AOT metadata. +#[test] +fn test_eval_fragment_trait_enum_exists_probe_aot_metadata() { + let out = compile_and_run( + r#" false, "enum" => "\EvalEnumExistsProbe"]) ? "E" : "e"; +echo trait_exists(trait: "MissingEvalTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEvalEnum", autoload: false) ? "E" : "e"; +echo function_exists("trait_exists"); echo function_exists("enum_exists");'); +"#, + ); + assert_eq!(out, "TTtEEeTEte11"); +} + +/// Verifies eval fragments can declare and use backed enums through the bridge. +#[test] +fn test_eval_fragment_declares_enum_cases_and_methods() { + let out = compile_and_run( + r#"name . ":" . $this->value; } + public static function fallback() { return self::Red; } +} +$cases = EvalDynColor::cases(); +echo enum_exists("evaldyncolor") ? "E" : "e"; +echo class_exists("EvalDynColor") ? "C" : "c"; +echo count($cases); +echo $cases[1] === EvalDynColor::Green ? "G" : "g"; +echo EvalDynColor::Green->label(); +echo EvalDynColor::from("r") === EvalDynColor::Red ? "F" : "f"; +echo is_null(EvalDynColor::tryFrom("missing")) ? "N" : "n"; +echo is_a(EvalDynColor::Red, "EvalDynLabel") ? "I" : "i";'); +"#, + ); + assert_eq!(out, "EC2Gcolor:Green:gFNI"); +} + +/// Verifies eval enums can import trait methods and report direct trait metadata. +#[test] +fn test_eval_declared_enum_trait_use() { + let out = compile_and_run( + r#"name; } + public static function suffix() { return "S"; } +} +enum EvalDynTraitEnum { + use EvalDynEnumTrait { + label as private hiddenLabel; + } + case Ready; + public function read() { return $this->label() . ":" . $this->hiddenLabel(); } +} +echo EvalDynTraitEnum::Ready->read(); echo ":"; +echo EvalDynTraitEnum::suffix(); echo ":"; +$ref = new ReflectionClass("EvalDynTraitEnum"); +$traits = $ref->getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenLabel"] . ":"; +$uses = class_uses(EvalDynTraitEnum::Ready); +echo count($uses) . ":" . $uses["EvalDynEnumTrait"] . ":"; +echo EvalDynTraitEnum::Ready->label();'); +"#, + ); + assert_eq!( + out, + "Ready:Ready:S:1:EvalDynEnumTrait:EvalDynEnumTrait::label:1:EvalDynEnumTrait:Ready" + ); + + let err = compile_and_run_expect_failure( + r#"cases()) ? "cases" : "bad"; echo ":"; +echo EvalDynPureSynthetic::Ready->traitCases(); echo ":"; +echo EvalDynPureSynthetic::from("x"); echo ":"; +echo EvalDynPureSynthetic::Ready->from("x"); echo ":"; +echo EvalDynBackedSynthetic::from("ready")->value; echo ":"; +echo EvalDynBackedSynthetic::Ready->from("ready")->value; echo ":"; +echo EvalDynBackedSynthetic::tryFrom("missing") === null ? "null" : "bad"; echo ":"; +echo EvalDynBackedSynthetic::traitFrom("x"); echo ":"; +echo EvalDynBackedSynthetic::Ready->traitCases(); echo ":"; +echo is_callable([EvalDynBackedSynthetic::Ready, "cases"]) ? "callable" : "bad";'); +"#, + ); + assert_eq!( + out, + "cases:trait-cases:trait-from:trait-from:ready:ready:null:trait-from:trait-cases:callable" + ); + + let err = compile_and_run_expect_failure( + r#"getMethods(ReflectionMethod::IS_STATIC); +echo count($methods) . ":"; +echo $methods[0]->getName() . "/" . $methods[1]->getName() . "/" . $methods[2]->getName() . ":"; +$cases = $ref->getMethod("cases"); +echo $cases->getReturnType() . ":"; +echo count($cases->invoke(null)) . ":"; +$from = new ReflectionMethod("EvalDynReflectSyntheticEnum", "from"); +$params = $from->getParameters(); +echo $from->getDeclaringClass()->getName() . ":"; +echo $from->getNumberOfParameters() . "/" . $from->getNumberOfRequiredParameters() . ":"; +echo $params[0]->getName() . "/" . $params[0]->getType() . ":"; +echo $from->getReturnType() . ":"; +echo $from->invoke(null, "ready")->name . ":"; +$try = ReflectionMethod::createFromMethodName("EvalDynReflectSyntheticEnum::tryFrom"); +echo $try->getReturnType() . ":"; +echo ($try->invokeArgs(null, ["missing"]) === null ? "null" : "bad") . ":"; +$pure = new ReflectionClass("EvalDynReflectPureSyntheticEnum"); +echo count($pure->getMethods()) . ":"; +echo $pure->hasMethod("from") ? "bad" : "nofrom";'); +"#, + ); + assert_eq!( + out, + "3:cases/from/tryFrom:array:1:EvalDynReflectSyntheticEnum:1/1:value/string|int:static:Ready:?static:null:1:nofrom" + ); +} + +/// Verifies eval enums support user interfaces derived from PHP enum marker interfaces. +#[test] +fn test_eval_declared_enum_marker_interface_inheritance() { + let out = compile_and_run( + r#"getInterfaceNames(); +echo count($backedInterfaces) . ":" . $backedInterfaces[0] . ":"; +echo $backedInterfaces[1] . ":" . $backedInterfaces[2] . ":"; +echo EvalDynMarkedBacked::Ready->value;'); +"#, + ); + assert_eq!( + out, + "UB2:EvalDynUnitMarker:UnitEnum:3:EvalDynBackedMarker:UnitEnum:BackedEnum:ready" + ); + + let err = compile_and_run_expect_failure( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "ValueError:\"live\" is not a valid backing value for enum EvalDynStatus" + ); +} + +/// Verifies eval-declared enums reject magic methods PHP forbids on enums. +#[test] +fn test_eval_fragment_rejects_forbidden_enum_magic_method() { + let err = compile_and_run_expect_failure( + r#" $object, "class" => "EvalRelationParent"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingEvalRelation", allow_string: false) ? "Y" : "N"; +echo function_exists("is_a"); echo function_exists("is_subclass_of");'); +"#, + ); + assert_eq!(out, "YYYNYYYYN11"); +} + +/// Verifies eval class-relation builtins materialize generated/AOT metadata. +#[test] +fn test_eval_class_relation_builtins_expose_aot_metadata() { + let out = compile_and_run_capture( + r#" "EvalAotRelationChild"]); +foreach ($stringParents as $name) { echo $name . ","; } +echo ":"; +echo function_exists("class_implements"); echo function_exists("class_parents");'); +class_alias("EvalAotRelationChild", "EvalAotRelationAlias"); +eval('echo ":"; +$aliasImplements = class_implements("EvalAotRelationAlias"); +ksort($aliasImplements); +foreach ($aliasImplements as $name) { echo $name . ","; } +echo ":"; +$aliasParents = class_parents("EvalAotRelationAlias"); +foreach ($aliasParents as $name) { echo $name . ","; }'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationBaseIface,:EvalAotRelationOuterTrait,:EvalAotRelationInnerTrait,:EvalAotRelationMid,EvalAotRelationBase,:EvalAotRelationMid,EvalAotRelationBase,:11:EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationChild,EvalAotRelationMid,EvalAotRelationBase," + ); +} + +/// Verifies eval `class_uses()` accepts eval-declared trait targets and aliases. +#[test] +fn test_eval_class_uses_exposes_eval_trait_targets() { + let out = compile_and_run( + r#"x = $x; } + public function read() { return $this->x; } +} +eval('class EvalRuntimeParentChild extends EvalRuntimeParentBase { + public function own() { return $this->read() + 1; } +} +$box = new EvalRuntimeParentChild(6); +echo get_class($box); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; +echo is_a($box, "EvalRuntimeParentBase") ? "P" : "p"; echo ":"; +echo is_subclass_of($box, "EvalRuntimeParentBase") ? "S" : "s"; echo ":"; +echo is_subclass_of("EvalRuntimeParentChild", "EvalRuntimeParentBase") ? "N" : "n"; echo ":"; +echo $box->read(); echo ":"; +echo $box->own(); echo ":"; +$parent = (new ReflectionClass("EvalRuntimeParentChild"))->getParentClass(); +echo $parent ? $parent->getName() : "missing";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalRuntimeParentChild:EvalRuntimeParentBase:D:P:S:N:6:7:EvalRuntimeParentBase" + ); +} + +/// Verifies eval-declared children can call inherited protected AOT instance methods. +#[test] +fn test_eval_declared_child_calls_inherited_protected_aot_instance_method() { + let out = compile_and_run_capture( + r#"add(3); + } +} +$box = new EvalRuntimeProtectedMethodChild(); +$box->run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5"); +} + +/// Verifies eval-declared children can call inherited protected AOT static methods. +#[test] +fn test_eval_declared_child_calls_inherited_protected_aot_static_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "6"); +} + +/// Verifies `parent::` in eval children can call inherited non-static AOT methods on `$this`. +#[test] +fn test_eval_declared_child_parent_static_syntax_calls_aot_instance_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "parent"); +} + +/// Verifies `parent::` in eval children bridges protected static AOT method scope. +#[test] +fn test_eval_declared_child_parent_static_syntax_calls_aot_static_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "parent-static"); +} + +/// Verifies eval-declared children can read and write inherited protected AOT properties. +#[test] +fn test_eval_declared_child_accesses_inherited_protected_aot_property() { + let out = compile_and_run_capture( + r#"x) ? "I:" : "i:"; + $this->x = 7; + echo $this->x; + } +} +$box = new EvalRuntimeProtectedPropertyChild(); +$box->run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "I:7"); +} + +/// Verifies eval-declared children can read and write inherited protected AOT static properties. +#[test] +fn test_eval_declared_child_accesses_inherited_protected_aot_static_property() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "S:8"); +} + +/// Verifies eval-declared children can read inherited protected AOT class constants. +#[test] +fn test_eval_declared_child_reads_inherited_protected_aot_class_constant() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "9"); +} + +/// Verifies eval-declared children can run inherited protected AOT constructors. +#[test] +fn test_eval_declared_child_runs_inherited_protected_aot_constructor() { + let out = compile_and_run_capture( + r#"x = $x + 2; + } +} + +eval('class EvalRuntimeProtectedConstructorChild extends EvalRuntimeProtectedConstructorParent { + public static function make() { + return new self(3); + } +} +$box = EvalRuntimeProtectedConstructorChild::make(); +echo $box->x;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5"); +} + +/// Verifies eval-declared classes inherit AOT callable object and method behavior. +#[test] +fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { + let out = compile_and_run_capture( + r#"prefix = $prefix; } + public function read(string $value = "R"): string { return $this->prefix . ":" . $value; } + public function __invoke(string $left = "A", string $right = "B"): string { + return $this->prefix . ":" . $left . $right; + } +} + +eval('class EvalRuntimeCallableChild extends EvalAotCallableParent {} +$box = new EvalRuntimeCallableChild("box"); +echo is_callable($box) ? "I:" : "bad:"; +echo $box(right: "D", left: "C") . ":"; +$first = $box(...); +echo $first("E", "F") . ":"; +echo call_user_func($box, "G", "H") . ":"; +echo call_user_func_array($box, ["right" => "J", "left" => "I"]) . ":"; +echo is_callable([$box, "read"]) ? "M:" : "bad:"; +echo call_user_func([$box, "read"], "K") . ":"; +echo call_user_func_array([$box, "read"], ["value" => "L"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "I:box:CD:box:EF:box:GH:box:IJ:M:box:K:box:L"); +} + +/// Verifies eval first-class callables retain access to inherited protected AOT methods. +#[test] +fn test_eval_declared_child_first_class_callable_inherited_protected_aot_method() { + let out = compile_and_run_capture( + r#"hidden(...); + } +} +$box = new EvalProtectedCallableChild(); +$public = $box->read(...); +echo $public("A") . ":"; +$hidden = $box->makeHidden(); +echo is_callable($hidden) ? "callable:" : "bad:"; +echo call_user_func($hidden, "B") . ":"; +echo $hidden("C");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "P:A:callable:H:B:H:C"); +} + +/// Verifies first-class AOT object callables keep eval-child late-static scope. +#[test] +fn test_eval_declared_child_first_class_aot_method_callable_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"hiddenScope(...); + } +} +$box = new EvalAotCallableScopeChild(); +$hidden = $box->makeHidden(); +echo $hidden() . "|"; +echo call_user_func($hidden);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotCallableScopeParent:EvalAotCallableScopeChild:EvalAotCallableScopeChild|\ +EvalAotCallableScopeParent:EvalAotCallableScopeChild:EvalAotCallableScopeChild" + ); +} + +/// Verifies eval-declared classes expose inherited AOT members to OOP introspection. +#[test] +fn test_eval_declared_class_inherits_aot_parent_member_introspection() { + let out = compile_and_run_capture( + r#"getName() . ":" . + (in_array("childRead", $objectMethods) ? "objectChild" : "bad");'); + } +} + +eval('class EvalRuntimeIntrospectionChild extends EvalAotIntrospectionParent { + public function childRead() {} + public function childView() { + $methods = get_class_methods($this); + echo in_array("guarded", $methods) ? "P" : "p"; + } +} +$box = new EvalRuntimeIntrospectionChild(); +echo method_exists("EvalRuntimeIntrospectionChild", "guarded") ? "classProtected:" : "bad:"; +echo method_exists($box, "guarded") ? "objectProtected:" : "bad:"; +echo property_exists($box, "pub") ? "objectPublicProp:" : "bad:"; +echo property_exists("EvalRuntimeIntrospectionChild", "prot") ? "classProtectedProp:" : "bad:"; +$outside = get_class_methods("EvalRuntimeIntrospectionChild"); +echo in_array("read", $outside) ? "outsideParent:" : "bad:"; +echo in_array("guarded", $outside) ? "bad:" : "outsideNoProtected:"; +echo in_array("childRead", $outside) ? "outsideChild:" : "bad:"; +$box->childView(); +echo ":"; +echo $box->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P:parentProtected:EvalRuntimeIntrospectionChild:EvalAotIntrospectionParent:EvalRuntimeIntrospectionChild:objectChild" + ); +} + +/// Verifies eval-declared class-like symbols remain visible in generated eval contexts. +#[test] +fn test_eval_declared_class_likes_are_visible_in_aot_nested_eval_context() { + let out = compile_and_run_capture( + r#"view(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CA:IA:TA:EACA:R"); +} + +/// Verifies eval-declared class inheritance uses dynamic methods and metadata. +#[test] +fn test_eval_declared_class_inherits_methods_and_metadata() { + let out = compile_and_run_capture( + r#"base = $base; } + public function sum($n) { return $this->base + $this->tail + $n; } +} +class EvalDynChild extends EvalDynBase implements EvalDynIface { + public int $tail = 4; + public function read($n) { return $this->sum($n); } +} +$box = new EvalDynChild(3); +echo $box->read(5) . ":"; +echo get_parent_class($box) . ":"; +echo is_a($box, "EvalDynBase") ? "isa" : "bad"; echo ":"; +echo is_a($box, "EvalDynIface") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalDynChild") ? "bad" : "self"; echo ":"; +echo is_subclass_of($box, "EvalDynBase") ? "sub" : "bad"; echo ":"; +$parents = class_parents($box); +echo count($parents) . ":" . $parents["EvalDynBase"] . ":"; +$implements = class_implements("EvalDynChild"); +echo count($implements) . ":" . $implements["EvalDynIface"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:EvalDynBase:isa:iface:self:sub:1:EvalDynBase:1:EvalDynIface" + ); +} + +/// Verifies eval static method calls preserve PHP forwarding and late-static binding. +#[test] +fn test_eval_declared_static_method_calls_preserve_forwarding() { + let out = compile_and_run_capture( + r#"read(4) . ":"; +echo $box->label() . ":"; +echo is_a($box, "EvalDynNamedReader") ? "isa" : "bad"; echo ":"; +echo is_subclass_of("EvalDynReaderBox", "EvalDynReader") ? "str" : "bad"; echo ":"; +$implements = class_implements($box); +echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $implements["EvalDynReader"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "iface:notclass:declared:5:box:isa:str:2:EvalDynNamedReader:EvalDynReader" + ); +} + +/// Verifies eval-declared method overrides enforce covariant return types. +#[test] +fn test_eval_declared_method_return_type_override_contracts() { + let out = compile_and_run_capture( + r#"id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2"); + + let err = compile_and_run_expect_failure( + r#"anyInt(7) . ":"; +echo $child->untypedInt("ok") . ":"; +echo $child->maybeInt(null) === null ? "null" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:mixed:ok:null"); + + let err = compile_and_run_expect_failure( + r#"read();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7"); + + let err = compile_and_run_expect_failure( + r#"read(8);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:ok"); + + let err = compile_and_run_expect_failure( + r#"id(); +echo ":" . get_class($child->makeSelf()) . ":"; +$child->done();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "12:EvalReturnRuntimeBase:"); + + let err = compile_and_run_expect_failure( + r#"id();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"done();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"make();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"id();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + +/// Verifies eval-declared abstract classes can defer interface methods to concrete children. +#[test] +fn test_eval_declared_abstract_class_and_final_method_contracts() { + let out = compile_and_run_capture( + r#"read($n) + 1; } +} +class EvalAbstractChild extends EvalAbstractBase { + public function read($n) { return $n + 2; } +} +$box = new EvalAbstractChild(); +echo $box->wrap(5) . ":"; +echo $box->label() . ":"; +echo is_a($box, "EvalAbstractContract") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:base:iface:abstract"); +} + +/// Verifies eval-declared final classes cannot be extended. +#[test] +fn test_eval_declared_final_class_extension_fails() { + let err = compile_and_run_expect_failure( + r#"seed + $n; } +} +class EvalDynamicTraitBox { + use EvalDynamicTrait; + public function read($n) { return $this->add($n) + 1; } +} +$box = new EvalDynamicTraitBox(); +echo $box->read(4) . ":"; +echo trait_exists("EvalDynamicTrait") ? "trait" : "bad"; echo ":"; +$traits = get_declared_traits(); +echo count($traits) . ":" . $traits[0] . ":"; +$uses = class_uses($box); +echo count($uses) . ":" . $uses["EvalDynamicTrait"] . ":"; +echo $box->seed;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "7:trait:1:EvalDynamicTrait:1:EvalDynamicTrait:2" + ); +} + +/// Verifies eval-declared trait adaptations resolve conflicts, aliases, and visibility. +#[test] +fn test_eval_declared_trait_adaptations() { + let out = compile_and_run_capture( + r#"talk() . ":" . $this->talkB() . ":" . $this->hidden(); + } +} +$box = new EvalAdaptBox(); +echo $box->read() . ":"; +echo $box->talk();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:B:secret:A"); +} + +/// Verifies eval trait declarations can compose other eval traits. +#[test] +fn test_eval_declared_trait_uses_trait_composition() { + let out = compile_and_run_capture( + r#"word() . $this->hiddenWord(); } +} +class EvalNestedBox { + use EvalNestedOuter; +} +$box = new EvalNestedBox(); +echo $box->read() . ":"; +$ref = new ReflectionClass("EvalNestedOuter"); +$traits = $ref->getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenWord"] . ":"; +$uses = class_uses($box); +echo count($uses) . ":" . $uses["EvalNestedOuter"] . ":"; +echo $box->word();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "inin:1:EvalNestedInner:EvalNestedInner::word:1:EvalNestedOuter:in" + ); +} + +/// Verifies eval-declared trait visibility adaptations affect bridge access checks. +#[test] +fn test_eval_declared_trait_visibility_adaptation_fails() { + let err = compile_and_run_expect_failure( + r#"hidden();'); +"#, + ); + assert!( + err.contains("Fatal error: uncaught exception"), + "stderr did not contain uncaught throwable diagnostic: {err}" + ); +} + +/// Verifies eval-declared trait aliases follow PHP collision and no-op rules. +#[test] +fn test_eval_declared_trait_alias_collision_rules() { + let out = compile_and_run_capture( + r#"source() . $this->target(); } +} +class EvalAliasNoopBox { + use EvalAliasSource { + source as source; + } +} +$box = new EvalAliasClassCollisionBox(); +echo $box->read() . ":"; +echo (new EvalAliasNoopBox())->source();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "TC:T"); + + let err = compile_and_run_expect_failure( + r#"value = $value; } +} +$box = new EvalTraitPropCompatBox(9); +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "9"); + + let err = compile_and_run_expect_failure( + r#"secret + $n; } + protected function add($n) { return $this->base + $n; } + public function readPrivate($n) { return $this->bump($n); } +} +class EvalVisibilityChild extends EvalVisibilityBase { + public function readProtected($n) { return $this->add($n); } +} +$box = new EvalVisibilityChild(); +echo $box->readPrivate(3) . ":"; +echo $box->readProtected(2);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:7"); +} + +/// Verifies eval OOP introspection builtins preserve PHP visibility and scope rules. +#[test] +fn test_eval_declared_oop_introspection_builtins() { + let out = compile_and_run_capture( + r#"dynamic = "dyn"; +echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; +echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; +echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; +echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; +echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; +echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; +$methods = get_class_methods("EvalOopIntrospectChild"); +sort($methods); +echo implode(",", $methods); echo ":"; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +$object->childView(); echo ":"; +$object->parentView(); echo ":"; +echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; +echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; +echo function_exists("method_exists"); echo function_exists("property_exists"); +echo function_exists("get_class_methods"); echo function_exists("get_object_vars");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + ); +} + +/// Verifies eval `get_class_vars()` exposes visible class-like defaults like PHP. +#[test] +fn test_eval_declared_get_class_vars_builtin() { + let out = compile_and_run_capture( + r#" $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } + public function baseView() { + $vars = get_class_vars(EvalClassVarsBase::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } +} +$outside = get_class_vars("EvalClassVarsChild"); +ksort($outside); +foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +(new EvalClassVarsChild())->childView(); +echo ":"; +(new EvalClassVarsChild())->baseView(); +echo ":"; +$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); +ksort($trait); +foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); +ksort($enum); +foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +echo function_exists("get_class_vars") ? "F" : "f";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" + ); +} + +/// Verifies eval `get_class_vars()` exposes generated/AOT class defaults by scope. +#[test] +fn test_eval_get_class_vars_exposes_aot_defaults_by_scope() { + let out = compile_and_run_capture( + r#" 1]; + public int $baseTyped; + public ?int $baseNullable = null; + public function parentView() { + return eval('$vars = get_class_vars(EvalAotClassVarsChild::class); +ksort($vars); +foreach ($vars as $name => $value) { + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; +}'); + } +} +class EvalAotClassVarsChild extends EvalAotClassVarsBase { + public $childPublic = "cp"; + protected $childProtected = "cq"; + private $childPrivate = "cs"; + public static $childStatic = "childStatic"; + public function childView() { + return eval('$vars = get_class_vars(self::class); +ksort($vars); +foreach ($vars as $name => $value) { + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; +}'); + } +} +eval('$outside = get_class_vars(EvalAotClassVarsChild::class); +ksort($outside); +foreach ($outside as $name => $value) { + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; +} +echo ":";'); +(new EvalAotClassVarsChild())->childView(); +echo ":"; +(new EvalAotClassVarsChild())->parentView(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "baseArray=1|baseNullable=null|basePublic=bp|baseStatic=static|baseTyped=null|childPublic=cp|childStatic=childStatic|:baseArray=1|baseNullable=null|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childPrivate=cs|childProtected=cq|childPublic=cp|childStatic=childStatic|:baseArray=1|baseNullable=null|basePrivate=bs|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childProtected=cq|childPublic=cp|childStatic=childStatic|" + ); +} + +/// Verifies eval OOP introspection builtins honor AOT inherited private-member rules. +#[test] +fn test_eval_oop_introspection_builtins_for_aot_inherited_private_members() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +$object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noOutsideObject:noOutsideClass:noChildParentPrivate:parentPrivate,noParentStatic,noClassPrivate" + ); +} + +/// Verifies eval `property_exists()` applies parent private object-property scope to AOT metadata. +#[test] +fn test_eval_property_exists_exposes_aot_parent_private_object_property_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noOutsideObject:noOutsideClass:noChildParentPrivate:parentPrivate,noParentStatic,noClassPrivate" + ); +} + +/// Verifies eval `get_class_methods()` follows PHP scope visibility for eval-declared metadata. +#[test] +fn test_eval_get_class_methods_exposes_declared_methods_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +(new EvalClassMethodsChild())->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basePublic,childPublic,childView,parentView:baseProtected,basePublic,childPrivate,childProtectedStatic,childPublic,childView,parentView:basePrivate,baseProtected,basePublic,childProtectedStatic,childPublic,childView,parentView" + ); +} + +/// Verifies eval `get_class_methods()` follows PHP scope visibility for generated/AOT metadata. +#[test] +fn test_eval_get_class_methods_exposes_aot_methods_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +(new EvalAotClassMethodsChild())->parentView(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basepublic,basestaticpublic,childpublic,childview,parentview:baseprotected,basepublic,basestaticpublic,childprivate,childprotectedstatic,childpublic,childview,parentview:baseprivate,baseprotected,basepublic,basestaticpublic,childprotectedstatic,childpublic,childview,parentview" + ); +} + +/// Verifies eval `get_object_vars()` skips uninitialized typed properties like PHP. +#[test] +fn test_eval_get_object_vars_skips_uninitialized_declared_properties() { + let out = compile_and_run_capture( + r#"a = 5; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +unset($object->a); +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars));'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PA:b:a,b:b"); +} + +/// Verifies direct reads of uninitialized eval-declared typed properties throw PHP errors. +#[test] +fn test_eval_uninitialized_typed_instance_property_reads_throw_error() { + let out = compile_and_run_capture( + r#"typed; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo $object->nullable; +} catch (Error $e) { + echo $e->getMessage(); +} +echo "|"; +echo is_null($object->defaultNull) ? "default-null" : "bad"; +echo "|"; +echo is_null($object->plain) ? "plain-null" : "bad"; +echo "|"; +$object->typed = 0; +echo $object->typed; +echo "|"; +unset($object->typed); +try { + echo $object->typed; +} catch (Error $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Typed property EvalUninitializedTypedRead::$typed must not be accessed before initialization|\ +Typed property EvalUninitializedTypedRead::$nullable must not be accessed before initialization|\ +default-null|plain-null|0|\ +Typed property EvalUninitializedTypedRead::$typed must not be accessed before initialization" + ); +} + +/// Verifies eval `get_object_vars()` exposes initialized generated/AOT properties by scope. +#[test] +fn test_eval_get_object_vars_exposes_initialized_aot_properties_by_scope() { + let out = compile_and_run_capture( + r#"childView(); echo ":"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "baseNullable,basePublic,childNullable,childPublic,implicit:baseNullable,baseProtected,basePublic,childNullable,childProtected,childPublic,childSecret,implicit:baseNullable,baseProtected,basePublic,baseSecret,childNullable,childProtected,childPublic,implicit" + ); +} + +/// Verifies AOT `get_object_vars()` lets parent scopes see shadowed private slots. +#[test] +fn test_eval_get_object_vars_uses_aot_private_parent_shadowing_scope() { + let out = compile_and_run_capture( + r#"childView(); echo "|"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "value:c|value:c|value:p"); +} + +/// Verifies eval-declared private parent properties keep separate storage when a child shadows them. +#[test] +fn test_eval_declared_private_parent_property_shadowing() { + let out = compile_and_run_capture( + r#"value; } +} +class EvalShadowParent extends EvalShadowGrand { + public $value = 2; + public function parentValue() { return $this->value; } +} +class EvalShadowChild extends EvalShadowParent { + public $value = 3; +} +$box = new EvalShadowChild(); +echo $box->grandValue() . ":"; +echo $box->parentValue() . ":"; +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:3:3"); +} + +/// Verifies eval-declared readonly properties can be initialized only in constructors. +#[test] +fn test_eval_declared_readonly_property_rules() { + let out = compile_and_run_capture( + r#"id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyBox(7); +echo $box->id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7"); + + let err = compile_and_run_capture( + r#"id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyFailBox(7); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr + ); + assert_eq!( + err.stdout, + "Error:Cannot modify readonly property EvalReadonlyFailBox::$id" + ); + + let unset = compile_and_run_capture( + r#"id = $id; } +} +$box = new EvalReadonlyUnsetBox(7); +try { + unset($box->id); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + unset.success, + "program failed: stdout={:?} stderr={}", + unset.stdout, unset.stderr + ); + assert_eq!( + unset.stdout, + "Error:Cannot unset readonly property EvalReadonlyUnsetBox::$id" + ); +} + +/// Verifies eval-declared readonly classes initialize typed properties and can inherit readonly parents. +#[test] +fn test_eval_declared_readonly_class_initializes_and_inherits() { + let out = compile_and_run_capture( + r#"id = $id; } + public function id() { return $this->id; } +} +readonly class EvalReadonlyClassChild extends EvalReadonlyClassBox {} +$box = new EvalReadonlyClassBox(7); +$child = new EvalReadonlyClassChild(9); +echo $box->id() . ":" . $child->id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:9"); +} + +/// Verifies eval-declared readonly classes leave static properties mutable. +#[test] +fn test_eval_declared_readonly_class_static_properties_remain_mutable() { + let static_out = compile_and_run_capture( + r#"id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyClassFailBox(7); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr + ); + assert_eq!( + err.stdout, + "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" + ); +} + +/// Verifies eval-declared readonly classes reject dynamic property creation. +#[test] +fn test_eval_declared_readonly_class_rejects_dynamic_property() { + let dynamic_err = compile_and_run_capture( + r#"id = $id; } +} +$box = new EvalReadonlyClassDynamicFailBox(7); +try { + $box->dynamic = 8; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + dynamic_err.success, + "program failed: stdout={:?} stderr={}", + dynamic_err.stdout, dynamic_err.stderr + ); + assert_eq!( + dynamic_err.stdout, + "Error:Cannot create dynamic property EvalReadonlyClassDynamicFailBox::$dynamic" + ); +} + +/// Verifies eval-declared readonly classes reject the global `AllowDynamicProperties` marker. +#[test] +fn test_eval_declared_readonly_class_rejects_allow_dynamic_properties() { + let attribute_err = compile_and_run_expect_failure( + r#"dynamic = 8;'); +"#, + ); + assert!( + magic.success, + "program failed: stdout={:?} stderr={}", + magic.stdout, magic.stderr + ); + assert_eq!(magic.stdout, "dynamic:8"); +} + +/// Verifies eval-declared readonly classes cannot extend non-readonly parents. +#[test] +fn test_eval_declared_readonly_class_rejects_non_readonly_parent() { + let parent_err = compile_and_run_expect_failure( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalHookChild extends EvalHookName { + public function shout() { return $this->value . "?"; } +} +$box = new EvalHookChild(); +$box->value = "Ada"; +echo $box->value . ":" . $box->shout();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada!:Ada!?"); + + let err = compile_and_run_capture( + r#" 42; + } +} +$box = new EvalHookReadOnly(); +try { + $box->answer = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr + ); + assert_eq!( + err.stdout, + "Error:Property EvalHookReadOnly::$answer is read-only" + ); +} + +/// Verifies eval-declared by-reference get hook syntax reads through the accessor. +#[test] +fn test_eval_declared_by_ref_get_property_hook() { + let out = compile_and_run_capture( + r#" $this->first . " " . $this->last; + } +} +$person = new EvalByRefGetHookPerson(); +echo $person->full;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada Lovelace"); +} + +/// Verifies eval-declared short set property hooks store their expression result. +#[test] +fn test_eval_declared_short_set_property_hooks() { + let out = compile_and_run_capture( + r#" $this->value; + set => trim($value); + } +} +class EvalShortSetHookLabel { + public string $text { + get => $this->text; + set(string $raw) => strtoupper($raw); + } +} +$name = new EvalShortSetHookName(); +$name->value = " Ada "; +echo "[" . $name->value . "]:"; +$label = new EvalShortSetHookLabel(); +$label->text = "hi"; +echo $label->text;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "[Ada]:HI"); +} + +/// Verifies eval-declared set-hook parameter types stay compatible with property writes. +#[test] +fn test_eval_declared_property_set_hook_parameter_type_compatibility() { + let out = compile_and_run_capture( + r#" $this->value; + set(mixed $raw) => $raw; + } +} +$box = new EvalWideSetHookParam(); +$box->value = "Ada"; +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada"); + + for source in [ + r#" $raw; + } +}'); +"#, + r#" $raw; + } +}'); +"#, + ] { + let err = compile_and_run_expect_failure(source); + assert!( + err.contains("Fatal error: eval()"), + "stderr did not contain eval fatal diagnostic: {err}" + ); + } +} + +/// Verifies eval-declared nullsafe and mixed-case property hook reads stay routed. +#[test] +fn test_eval_declared_nullsafe_and_mixed_case_property_hooks() { + let out = compile_and_run_capture( + r#" $this->first . " " . $this->last; + } +} +class EvalMixedCaseHookBox { + private int $store = 0; + public int $Total { + get { return $this->store; } + } + public function set(int $value) { $this->store = $value; } +} +function eval_hook_describe($person) { + return $person?->full ?? "(none)"; +} +$person = new EvalNullsafeHookPerson(); +$box = new EvalMixedCaseHookBox(); +$box->set(5); +echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada Lovelace|(none)|5"); +} + +/// Verifies eval-declared magic property methods handle missing and inaccessible properties. +#[test] +fn test_eval_declared_magic_property_methods() { + let out = compile_and_run_capture( + r#"secret; } + protected function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "read:" . $name; + } + private function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPropertyBox(); +echo $box->readOwn() . ":"; +echo $box->secret . ":"; +echo $box->missing . ":"; +$box->secret = "new"; +$box->other = "B"; +$box->events = $box->events . "public;"; +echo $box->events;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "raw:read:secret:read:missing:get:secret;get:missing;set:secret=new;set:other=B;public;" + ); +} + +/// Verifies eval reads existing dynamic properties before falling back to `__get`. +#[test] +fn test_eval_declared_magic_get_preserves_existing_dynamic_property() { + let out = compile_and_run_capture( + r#"known = "plain"; +echo $box->known . ":"; +echo $box->missing;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "plain:magic:missing"); +} + +/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. +#[test] +fn test_eval_declared_magic_isset_empty_and_unset_property_methods() { + let out = compile_and_run_capture( + r#"events = $this->events . "isset:" . $name . ";"; + return $name !== "no"; + } + protected function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return $name === "empty" ? "" : "value:" . $name; + } + private function __unset($name) { + $this->events = $this->events . "unset:" . $name . ";"; + } +} +$box = new EvalMagicPropertyProbeBox(); +echo isset($box->present) ? "P" : "p"; echo ":"; +echo isset($box->nullish) ? "N" : "n"; echo ":"; +echo isset($box->secret) ? "S" : "s"; echo ":"; +echo isset($box->no) ? "bad" : "no"; echo ":"; +echo empty($box->secret) ? "bad" : "filled"; echo ":"; +echo empty($box->empty) ? "empty" : "bad"; echo ":"; +unset($box->present); +unset($box->secret); +unset($box->missing); +echo isset($box->present) ? "bad" : "unset"; echo ":"; +echo $box->events;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "P:n:S:no:filled:empty:unset:isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" + ); +} + +/// Verifies eval-declared interface property hook contracts validate class properties. +#[test] +fn test_eval_declared_interface_property_hook_contracts() { + let out = compile_and_run_capture( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalIfacePlainBox implements EvalIfaceHookContract { + public string $value = "Grace"; +} +$box = new EvalIfaceHookBox(); +$box->value = "Ada"; +$plain = new EvalIfacePlainBox(); +echo $box->name . ":" . $box->value . ":" . $plain->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "box:Ada!:Grace"); + + let err = compile_and_run_expect_failure( + r#" 42; + } +}'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"sides; + } +} +abstract class EvalPlainAbstractPolygon extends EvalPlainAbstractShape {} +class EvalPlainAbstractSquare extends EvalPlainAbstractPolygon { + public int $sides = 4; +} + +abstract class EvalPlainAbstractEntity { + abstract public int $id { get; set; } +} +class EvalPlainAbstractUser extends EvalPlainAbstractEntity { + public function __construct(public int $id) {} +} + +abstract class EvalPlainAbstractReadonlyBase { + abstract public int $value { get; } +} +class EvalPlainAbstractReadonlyBox extends EvalPlainAbstractReadonlyBase { + public readonly int $value; + + public function __construct(int $value) { + $this->value = $value; + } +} + +$shape = new EvalPlainAbstractSquare(); +$user = new EvalPlainAbstractUser(7); +$box = new EvalPlainAbstractReadonlyBox(42); +echo $shape->show() . ":" . $user->id . ":" . $box->value;'); +"#, + ); + assert_eq!(out, "4:7:42"); +} + +/// Verifies eval-declared abstract property hook contracts validate concrete subclasses. +#[test] +fn test_eval_declared_abstract_property_hook_contracts() { + let out = compile_and_run_capture( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalPlainAbstractHookBox extends EvalAbstractHookBase { + public string $value = "Grace"; +} +$box = new EvalAbstractHookBox(); +$box->value = "Ada"; +$plain = new EvalPlainAbstractHookBox(); +echo $box->value . ":" . $plain->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada!:Grace"); + + let err = compile_and_run_expect_failure( + r#"getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$missing = 9; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + errors.success, + "program failed: stdout={:?} stderr={}", + errors.stdout, errors.stderr + ); + assert_eq!( + errors.stdout, + "Error:Access to undeclared static property EvalInvalidStaticPropBox::$missing|\ +Error:Access to undeclared static property EvalInvalidStaticPropBox::$instance|\ +Error:Typed static property EvalInvalidStaticPropBox::$typed must not be accessed before initialization|\ +Error:Access to undeclared static property EvalInvalidStaticPropBox::$missing" + ); +} + +/// Verifies invalid eval-declared static method calls throw catchable Error objects. +#[test] +fn test_eval_declared_invalid_static_method_calls_throw_error() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + EvalMissingStaticCallBox::missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalAbstractStaticCallBox::abs(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Non-static method EvalInvalidStaticCallBox::read() cannot be called statically|\ +Error:Call to undefined method EvalMissingStaticCallBox::missing()|\ +Error:Cannot call abstract method EvalAbstractStaticCallBox::abs()" + ); +} + +/// Verifies eval-declared static interface methods are validated and reflected. +#[test] +fn test_eval_declared_static_interface_methods() { + let out = compile_and_run_capture( + r#"getMethods()[0]; +echo $listed->getName() . ":"; +echo $listed->isStatic() ? "static" : "instance"; +echo ":"; +$method = new ReflectionMethod(EvalStaticContract::class, "make"); +echo $method->isStatic() ? "S" : "s";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "S:box:make:static:S"); + + let err = compile_and_run_expect_failure( + r#"label = $left . $right; + } + public function read($left, $right) { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left, $right) { + return $left . "-" . $right; + } +} +$box = new EvalNamedMethodBox(right: "B", left: "A"); +echo $box->read(right: "D", left: "C") . ":"; +$args = ["right" => "F", "left" => "E"]; +echo $box->read(...$args) . ":"; +echo EvalNamedMethodBox::join(right: "H", left: "G");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:C:D:AB:E:F:G-H"); +} + +/// Verifies eval-declared constructors and methods bind constant-expression defaults. +#[test] +fn test_eval_declared_method_constant_default_arguments() { + let out = compile_and_run_capture( + r#"label = $label; + } + public function read() { + return $this->label; + } +} +class EvalDefaultConstBox extends EvalDefaultConstBase { + const LABEL = "box"; + public function __construct($label = self::LABEL) { + $this->label = $label; + } + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; + } + public static function join($label = self::LABEL, $parent = parent::LABEL) { + return $label . "-" . $parent; + } +} +$box = new EvalDefaultConstBox(); +echo $box->read() . ":"; +echo EvalDefaultConstBox::join();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:box-base" + ); +} + +/// Verifies eval-declared constructors and methods bind variadic arguments. +#[test] +fn test_eval_declared_method_variadic_arguments() { + let out = compile_and_run_capture( + r#"label = $parts[0] . $parts["right"]; + } + public function read($head, ...$tail) { + echo count($tail) . ":"; + return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; + } + public static function join(...$items) { + return $items[0] . $items[1]; + } +} +$box = new EvalVariadicMethodBox("A", right: "B"); +echo $box->read("C", "D", named: "E", tail: "F") . ":"; +echo EvalVariadicMethodBox::join("G", "H");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "3:AB:C:D:E:F:GH"); +} + +/// Verifies eval-declared method parameter type hints are enforced through the bridge. +#[test] +fn test_eval_declared_method_parameter_type_hints() { + let out = compile_and_run_capture( + r#"read($dep, "3", 4);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalTypedDep:7"); +} + +/// Verifies eval-declared methods write back by-reference arguments through compiled eval calls. +#[test] +fn test_eval_declared_method_by_ref_arguments() { + let out = compile_and_run_capture( + r#"change($value); +EvalByRefMethodBox::changeStatic($value); +$named = "B"; +$box->changeVariadic($value, named: $named); +$items = ["k" => "C"]; +$box->change($items["k"]); +$prop = new EvalByRefPropertyBox(); +$box->change($prop->value); +$propName = "value"; +$box->change($prop->{$propName}); +echo $ctor . ":" . $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Z-ctor:A-method-static-variadic:B-named:C-method:D-method-method" + ); +} + +/// Verifies eval-declared methods can mutate eval static properties passed by reference. +#[test] +fn test_eval_declared_method_by_ref_static_property_arguments() { + let out = compile_and_run_capture( + r#"set(self::$secret, "secret"); + return self::$secret; + } +} +$changer = new EvalByRefStaticPropertyChanger(); +$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); +echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value) . ":"; +echo EvalByRefStaticPropertyBox::$value . ":"; +$class = "EvalByRefStaticPropertyBox"; +$changer->set($class::$other, "dynamic"); +$name = "third"; +$changer->set($class::${$name}, "name"); +echo EvalByRefStaticPropertyBox::$other . ":"; +echo EvalByRefStaticPropertyBox::$third . ":"; +echo EvalByRefStaticPropertyBox::updatePrivate($changer) . ":"; +$changer->set(EvalAotByRefStaticPropertyBox::$value, "aot-changed"); +echo EvalAotByRefStaticPropertyBox::$value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "right:right:dynamic:name:secret:aot-changed"); +} + +/// Verifies eval methods mutate property array elements passed by reference. +#[test] +fn test_eval_declared_method_by_ref_property_array_element_arguments() { + let out = compile_and_run_capture( + r#" "old"]; +} +eval('class EvalByRefPropertyArrayElementChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefPropertyArrayElementBox { + public $items = ["first" => "old", "same" => "same"]; + public $other = null; + public static $staticItems = ["first" => "static-old", "same" => "static-same"]; +} +$changer = new EvalByRefPropertyArrayElementChanger(); +$box = new EvalByRefPropertyArrayElementBox(); +$changer->set($box->items["first"], "changed"); +$name = "items"; +$changer->set($box->{$name}["dynamic"], "dynamic"); +$changer->set($box->other["created"], "created"); +echo $box->items["first"] . ":" . $box->items["dynamic"] . ":" . $box->other["created"] . ":"; +echo $changer->pair($box->items["same"], $box->items["same"]) . ":" . $box->items["same"] . ":"; +$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); +$class = "EvalByRefPropertyArrayElementBox"; +$staticName = "staticItems"; +$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); +echo EvalByRefPropertyArrayElementBox::$staticItems["first"] . ":"; +echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"] . ":"; +echo $changer->pair( + EvalByRefPropertyArrayElementBox::$staticItems["same"], + EvalByRefPropertyArrayElementBox::$staticItems["same"] +) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"] . ":"; +$changer->set(EvalAotByRefPropertyArrayElementBox::$items["aot"], "aot-changed"); +echo EvalAotByRefPropertyArrayElementBox::$items["aot"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "changed:dynamic:created:right:right:static:static-dynamic:right:right:aot-changed" + ); +} + +/// Verifies eval dynamic static callables dispatch eval-declared static methods. +#[test] +fn test_eval_declared_static_method_dynamic_callables() { + let out = compile_and_run_capture( + r#" "F", "left" => "E"]) . ":"; +$named = "EvalStaticCallableBox::join"; +echo $named(right: "H", left: "G");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:CD:EF:GH"); +} + +/// Verifies eval first-class callable syntax dispatches functions and methods. +#[test] +fn test_eval_first_class_callables_dispatch_functions_and_methods() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + public function add($value) { + return $value + $this->offset; + } + public function keep($value) { + return $value > 2; + } + public function sum($carry, $value) { + return $carry + $value + $this->offset; + } + public function show($value, $key) { + echo $key . $value; + } + public static function join($left, $right) { + return $left . $right; + } + public static function compareDesc($left, $right) { + return $right - $left; + } + public static function label($value) { + return "base:" . $value; + } + public static function relay($value) { + $fn = static::label(...); + return $fn($value); + } +} +class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { + public static function label($value) { + return "child:" . $value; + } +} +$function = eval_fc_double(...); +echo $function(4) . ":"; +echo (strlen(...))("abcd") . ":"; +$box = new EvalFirstClassCallableBase(3); +$method = $box->add(...); +echo $method(4) . ":"; +echo call_user_func($box->add(...), 5) . ":"; +$static = EvalFirstClassCallableBase::join(...); +echo $static(right: "B", left: "A") . ":"; +$mapped = array_map($box->add(...), [1, 2]); +echo $mapped[0] . $mapped[1] . ":"; +$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); +echo count($filtered) . ":"; +echo array_reduce([1, 2], $box->sum(...), 0) . ":"; + $walkedForWalk = ["a" => 1]; + array_walk($walkedForWalk, $box->show(...)); +echo ":"; +$sorted = [3, 1, 2]; +usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); +echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; +echo EvalFirstClassCallableChild::relay("ok");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:4:7:8:AB:45:2:9:a1:321:child:ok"); +} + +/// Verifies eval first-class static callables preserve late-static forwarding metadata. +#[test] +fn test_eval_first_class_static_callables_preserve_called_class() { + let out = compile_and_run_capture( + r#"instanceWho() . ":"; +echo $child->instanceCall() . ":"; +echo EvalGetCalledClassChild::staticWho() . ":"; +echo EvalGetCalledClassChild::staticCallArray() . ":"; +echo EvalGetCalledClassBase::staticWho() . ":"; +try { + get_called_class(); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +$fn = EvalGetCalledClassChild::makeCallable(); +try { + $fn(); +} catch (Error $e) { + echo "callable:"; +} +echo function_exists("get_called_class") . ":" . is_callable("get_called_class");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassBase:Error:get_called_class() must be called from within a class:callable:1:1" + ); +} + +/// Verifies eval dynamic static receivers dispatch methods, properties, constants, and `::class`. +#[test] +fn test_eval_dynamic_static_receivers() { + let out = compile_and_run_capture( + r#"data[$offset]; + } + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + public function offsetUnset(mixed $offset): void { + unset($this->data[$offset]); + } +} +class EvalStaticPropertyArrayAccessHolder { + public static $box; +} +EvalDynamicStaticPropertyArrayWrite::$items[0] = "a"; +EvalDynamicStaticPropertyArrayWrite::$items[] = "b"; +EvalDynamicStaticPropertyArrayWrite::$items[0] .= "A"; +$class = "EvalDynamicStaticPropertyArrayWrite"; +$class::$dyn[1] = "x"; +$class::$dyn[] = "y"; +echo EvalDynamicStaticPropertyArrayWrite::$items[0] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$items[1] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$dyn[1] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$dyn[2] . "|"; +EvalAotStaticPropertyArrayWrite::$items[0] = "m"; +EvalAotStaticPropertyArrayWrite::$items[] = "n"; +EvalAotStaticPropertyArrayWrite::$items[0] .= "M"; +echo EvalAotStaticPropertyArrayWrite::$items[0] . ":"; +echo EvalAotStaticPropertyArrayWrite::$items[1] . "|"; +EvalStaticPropertyArrayAccessHolder::$box = new EvalStaticPropertyArrayAccessBox(); +EvalStaticPropertyArrayAccessHolder::$box[] = "q"; +EvalStaticPropertyArrayAccessHolder::$box[0] .= "Q"; +echo EvalStaticPropertyArrayAccessHolder::$box[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "aA:b:x:y|mM:n|qQ"); +} + +/// Verifies eval unsets object-property and static-property array elements. +#[test] +fn test_eval_property_and_static_property_array_unset_statements() { + let out = compile_and_run_capture( + r#"removed = $offset; + } +} + +class EvalAotPropertyArrayUnset { + public array $items = ["a", "b"]; + public static array $staticItems = ["x", "y"]; +} + +eval('class EvalDynamicPropertyArrayUnset { + public array $items = ["a", "b"]; + public static $staticItems = ["x", "y"]; + public $box; + public static $staticBox; +} +$dyn = new EvalDynamicPropertyArrayUnset(); +unset($dyn->items[0]); +$name = "items"; +unset($dyn->{$name}[1]); +echo isset($dyn->items[0]) ? "bad" : "d0"; echo ":"; +echo isset($dyn->items[1]) ? "bad" : "d1"; echo "|"; +unset(EvalDynamicPropertyArrayUnset::$staticItems[0]); +$class = "EvalDynamicPropertyArrayUnset"; +unset($class::$staticItems[1]); +echo isset(EvalDynamicPropertyArrayUnset::$staticItems[0]) ? "bad" : "s0"; echo ":"; +echo isset(EvalDynamicPropertyArrayUnset::$staticItems[1]) ? "bad" : "s1"; echo "|"; +$aot = new EvalAotPropertyArrayUnset(); +unset($aot->items[0]); +unset(EvalAotPropertyArrayUnset::$staticItems[0]); +echo isset($aot->items[0]) ? "bad" : "a0"; echo ":"; +echo isset(EvalAotPropertyArrayUnset::$staticItems[0]) ? "bad" : "as0"; echo "|"; +$dyn->box = new EvalArrayUnsetAccessBox(); +unset($dyn->box["drop"]); +echo $dyn->box->removed . ":" . $dyn->box["keep"] . "|"; +EvalDynamicPropertyArrayUnset::$staticBox = new EvalArrayUnsetAccessBox(); +unset(EvalDynamicPropertyArrayUnset::$staticBox["drop"]); +echo EvalDynamicPropertyArrayUnset::$staticBox->removed . ":"; +echo EvalDynamicPropertyArrayUnset::$staticBox["keep"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "d0:d1|s0:s1|a0:as0|drop:K|drop:K"); +} + +/// Verifies eval `isset()` and `empty()` probe static properties without normal read fatals. +#[test] +fn test_eval_static_property_isset_empty_probes() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +$class = "EvalDynamicStaticUnset"; +try { + unset($class::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$name = "value"; +try { + unset($class::${$name}); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$aot = "EvalAotStaticUnset"; +try { + unset($aot::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + unset($aot::${$name}); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + unset(EvalMissingStaticUnset::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Class \"EvalMissingStaticUnset\" not found" + ); +} + +/// Verifies eval invokable objects dispatch through variable and callback call paths. +#[test] +fn test_eval_declared_invokable_object_dynamic_callables() { + let out = compile_and_run_capture( + r#"label = $label; + } + private function __invoke($left = "A", $right = "B") { + return $this->label . ":" . $left . $right; + } +} +class EvalPlainCallableProbe {} +$box = new EvalInvokableBox("box"); +$plain = new EvalPlainCallableProbe(); +echo is_callable($box) ? "Y:" : "N:"; +echo is_callable($plain) ? "bad:" : "plain:"; +echo $box(right: "D", left: "C") . ":"; +try { + $plain(eval_plain_call_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; +echo (new EvalInvokableBox("new"))("E", "F") . ":"; +echo call_user_func($box, "G", "H") . ":"; +$first = $box(...); +echo $first("K", "L") . ":"; +echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:box:IJ" + ); +} + +/// Verifies eval AOT invokable objects dispatch through variable and callback call paths. +#[test] +fn test_eval_aot_invokable_object_dynamic_callables() { + let out = compile_and_run_capture( + r#" "I", "left" => "H"]) . ":"; +try { + (new EvalAotPlainInvokableProbe())(eval_aot_invokable_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:CD:EB:FG:JK:HI:Error:Object of type EvalAotPlainInvokableProbe is not callable" + ); +} + +/// Verifies eval call_user_func rejects non-invokable objects with PHP's TypeError. +#[test] +fn test_eval_call_user_func_rejects_non_invokable_object() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array($plain, []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$aotPlain = new EvalAotPlainCallbackError(); +try { + call_user_func($aotPlain); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given" + ); +} + +/// Verifies eval call_user_func rejects invalid method callable arrays with PHP's TypeError. +#[test] +fn test_eval_call_user_func_rejects_invalid_method_callable_arrays() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array([$missing, "missing"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func(["EvalInstanceCallbackArray", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" + ); +} + +/// Verifies eval call_user_func validates method callable arrays for AOT classes too. +#[test] +fn test_eval_call_user_func_rejects_invalid_aot_method_callable_arrays() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func([new EvalAotPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array(["EvalAotInstanceCallbackArray", "inst"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalAotMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalAotPrivateCallbackArray::hidden()|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, non-static method EvalAotInstanceCallbackArray::inst() cannot be called statically" + ); +} + +/// Verifies eval `is_callable()` probes method callable arrays for AOT classes. +#[test] +fn test_eval_is_callable_checks_aot_method_callable_arrays() { + let out = compile_and_run_capture( + r#"DoThing("A", name: "B") . ":"; +echo $box->hidden("C", name: "D");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "DoThing:A:B:hidden:C:D"); +} + +/// Verifies missing eval-declared instance methods throw catchable PHP errors. +#[test] +fn test_eval_declared_missing_instance_method_throws_error() { + let out = compile_and_run_capture( + r#"missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to undefined method EvalMissingInstanceCallBox::missing()" + ); +} + +/// Verifies eval static method fallback dispatches missing and inaccessible methods through `__callStatic`. +#[test] +fn test_eval_declared_magic_call_static_method_fallback() { + let out = compile_and_run_capture( + r#"DoThing("A", "B") . ":"; +echo $box->hidden("C", "D") . ":"; +echo EvalAotMagicStaticBox::DoStatic("E", "F") . ":"; +echo EvalAotMagicStaticBox::Hidden("G", "H") . ":"; +echo is_callable([$box, "hidden"]) ? "OC:" : "bad:"; +echo call_user_func([$box, "hidden"], "I", "J") . ":"; +echo is_callable(["EvalAotMagicStaticBox", "Hidden"]) ? "SC:" : "bad:"; +echo call_user_func(["EvalAotMagicStaticBox", "Hidden"], "K", "L");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "DoThing:A:B:hidden:C:D:DoStatic:E:F:Hidden:G:H:OC:hidden:I:J:SC:Hidden:K:L" + ); +} + +/// Verifies eval-declared subclasses expose inherited AOT `__callStatic` to callback probes. +#[test] +fn test_eval_declared_child_inherits_aot_magic_call_static_callbacks() { + let out = compile_and_run_capture( + r#"name() . ":" . $box->label();'); +"#, + ); + assert_eq!(out, "child:contract"); + + let err = compile_and_run_expect_failure( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); + + let err = compile_and_run_expect_failure( + r#"hasMethod("aotLabel") ? "H" : "h"; echo ":"; +echo count($r->getMethods()); echo ":"; +echo get_class_methods("EvalAotOnlyReflectableContract")[0] ?? "none";'); +"#, + ); + assert_eq!(out, "H:1:aotlabel"); +} + +/// Verifies eval interface `#[Override]` can target a generated/AOT parent interface. +#[test] +fn test_eval_declared_interface_override_attribute_accepts_aot_parent() { + let out = compile_and_run( + r#"aotLabel();'); +"#, + ); + assert_eq!(out, "aot"); +} + +/// Verifies eval classes can implement generated/AOT interface method contracts. +#[test] +fn test_eval_declared_class_implements_aot_interface_methods() { + let out = compile_and_run( + r#"label("Ada") . ":" . EvalAotImplementedBox::staticLabel("Bob");'); +"#, + ); + assert_eq!(out, "I:Ada:S:Bob"); +} + +/// Verifies eval rejects concrete classes missing generated/AOT interface methods. +#[test] +fn test_eval_declared_class_rejects_missing_aot_interface_methods() { + let err = compile_and_run_expect_failure( + r#"name = "Grace"; +echo $box->name;'); +"#, + ); + assert_eq!(out, "Grace"); +} + +/// Verifies eval rejects concrete classes missing generated/AOT interface properties. +#[test] +fn test_eval_declared_class_rejects_missing_aot_interface_property() { + let err = compile_and_run_expect_failure( + r#"hasProperty("name") ? "H:" : "h:"; +echo $ref->hasProperty("parentName") ? "P:" : "p:"; +$properties = $ref->getProperties(); +echo count($properties) . ":"; +$property = $ref->getProperty("name"); +$parent = $ref->getProperty("parentName"); +$direct = new ReflectionProperty("EvalAotReflectPropertyContract", "name"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $property->getName() . ":"; +echo $property->getDeclaringClass()->getName() . ":"; +echo $property->getType()->getName() . ":"; +echo ($property->hasHooks() ? "hooks" : "plain") . ":"; +echo ($property->hasHook($getCase) ? "G" : "g") . ":"; +echo ($property->hasHook($setCase) ? "S" : "s") . ":"; +$hooks = $property->getHooks(); +echo count($hooks) . ":"; +echo $hooks["get"]->getName() . ":"; +echo $hooks["set"]->getName() . ":"; +echo ($property->getHook($getCase)->isAbstract() ? "A" : "a") . ":"; +echo ($direct->hasHook($setCase) ? "D" : "d") . ":"; +echo $parent->getDeclaringClass()->getName() . ":"; +echo ($parent->hasHook($getCase) ? "PG" : "pg") . ":"; +echo ($parent->hasHook($setCase) ? "bad" : "ps");'); +"#, + ); + assert_eq!( + out, + "H:P:2:name:EvalAotReflectPropertyContract:string:hooks:G:S:2:$name::get:$name::set:A:D:EvalAotReflectPropertyParent:PG:ps" + ); +} + +/// Verifies eval `#[Override]` can target generated/AOT parent class methods. +#[test] +fn test_eval_declared_class_override_attribute_accepts_aot_parent_method() { + let out = compile_and_run( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); +} + +/// Verifies eval `#[Override]` can target AOT methods through eval parents. +#[test] +fn test_eval_declared_class_override_attribute_accepts_inherited_aot_parent_method() { + let out = compile_and_run( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); +} + +/// Verifies eval rejects overriding final generated/AOT parent methods. +#[test] +fn test_eval_declared_class_rejects_final_aot_parent_method_override() { + let err = compile_and_run_expect_failure( + r#"label("ok");'); +"#, + ); + assert_eq!(out, "ok"); +} + +/// Verifies eval concrete classes must implement generated/AOT abstract parent methods. +#[test] +fn test_eval_declared_class_rejects_missing_aot_abstract_parent_method() { + let err = compile_and_run_expect_failure( + r#"id;'); +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies eval concrete classes must implement generated/AOT abstract parent properties. +#[test] +fn test_eval_declared_class_rejects_missing_aot_abstract_parent_property() { + let err = compile_and_run_expect_failure( + r#"id = $id; } +} +echo (new EvalAotAbstractPropertyReadonlyChild(9))->id;'); +"#, + ); + assert_eq!(out, "9"); + + let err = compile_and_run_expect_failure( + r#"id = $id; } +}'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + +/// Verifies eval rejects global builtin attributes on unsupported OOP targets. +#[test] +fn test_eval_declared_builtin_attribute_target_validation() { + let cases = [ + r#"eval('#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}');"#, + r#"eval('class EvalInvalidAttrProperty { #[\Override] public int $value; }');"#, + r#"eval('class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }');"#, + ]; + + for source in cases { + let err = compile_and_run_expect_failure(&format!(" "B", "left" => "A"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Y:AB"); +} + +/// Verifies eval-declared class constants work through the bridge. +#[test] +fn test_eval_declared_class_constants_and_scoped_fetches() { + let out = compile_and_run_capture( + r#"getName() . ":"; +$attrArgs = $attrs[0]->getArguments(); +echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs[1] . ":"; +echo $attrArgs[2] . ":" . ($attrArgs[3] ? "T" : "F") . ":" . (is_null($attrArgs[4]) ? "N" : "bad") . ":"; +echo $attrArgs[5] . ":"; +echo count($attrArgs[6]) . ":" . $attrArgs[6][0] . ":" . $attrArgs[6][1] . ":"; +$tagArgs = $attrs[1]->getArguments(); +echo $attrs[1]->getName() . ":" . $tagArgs[0] . ":"; +echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N" + ); +} + +/// Verifies eval attribute array arguments preserve string keys. +#[test] +fn test_eval_declared_class_attribute_string_keyed_array_args() { + let out = compile_and_run_capture( + r#"items = $items; + } +} +#[EvalArrayAttribute(["plain", "name" => "Ada", "nested" => ["inner" => "value"]])] +class EvalArrayAttributeTarget {} +$args = class_attribute_args("EvalArrayAttributeTarget", "EvalArrayAttribute"); +$items = $args[0]; +echo count($items) . ":" . $items[0] . ":" . $items["name"] . ":" . $items["nested"]["inner"] . ":"; +$attr = class_get_attributes("EvalArrayAttributeTarget")[0]; +$attrItems = $attr->getArguments()[0]; +echo count($attrItems) . ":" . $attrItems[0] . ":" . $attrItems["name"] . ":" . $attrItems["nested"]["inner"] . ":"; +$instanceItems = $attr->newInstance()->items; +echo count($instanceItems) . ":" . $instanceItems[0] . ":" . $instanceItems["name"] . ":" . $instanceItems["nested"]["inner"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3:plain:Ada:value:3:plain:Ada:value:3:plain:Ada:value" + ); +} + +/// Verifies eval attribute array arguments preserve PHP-normalized scalar keys. +#[test] +fn test_eval_declared_class_attribute_scalar_keyed_array_args() { + let out = compile_and_run_capture( + r#"items = $items; + } +} +#[EvalScalarKeyAttribute([2 => "two", true => "bool", null => "null", 1.8 => "float", "name" => "Ada"])] +class EvalScalarKeyAttributeTarget {} +$args = class_attribute_args("EvalScalarKeyAttributeTarget", "EvalScalarKeyAttribute"); +$items = $args[0]; +echo count($items) . ":" . $items[2] . ":" . $items[1] . ":" . $items[""] . ":" . $items["name"] . ":"; +$attr = class_get_attributes("EvalScalarKeyAttributeTarget")[0]; +$attrItems = $attr->getArguments()[0]; +echo count($attrItems) . ":" . $attrItems[2] . ":" . $attrItems[1] . ":" . $attrItems[""] . ":" . $attrItems["name"] . ":"; +$instanceItems = $attr->newInstance()->items; +echo count($instanceItems) . ":" . $instanceItems[2] . ":" . $instanceItems[1] . ":" . $instanceItems[""] . ":" . $instanceItems["name"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "4:two:float:null:Ada:4:two:float:null:Ada:4:two:float:null:Ada" + ); +} + +/// Verifies eval can read generated/AOT float attribute arguments. +#[test] +fn test_eval_reflection_class_exposes_aot_float_attribute_args() { + let out = compile_and_run_capture( + r#"getArguments(); +echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs["value"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:1.5:-2.25:2:1.5:-2.25"); +} + +/// Verifies eval ReflectionAttribute::newInstance builds eval-declared attribute objects. +#[test] +fn test_eval_reflection_attribute_new_instance_for_eval_class() { + let out = compile_and_run_capture( + r#"path = $path; + $this->code = $code; + $this->enabled = $enabled; + } + public function summary() { + return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); + } +} +#[EvalRoute("/home", -7, true)] +class EvalRouteTarget {} +$attrs = class_get_attributes("EvalRouteTarget"); +$instance = $attrs[0]->newInstance(); +echo get_class($instance) . ":" . $instance->summary();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalRoute:/home:-7:T"); +} + +/// Verifies eval ReflectionClass/Method/Property expose eval-declared attributes. +#[test] +fn test_eval_reflection_member_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + } + public function label() { + return $this->name; + } +} +#[EvalMarker("class")] +class EvalReflectTarget { + #[EvalMarker("method")] + public function handle() {} + #[EvalMarker("property")] + public $id; +} +$classAttrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); +echo count($classAttrs) . ":" . (new ReflectionClass("EvalReflectTarget"))->getName() . ":"; +echo $classAttrs[0]->getName() . ":" . $classAttrs[0]->newInstance()->label() . ":"; +$methodAttrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); +echo count($methodAttrs) . ":" . (new ReflectionMethod("EvalReflectTarget", "handle"))->getName() . ":"; +echo $methodAttrs[0]->getName() . ":"; +echo $methodAttrs[0]->getArguments()[0] . ":" . $methodAttrs[0]->newInstance()->label() . ":"; +$propertyAttrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); +echo count($propertyAttrs) . ":" . (new ReflectionProperty("EvalReflectTarget", "id"))->getName() . ":"; +echo $propertyAttrs[0]->getName() . ":"; +echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" + ); +} + +/// Verifies eval ReflectionClass/Method/Function expose source-location metadata. +#[test] +fn test_eval_reflection_source_locations() { + let out = compile_and_run( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; +echo $function->getStartLine(); echo ":"; echo $function->getEndLine();'); +"#, + ); + assert_eq!(out, "F:1:5:2:4:6:8"); +} + +/// Verifies eval ReflectionClass exposes generated/AOT source-location metadata. +#[test] +fn test_eval_reflection_aot_class_source_locations() { + let out = compile_and_run_capture( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +$interface = new ReflectionClass("EvalAotReflectInterfaceSourceE2E"); +echo $interface->getFileName() === false ? "f" : "F"; echo ":"; +echo $interface->getStartLine(); echo ":"; echo $interface->getEndLine(); echo ":"; +$trait = new ReflectionClass("EvalAotReflectTraitSourceE2E"); +echo $trait->getFileName() === false ? "f" : "F"; echo ":"; +echo $trait->getStartLine(); echo ":"; echo $trait->getEndLine(); echo ":"; +$enum = new ReflectionClass("EvalAotReflectEnumSourceE2E"); +echo $enum->getFileName() === false ? "f" : "F"; echo ":"; +echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "F:2:2:F:5:5:F:6:6:F:7:7"); +} + +/// Verifies eval ReflectionMethod exposes generated/AOT source-location metadata. +#[test] +fn test_eval_reflection_aot_method_source_locations() { + let out = compile_and_run_capture( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "F:3:3"); +} + +/// Verifies eval ReflectionAttribute exposes owner target and repetition metadata. +#[test] +fn test_eval_reflection_attribute_target_and_repetition() { + let out = compile_and_run_capture( + r#"getAttributes(); +echo $classAttrs[0]->getTarget() . "/" . ($classAttrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $classAttrs[1]->getTarget() . "/" . ($classAttrs[1]->isRepeated() ? "R" : "r") . ":"; +$methodAttr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; +echo $methodAttr->getTarget() . "/" . ($methodAttr->isRepeated() ? "R" : "r") . ":"; +$propertyAttr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; +echo $propertyAttr->getTarget() . "/" . ($propertyAttr->isRepeated() ? "R" : "r") . ":"; +$paramAttr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; +echo $paramAttr->getTarget() . "/" . ($paramAttr->isRepeated() ? "R" : "r") . ":"; +$constAttr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; +echo $constAttr->getTarget() . "/" . ($constAttr->isRepeated() ? "R" : "r") . ":"; +$caseAttr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; +echo $caseAttr->getTarget() . "/" . ($caseAttr->isRepeated() ? "R" : "r") . ":"; +echo method_exists($classAttrs[0], "getTarget") ? "Y" : "n"; +echo method_exists($classAttrs[0], "isRepeated") ? "Y" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1/R:1/R:4/r:8/r:32/r:16/r:16/r:YY"); +} + +/// Verifies eval ReflectionClass exposes namespace-derived class-name parts. +#[test] +fn test_eval_reflection_class_name_parts() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); +} + +/// Verifies eval ReflectionClass exposes implemented interface and used trait names. +#[test] +fn test_eval_reflection_class_relation_names() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces) . ":" . $interfaces[0] . ":"; +echo count($traits) . ":" . $traits[0] . ":" . $traits[1] . ":"; +$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); +echo count($parentInterfaces) . ":" . $parentInterfaces[0] . ":"; +$interfaceObjects = $ref->getInterfaces(); +echo count($interfaceObjects) . ":" . $interfaceObjects["EvalRelationIface"]->getName() . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["EvalRelationTrait"]->getName() . ":" . $traitObjects["EvalRelationOtherTrait"]->getName() . ":"; +$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); +echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["EvalRelationParent"]->getName() . ":"; +$aliases = $ref->getTraitAliases(); +echo count($aliases) . ":" . $aliases["relationAlias"] . ":" . $aliases["hiddenOther"] . ":"; +$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); +echo count($inheritedAliases);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" + ); +} + +/// Verifies eval ReflectionClass exposes generated/AOT implemented interface names. +#[test] +fn test_eval_reflection_class_get_interface_names_for_aot_class() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($interfaces); +echo count($interfaces) . ":"; +echo implode(",", $interfaces) . ":"; +$interfaceObjects = (new ReflectionClass("EvalAotReflectIfaceTarget"))->getInterfaces(); +ksort($interfaceObjects); +echo count($interfaceObjects) . ":" . implode(",", array_keys($interfaceObjects)) . ":"; +echo $interfaceObjects["EvalAotReflectIfaceBase"]->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild:2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild:EvalAotReflectIfaceBase" + ); +} + +/// Verifies eval ReflectionClass exposes generated/AOT direct trait-use metadata. +#[test] +fn test_eval_reflection_class_get_trait_names_for_aot_class() { + let out = compile_and_run_capture( + r#"getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["EvalAotReflectRelationOuterTrait"]->getName() . ":"; +$nestedTraits = (new ReflectionClass("EvalAotReflectRelationOuterTrait"))->getTraitNames(); +echo count($nestedTraits) . ":" . $nestedTraits[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalAotReflectRelationOuterTrait:1:EvalAotReflectRelationOuterTrait:1:EvalAotReflectRelationInnerTrait" + ); +} + +/// Verifies eval ReflectionClass exposes generated/AOT direct trait aliases. +#[test] +fn test_eval_reflection_class_get_trait_aliases_for_aot_class() { + let out = compile_and_run_capture( + r#"getTraitAliases(); +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:EvalAotReflectAliasTrait::original"); +} + +/// Verifies eval ReflectionClass exposes generated/AOT enum trait metadata. +#[test] +fn test_eval_reflection_class_get_trait_metadata_for_aot_enum() { + if !codegen_fixture_uses_ir_backend() { + return; + } + let out = compile_and_run_capture( + r#"getTraitNames(); +$aliases = $ref->getTraitAliases(); +echo count($traits) . ":" . implode(",", $traits) . ":"; +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalAotReflectEnumTrait:1:EvalAotReflectEnumTrait::original" + ); +} + +/// Verifies eval can fetch a generated/AOT enum case only referenced inside eval source. +#[test] +fn test_eval_fetches_aot_enum_case_object_from_eval_only_reference() { + if !codegen_fixture_uses_ir_backend() { + return; + } + let out = compile_and_run_capture( + r#"getTraitNames(); +$aliases = $ref->getTraitAliases(); +echo count($traits) . ":" . implode(",", $traits) . ":"; +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + &dir, + 8_388_608, + false, + false, + ); + let start = user_asm + .find("_eval_reflection_class_traits:\n") + .expect("missing trait metadata table"); + let count_start = user_asm + .find("_eval_reflection_class_trait_count:\n") + .expect("missing trait metadata count"); + let count_tail = &user_asm[count_start..start]; + let tail = &user_asm[start..]; + let end = tail + .find(".globl _eval_reflection_class_trait_alias_count") + .expect("missing trait alias metadata table"); + let trait_table = &tail[..end]; + + assert!( + count_tail.contains(" .quad 1\n"), + "unexpected trait table count:\n{count_tail}\n{trait_table}" + ); + + assert_eq!( + trait_table + .matches(".ascii \"EvalAotReflectTraitEnum\"") + .count(), + 1, + "{trait_table}" + ); +} + +/// Verifies eval ReflectionClass::implementsInterface reports class, enum, and +/// interface metadata through the bridge. +#[test] +fn test_eval_reflection_class_implements_interface_predicate() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalImplChild") ? "C" : "c"; +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; +echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; +echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBEIPt"); +} + +/// Verifies eval ReflectionClass::implementsInterface uses generated/AOT relations. +#[test] +fn test_eval_reflection_class_implements_interface_for_aot_class() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalAotReflectImplChild") ? "C" : "c"; +echo $ref->implementsInterface("evalaotreflectimplbase") ? "B" : "b"; +echo $ref->implementsInterface("Iterator") ? "I" : "i";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBi"); +} + +/// Verifies eval-declared children expose interfaces inherited from AOT parents. +#[test] +fn test_eval_declared_class_reflects_aot_parent_interfaces() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($names); +echo implode(",", $names) . ":"; +$objects = $ref->getInterfaces(); +ksort($objects); +echo implode(",", array_keys($objects)) . ":"; +echo $ref->implementsInterface("EvalAotParentMarkerChild") ? "C" : "c"; +echo $ref->implementsInterface("evalaotparentmarkerbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalAotParentMarkerChild") ? "S" : "s"; +echo is_subclass_of("EvalAotParentMarkerLeaf", "EvalAotParentMarkerChild") ? "U" : "u"; +$box = new EvalAotParentMarkerLeaf(); +echo $box instanceof EvalAotParentMarkerChild ? "I" : "i";'); +$box = new EvalAotParentMarkerLeaf(); +echo ":"; +echo $box instanceof EvalAotParentMarkerChild ? "N" : "n"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotParentMarkerBase,EvalAotParentMarkerChild:EvalAotParentMarkerBase,EvalAotParentMarkerChild:EvalAotParentMarkerBase,EvalAotParentMarkerChild:CBSUI:N" + ); +} + +/// Verifies eval-declared interfaces expose inherited AOT parent interfaces. +#[test] +fn test_eval_declared_interface_reflects_aot_parent_interfaces() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($names); +echo implode(",", $names) . ":"; +echo (new ReflectionClass("EvalAotInterfaceMarkerLeaf"))->implementsInterface("EvalAotInterfaceMarkerBase") ? "I" : "i"; +echo (new ReflectionClass("EvalAotInterfaceMarkerBox"))->implementsInterface("evalaotinterfacemarkerbase") ? "C" : "c"; +echo (new ReflectionClass("EvalAotInterfaceMarkerLeaf"))->isSubclassOf("EvalAotInterfaceMarkerBase") ? "S" : "s"; +echo is_subclass_of("EvalAotInterfaceMarkerLeaf", "EvalAotInterfaceMarkerBase") ? "U" : "u"; +$box = new EvalAotInterfaceMarkerBox(); +echo $box instanceof EvalAotInterfaceMarkerBase ? "N" : "n"; +echo is_a($box, "EvalAotInterfaceMarkerBase") ? "A" : "a";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild:EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild,EvalAotInterfaceMarkerLeaf:EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild,EvalAotInterfaceMarkerLeaf:ICSUNA" + ); +} + +/// Verifies eval `ReflectionClass::implementsInterface()` throws ReflectionException +/// for missing or non-interface argument names. +#[test] +fn test_eval_reflection_class_implements_interface_rejects_non_interfaces() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalImplRejectOther") ? "T" : "F"; +try { + $ref->implementsInterface("EvalImplRejectClass"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectTrait"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectEnum"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectMissing"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" + ); +} + +/// Verifies eval ReflectionClass::isSubclassOf reports parent and interface +/// metadata through the linked eval bridge. +#[test] +fn test_eval_reflection_class_is_subclass_of_predicate() { + let out = compile_and_run_capture( + r#"isSubclassOf("EvalSubclassParent") ? "P" : "p"; +echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; +echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; +echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; +echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; +echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; +echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; +echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; +try { + $ref->isSubclassOf("EvalSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PBIsJxtqE:missing"); +} + +/// Verifies eval ReflectionClass::isSubclassOf can query generated AOT class +/// relations when the reflected class was declared outside the eval fragment. +#[test] +fn test_eval_reflection_class_is_subclass_of_aot_class() { + let out = compile_and_run_capture( + r#"isSubclassOf("EvalAotSubclassParent") ? "P" : "p"; +echo $child->isSubclassOf("EvalAotSubclassChild") ? "S" : "s"; +$impl = new ReflectionClass("EvalAotSubclassImpl"); +echo $impl->isSubclassOf("EvalAotSubclassIface") ? "I" : "i";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PsI"); +} + +/// Verifies eval ReflectionClass::isInstance reports eval-declared object +/// relations through the linked eval bridge. +#[test] +fn test_eval_reflection_class_is_instance_predicate() { + let out = compile_and_run_capture( + r#"getName(); echo ":"; +echo $objectRef->getParentClass()->getName(); echo ":"; +echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; +echo $base->isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); +} + +/// Verifies eval ReflectionClass::isInstance can query generated AOT object +/// relations when the reflected class was declared outside the eval fragment. +#[test] +fn test_eval_reflection_class_is_instance_aot_class() { + let out = compile_and_run_capture( + r#"isInstance(new EvalAotInstanceChild()) ? "P" : "p"; +$child = new ReflectionClass("EvalAotInstanceChild"); +echo $child->isInstance(new EvalAotInstanceParent()) ? "C" : "c"; +$iface = new ReflectionClass("EvalAotInstanceIface"); +$objectRef = new ReflectionClass(new EvalAotInstanceChild()); +echo $iface->isInstance(new EvalAotInstanceImpl()) ? "I" : "i"; echo ":"; +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PcI:EvalAotInstanceChild:EvalAotInstanceParent"); +} + +/// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. +#[test] +fn test_eval_reflection_class_get_parent_class() { + let out = compile_and_run_capture( + r#"getParentClass(); +echo $parent->getName() . ":"; +$root = (new ReflectionClass("EvalBridgeParent"))->getParentClass(); +if ($root === false) { + echo "false"; +} else { + echo "bad"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalBridgeParent:false"); +} + +/// Verifies eval ReflectionClass::getParentClass materializes generated/AOT parents. +#[test] +fn test_eval_reflection_class_get_parent_class_for_aot_class() { + let out = compile_and_run_capture( + r#"getParentClass(); +if ($parent === false) { + echo "missing"; +} else { + echo $parent->getName(); +} +echo ":"; +$root = (new ReflectionClass("EvalAotReflectParentBase"))->getParentClass(); +echo $root === false ? "false" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalAotReflectParentBase:false"); +} + +/// Verifies eval ReflectionClass::getConstructor crosses the generated runtime bridge. +#[test] +fn test_eval_reflection_class_get_constructor() { + let out = compile_and_run_capture( + r#"getConstructor(); +echo $base->getName() . "/" . $base->getNumberOfParameters(); +echo "/" . $base->getNumberOfRequiredParameters() . ":"; +$child = (new ReflectionClass("EvalBridgeCtorChild"))->getConstructor(); +echo $child->getName() . "/" . $child->getNumberOfParameters(); +echo "/" . $child->getNumberOfRequiredParameters() . ":"; +$plain = (new ReflectionClass("EvalBridgeCtorPlain"))->getConstructor(); +echo $plain === null ? "null" : "bad"; +echo ":"; +$interface = (new ReflectionClass("EvalBridgeCtorInterface"))->getConstructor(); +echo $interface->getName() . "/" . $interface->getNumberOfParameters(); +echo "/" . $interface->getNumberOfRequiredParameters() . ":"; +$trait = (new ReflectionClass("EvalBridgeCtorTrait"))->getConstructor(); +echo $trait->getName() . "/" . $trait->getNumberOfParameters(); +echo "/" . $trait->getNumberOfRequiredParameters(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); +} + +/// Verifies eval ReflectionClass reports class-like final and abstract flags. +#[test] +fn test_eval_reflection_class_modifier_flags() { + let out = compile_and_run_capture( + r#"isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Afite:aFite:aFitE:afIte:afiTe"); +} + +/// Verifies eval ReflectionClass reports PHP modifier bitmasks through the bridge. +#[test] +fn test_eval_reflection_class_modifier_bitmask() { + let out = compile_and_run_capture( + r#"getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierFinal"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierEnum"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierIface"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierTrait"))->getModifiers();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "64:32:65536:65568:32:0:0"); +} + +/// Verifies eval ReflectionClass reports readonly class status through the bridge. +#[test] +fn test_eval_reflection_class_readonly_predicate() { + let out = compile_and_run_capture( + r#"isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "rRRrrr"); +} + +/// Verifies eval ReflectionClass reports instantiability through the bridge. +#[test] +fn test_eval_reflection_class_instantiable_predicate() { + let out = compile_and_run_capture( + r#"isInstantiable() ? "A" : "a"; +echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "aBCprite"); +} + +/// Verifies eval ReflectionClass modifier and lifecycle predicates use generated/AOT class flags. +#[test] +fn test_eval_reflection_class_aot_modifier_flags() { + let out = compile_and_run( + r#"isAbstract() ? "A" : "a"; + echo $ref->isFinal() ? "F" : "f"; + echo $ref->isReadOnly() ? "R" : "r"; + echo $ref->isEnum() ? "E" : "e"; + echo "/" . $ref->getModifiers() . "/"; + echo $ref->isInstantiable() ? "I" : "i"; + echo $ref->isCloneable() ? "C" : "c"; + echo ":"; +} +eval_aot_modifier_line("EvalAotModifierAbstract"); +eval_aot_modifier_line("EvalAotModifierFinal"); +eval_aot_modifier_line("EvalAotModifierReadonly"); +eval_aot_modifier_line("EvalAotModifierEnum"); +'); +"#, + ); + assert_eq!(out, "Afre/64/ic:aFre/32/IC:afRe/65536/IC:aFrE/32/ic:"); +} + +/// Verifies eval ReflectionClass lifecycle predicates use generated/AOT lifecycle visibility. +#[test] +fn test_eval_reflection_class_aot_lifecycle_visibility_predicates() { + let out = compile_and_run( + r#"isInstantiable() ? "N" : "n"; +echo (new ReflectionClass("EvalAotInstPublicCtor"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalAotInstPrivateCtor"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalAotInstProtectedCtor"))->isInstantiable() ? "T" : "t"; +echo ":"; +echo (new ReflectionClass("EvalAotCloneNoHook"))->isCloneable() ? "N" : "n"; +echo (new ReflectionClass("EvalAotClonePublicHook"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalAotClonePrivateHook"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalAotCloneProtectedHook"))->isCloneable() ? "T" : "t"; +'); +"#, + ); + assert_eq!(out, "NPrt:NPrt"); +} + +/// Verifies eval ReflectionClass reports named eval class-like symbols as non-anonymous through +/// the generated reflection-owner bridge. +#[test] +fn test_eval_reflection_class_anonymous_predicate() { + let out = compile_and_run_capture( + r#"isAnonymous() ? "C" : "c"; +echo (new ReflectionClass("EvalAnonIface"))->isAnonymous() ? "I" : "i"; +echo (new ReflectionClass("EvalAnonTrait"))->isAnonymous() ? "T" : "t"; +echo (new ReflectionClass("EvalAnonEnum"))->isAnonymous() ? "E" : "e";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "cite"); +} + +/// Verifies eval anonymous class expressions instantiate and reflect as anonymous through the bridge. +#[test] +fn test_eval_anonymous_class_expression_runtime_and_reflection() { + let out = compile_and_run_capture( + r#"prefix = $prefix; } +} +function eval_runtime_anon_make($prefix) { + return new class($prefix) extends EvalRuntimeAnonBase implements EvalRuntimeAnonLabel { + public function label() { return $this->prefix . ":anon"; } + }; +} +$first = eval_runtime_anon_make("A"); +$second = eval_runtime_anon_make("B"); +echo $first->label(); echo ":"; +echo $second->label(); echo ":"; +echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; +$ref = new ReflectionClass(get_class($first)); +echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; +echo $ref->implementsInterface("EvalRuntimeAnonLabel") ? "iface" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:anon:B:anon:same:anonymous:iface"); +} + +/// Verifies eval readonly anonymous class expressions initialize and reject writes. +#[test] +fn test_eval_readonly_anonymous_class_expression_runtime() { + let out = compile_and_run( + r#"label . ":"; +try { + $box->label = "bad"; + echo "bad"; +} catch (Error $e) { + echo get_class($e); +}'); +"#, + ); + assert_eq!(out, "frozen:Error"); +} + +/// Verifies eval ReflectionClass reports method, property, and constant membership through the bridge. +#[test] +fn test_eval_reflection_class_member_existence() { + let out = compile_and_run_capture( + r#"hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; +echo ":"; +$iface = new ReflectionClass("EvalMemberIface"); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; +echo ":"; +$trait = new ReflectionClass("EvalMemberTrait"); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; +echo ":"; +$pure = new ReflectionClass("EvalMemberPureEnum"); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; +echo ":"; +$backed = new ReflectionClass("EvalMemberBackedEnum"); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); +} + +/// Verifies eval ReflectionClass returns constant values and enum cases through the bridge. +#[test] +fn test_eval_reflection_class_constant_values() { + let out = compile_and_run_capture( + r#"getConstants(); +$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); +$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); +$none = $ref->getConstants(0); +$null = $ref->getConstants(null); +echo $ref->getConstant("OWN") . ":"; +echo $ref->getConstant("BASE") . ":"; +echo $ref->getConstant("LIMIT") . ":"; +echo $ref->getConstant("SECRET") . ":"; +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":" . count($all) . ":" . $all["OWN"] . ":" . $all["BASE"] . ":" . $all["LIMIT"]; +echo ":" . count($public) . ":" . $public["OWN"] . ":" . $public["BASE"]; +echo ":" . count($private) . ":" . $private["SECRET"]; +echo ":" . count($none) . ":" . count($null); +$trait = new ReflectionClass("EvalReflectConstTrait"); +$traitAll = $trait->getConstants(); +echo ":" . $trait->getConstant("TRAIT_VALUE") . ":" . count($traitAll) . ":" . $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass("EvalReflectConstEnum"); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":" . $case->name; +echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($enumAll);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" + ); +} + +/// Verifies eval ReflectionClass returns class-constant reflector objects through the bridge. +#[test] +fn test_eval_reflection_class_constant_reflector_objects() { + let out = compile_and_run_capture( + r#"label = $label; + } + public function label() { + return $this->label; + } +} +class EvalReflectConstObjectTarget { + #[EvalReflectConstMarker("const")] + final public const ANSWER = 42; +} +enum EvalReflectConstObjectEnum { + #[EvalReflectConstMarker("case")] + case Ready; + final public const LEVEL = 7; +} +$ref = new ReflectionClass("EvalReflectConstObjectTarget"); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); +echo $single->getName() . ":"; +echo ($single->isFinal() ? "F" : "f") . ":"; +echo count($all) . ":" . $all[0]->getName() . ":"; +echo $single->getAttributes()[0]->newInstance()->label() . ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +echo ":" . count($public) . ":" . $public[0]->getName(); +echo ":" . count($final) . ":" . $final[0]->getName(); +$enum = new ReflectionClass("EvalReflectConstObjectEnum"); +$enumAll = $enum->getReflectionConstants(); +$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); +echo ":" . $case->getAttributes()[0]->newInstance()->label() . ":"; +echo count($level->getAttributes()) . ":"; +echo $level->isFinal() ? "F" : "f"; +echo ":" . count($enumFinal) . ":" . $enumFinal[0]->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ANSWER:F:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:F:1:LEVEL" + ); +} + +/// Verifies eval ReflectionMethod and ReflectionProperty expose member predicates through the bridge. +#[test] +fn test_eval_reflection_member_predicates() { + let out = compile_and_run_capture( + r#"isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; +echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->isProtectedSet() ? "T" : "t"; +echo $staticProp->isPrivateSet() ? "D" : "d"; +echo $staticProp->getModifiers(); +echo ":"; +$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->isProtectedSet() ? "T" : "t"; +echo $visibleProp->isPrivateSet() ? "D" : "d"; +echo $visibleProp->getModifiers(); +echo ":"; +$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->isProtectedSet() ? "T" : "t"; +echo $readonlyProp->isPrivateSet() ? "D" : "d"; +echo $readonlyProp->getModifiers(); +echo ":"; +$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); +echo ":"; +$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); +echo ":"; +$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); +echo ":"; +$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; +echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; +echo $classReadonlyProp->getModifiers(); +echo ":"; +echo $visibleProp->isDynamic() ? "D" : "d";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" + ); +} + +/// Verifies eval ReflectionProperty reports generated asymmetric set-visibility predicates. +#[test] +fn test_eval_reflection_property_set_visibility_predicates_for_aot_class() { + let out = compile_and_run( + r#"isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers(); echo ":"; +$protected = new ReflectionProperty("EvalAotReflectSetVisibility", "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers();'); +"#, + ); + assert_eq!(out, "Pt4129:pT2049"); +} + +/// Verifies eval-declared asymmetric property visibility enforces writes and reflects metadata. +#[test] +fn test_eval_declared_asymmetric_property_visibility() { + let out = compile_and_run( + r#"privateValue = $value; + $this->protectedName = $name; + } +} +class EvalDeclaredAsymChild extends EvalDeclaredAsymBase { + public function childWrite($name) { + $this->protectedName = $name; + } +} +$box = new EvalDeclaredAsymChild(); +echo $box->privateValue . ":" . $box->protectedName . ":"; +$box->ownerWrite(7, "owner"); +echo $box->privateValue . ":" . $box->protectedName . ":"; +$box->childWrite("child"); +echo $box->protectedName . ":"; +$private = new ReflectionProperty("EvalDeclaredAsymBase", "privateValue"); +echo ($private->isPrivateSet() ? "P" : "p") . ($private->isProtectedSet() ? "T" : "t"); +echo $private->getModifiers() . ":"; +$protected = new ReflectionProperty("EvalDeclaredAsymBase", "protectedName"); +echo ($protected->isPrivateSet() ? "P" : "p") . ($protected->isProtectedSet() ? "T" : "t"); +echo $protected->getModifiers();'); +"#, + ); + assert_eq!(out, "1:base:7:owner:child:Pt4129:pT2049"); + + let errors = compile_and_run_capture( + r#"privateValue = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + unset($box->protectedValue); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + errors.success, + "program failed: stdout={:?} stderr={}", + errors.stdout, errors.stderr + ); + assert_eq!( + errors.stdout, + "Error:Cannot modify private(set) property EvalDeclaredAsymErrorBox::$privateValue from global scope|\ +Error:Cannot unset protected(set) property EvalDeclaredAsymErrorBox::$protectedValue from global scope" + ); +} + +/// Verifies eval-declared inherited properties preserve PHP redeclaration invariants. +#[test] +fn test_eval_declared_inherited_property_redeclaration_contracts() { + let out = compile_and_run( + r#"count = 7; } +} +class EvalPropertyReadonlyWidenBase { + protected int $count = 0; + public function count() { return $this->count; } +} +class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { + public readonly int $count; + public function __construct() { $this->count = 9; } +} +$box = new EvalPropertyRedeclareChild(); +$box->value = "ok"; +$readonly = new EvalPropertyReadonlyAddChild(); +$widened = new EvalPropertyReadonlyWidenChild(); +echo $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();'); +"#, + ); + assert_eq!(out, "ok:7:9:9"); + + let err = compile_and_run_expect_failure( + r#"name = $name; } +} +$box = new EvalIfaceAsymChild(); +echo $box->name . ":"; +$box->rename("child"); +echo $box->name . ":"; +$ref = new ReflectionProperty("EvalIfaceAsymContract", "name"); +echo ($ref->isProtectedSet() ? "T" : "t") . ($ref->isPrivateSet() ? "P" : "p"); +echo $ref->getModifiers();'); +"#, + ); + assert_eq!(out, "base:child:Tp2625"); +} + +/// Verifies eval can observe AOT constructor-promotion metadata through +/// `ReflectionProperty::isPromoted()`. +#[test] +fn test_eval_reflection_property_is_promoted_for_aot_class() { + let out = compile_and_run_capture( + r#"isPromoted() ? "I" : "i"; +$root = new ReflectionProperty("\EvalAotPromotedBase", "id"); +echo $root->isPromoted() ? "I" : "i"; +$name = new ReflectionProperty("EvalAotPromotedBase", "name"); +echo $name->isPromoted() ? "N" : "n"; +$child = new ReflectionProperty("EvalAotPromotedChild", "id"); +echo $child->isPromoted() ? "C" : "c"; +$plain = new ReflectionProperty("EvalAotPromotedPlain", "id"); +echo $plain->isPromoted() ? "P" : "p"; +$static = new ReflectionProperty("EvalAotPromotedPlain", "count"); +echo $static->isPromoted() ? "S" : "s"; +$class = new ReflectionClass("EvalAotPromotedBase"); +echo $class->hasProperty("id") ? "H" : "h"; +echo $class->hasProperty("missing") ? "M" : "m"; +$listed = $class->getProperty("id"); +echo $listed->isPromoted() ? "G" : "g"; +echo $listed->getDeclaringClass()->getName(); +$rootClass = new ReflectionClass("\EvalAotPromotedBase"); +echo $rootClass->hasProperty("name") ? "N" : "n"; +$properties = $class->getProperties(); +echo ":" . count($properties); +$listedId = false; +$listedName = false; +foreach ($properties as $property) { + if ($property->getName() === "id") { + $listedId = $property->isPromoted(); + } + if ($property->getName() === "name") { + $listedName = $property->isPromoted(); + } +} +echo $listedId ? "I" : "i"; +echo $listedName ? "N" : "n"; +$publicProperties = $class->getProperties(ReflectionProperty::IS_PUBLIC); +$protectedProperties = $class->getProperties(filter: ReflectionProperty::IS_PROTECTED); +echo ":" . count($publicProperties) . $publicProperties[0]->getName(); +echo ":" . count($protectedProperties) . $protectedProperties[0]->getName(); +echo ":" . count($class->getProperties(0)); +try { + $class->getProperty("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "IINCpsHmGEvalAotPromotedBaseN:2IN:1id:1name:0:Property EvalAotPromotedBase::$missing does not exist" + ); +} + +/// Verifies eval reports declaring classes for inherited generated/AOT properties. +#[test] +fn test_eval_reflection_property_declaring_class_for_inherited_aot_members() { + let out = compile_and_run_capture( + r#"getProperty("base"); +echo $base->getDeclaringClass()->getName() . ":"; +$static = $class->getProperty("baseStatic"); +echo $static->getDeclaringClass()->getName() . ":"; +$own = $class->getProperty("own"); +echo $own->getDeclaringClass()->getName() . ":"; +$listed = null; +foreach ($class->getProperties() as $property) { + if ($property->getName() === "base") { + $listed = $property; + } +} +echo $listed->getDeclaringClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectPropertyDeclaringBase:EvalAotReflectPropertyDeclaringBase:EvalAotReflectPropertyDeclaringChild:EvalAotReflectPropertyDeclaringBase" + ); +} + +/// Verifies eval exposes declared generated/AOT property types through +/// `ReflectionProperty::hasType()` and `getType()`. +#[test] +fn test_eval_reflection_property_exposes_aot_type_metadata() { + let out = compile_and_run_capture( + r#"hasType() ? "H:" : "h:"; +$type = $id->getType(); +$parts = $type->getTypes(); +echo $parts[0]->getName() . ($parts[0]->isBuiltin() ? "B" : "C"); +echo "," . $parts[1]->getName() . ($parts[1]->isBuiltin() ? "B" : "C"); +echo $type->allowsNull() ? ":N" : ":n"; +$dep = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "dep"); +$depType = $dep->getType(); +echo ":" . ($dep->hasType() ? "D" : "d"); +echo $depType->allowsNull() ? "?" : "!"; +echo $depType->getName() . ($depType->isBuiltin() ? "B" : "C"); +$static = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "count"); +$staticType = $static->getType(); +echo ":" . ($static->hasType() ? "S" : "s"); +echo $staticType->allowsNull() ? "?" : "!"; +echo $staticType->getName() . ($staticType->isBuiltin() ? "B" : "C"); +$base = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "baseName"); +$baseType = $base->getType(); +echo ":" . ($base->hasType() ? "B" : "b"); +echo $baseType->allowsNull() ? "?" : "!"; +echo $baseType->getName() . ($baseType->isBuiltin() ? "B" : "C"); +$untyped = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "untyped"); +echo ":" . ($untyped->hasType() ? "U" : "u"); +echo $untyped->getType() === null ? "N" : "n"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "H:intB,stringB:n:D?EvalAotReflectPropertyTypeDepC:S?intB:B?stringB:uN" + ); +} + +/// Verifies eval exposes supported generated/AOT property defaults through +/// `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()`. +#[test] +fn test_eval_reflection_property_exposes_aot_default_metadata() { + let out = compile_and_run_capture( + r#"hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectPropertyDefaultTarget"))->getProperties() as $property) { + if ($property->getName() === "count") { + $listed = $property; + } +} +echo "listed:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue(); +$defaults = (new ReflectionClass("EvalAotReflectPropertyDefaultTarget"))->getDefaultProperties(); +echo "|defaults:"; +echo $defaults["label"] . ":"; +echo $defaults["count"] . ":"; +echo $defaults["base"] . ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo $defaults["flag"] ? "F:" : "f:"; +echo $defaults["neg"] . ":"; +echo array_key_exists("typed", $defaults) ? "T" : "t"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "count:D:7|label:D:ok|nullable:D:null|implicit:D:null|typed:d:null|base:D:3|flag:D:1|neg:D:-1.5|listed:D:7|defaults:ok:7:3:I:N:F:-1.5:t" + ); +} + +/// Verifies eval ReflectionProperty exposes generated/AOT non-empty array defaults. +#[test] +fn test_eval_reflection_property_exposes_aot_array_default_values() { + let out = compile_and_run_capture( + r#" "L", 2 => "R", "tail"]; +} +echo eval('$property = new ReflectionProperty("EvalAotReflectArrayPropertyDefaultTarget", "items"); +$items = $property->getDefaultValue(); +$defaults = (new ReflectionClass("EvalAotReflectArrayPropertyDefaultTarget"))->getDefaultProperties(); +return $items["left"] . ":" . $items[2] . ":" . $items[3] . ":" . $defaults["items"]["left"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "L:R:tail:L"); +} + +/// Verifies eval exposes generated/AOT member attributes through Reflection. +#[test] +fn test_eval_reflection_member_exposes_aot_attributes() { + let out = compile_and_run_capture( + r#"getAttributes(); +echo "M" . count($methodAttrs) . ":"; +echo $methodAttrs[0]->getName() . ":" . $methodAttrs[0]->getArguments()[0] . ":"; +$propertyAttrs = (new ReflectionProperty("EvalAotReflectAttrTarget", "id"))->getAttributes(); +echo "P" . count($propertyAttrs) . ":"; +echo $propertyAttrs[0]->getName() . ":"; +$propertyArgs = $propertyAttrs[0]->getArguments(); +echo $propertyArgs[0] . ":" . $propertyArgs[1] . ":"; +$baseMethodAttrs = (new ReflectionMethod("EvalAotReflectAttrTarget", "baseRun"))->getAttributes(); +echo "BM" . count($baseMethodAttrs) . ":"; +$args = $baseMethodAttrs[0]->getArguments(); +echo $args[0] . ":" . $args[1] . ":" . ($args[2] ? "T" : "F") . ":" . ($args[3] === null ? "N" : "n") . ":"; +$basePropertyAttrs = (new ReflectionProperty("EvalAotReflectAttrTarget", "baseId"))->getAttributes(); +echo "BP" . count($basePropertyAttrs) . ":" . $basePropertyAttrs[0]->getArguments()[0] . ":"; +$listedMethod = (new ReflectionClass("EvalAotReflectAttrTarget"))->getMethod("run"); +echo count($listedMethod->getAttributes()) . ":"; +$listedProperty = (new ReflectionClass("EvalAotReflectAttrTarget"))->getProperty("id"); +echo count($listedProperty->getAttributes()) . ":"; +$constantAttrs = (new ReflectionClassConstant("EvalAotReflectAttrTarget", "LIMIT"))->getAttributes(); +echo "C" . count($constantAttrs) . ":"; +echo $constantAttrs[0]->getName() . ":"; +$constantArgs = $constantAttrs[0]->getArguments(); +echo $constantArgs[0] . ":" . $constantArgs[1] . ":"; +$listedConstant = (new ReflectionClass("EvalAotReflectAttrTarget"))->getReflectionConstant("LIMIT"); +echo count($listedConstant->getAttributes()) . ":"; +$caseAttrs = (new ReflectionClassConstant("EvalAotReflectAttrEnum", "Ready"))->getAttributes(); +echo "E" . count($caseAttrs) . ":" . $caseAttrs[0]->getArguments()[0]; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "M1:EvalAotMemberAttr:method:P1:EvalAotMemberAttr:property:-3:BM1:base:7:T:N:BP1:baseProp:1:1:C1:EvalAotMemberAttr:constant:11:1:E1:case" + ); +} + +/// Verifies eval class-attribute helpers expose generated/AOT class attributes. +#[test] +fn test_eval_reflection_class_exposes_aot_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + $this->value = $value; + $this->summary = "ok"; + } + + public function label(): string { + return $this->name . ":" . $this->value; + } +} +#[EvalAotClassAttr("class", 1, ["nested", "kind" => "class", "meta" => ["inner" => "value"]]), EvalAotClassAttr("again", 2, ["later", "kind" => "again"])] +class EvalAotClassAttrTarget {} +echo eval('$names = class_attribute_names("EvalAotClassAttrTarget"); +echo count($names) . ":" . $names[0] . ":" . $names[1] . ":"; +$args = class_attribute_args("evalaotclassattrtarget", "EvalAotClassAttr"); +echo count($args) . ":" . $args[0] . ":" . $args[1] . ":"; +echo count($args[2]) . ":" . $args[2][0] . ":" . $args[2]["kind"] . ":" . $args[2]["meta"]["inner"] . ":"; +$attrs = class_get_attributes("EvalAotClassAttrTarget"); +echo count($attrs) . ":" . $attrs[0]->getName() . ":"; +echo ($attrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $attrs[0]->getArguments()[0] . ":" . $attrs[0]->getArguments()[1] . ":"; +echo count($attrs[0]->getArguments()[2]) . ":" . $attrs[0]->getArguments()[2][0] . ":" . $attrs[0]->getArguments()[2]["kind"] . ":"; +$firstInstance = $attrs[0]->newInstance(); +echo $attrs[0]->getTarget() . ":" . $firstInstance->label() . ":" . $firstInstance->summary . ":"; +$refAttrs = (new ReflectionClass("EvalAotClassAttrTarget"))->getAttributes(); +echo count($refAttrs) . ":" . $refAttrs[1]->getArguments()[0] . ":"; +echo count($refAttrs[1]->getArguments()[2]) . ":" . $refAttrs[1]->getArguments()[2][0] . ":" . $refAttrs[1]->getArguments()[2]["kind"] . ":"; +echo $refAttrs[1]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2:EvalAotClassAttr:EvalAotClassAttr:3:class:1:3:nested:class:value:2:EvalAotClassAttr:R:class:1:3:nested:class:1:class:1:ok:2:again:2:later:again:again:2" + ); +} + +/// Verifies eval ReflectionAttribute::newInstance constructs generated/AOT attribute classes. +#[test] +fn test_eval_reflection_attribute_new_instance_constructs_aot_attribute() { + let out = compile_and_run_capture( + r#"name = $name; + $this->value = $value; + } + + public function label(): string { + return $this->name . ":" . $this->value; + } +} +class EvalAotAttributeInstanceTarget { + #[EvalAotAttributeInstance("method", 2)] + public function run() {} + + #[EvalAotAttributeInstance("property", 3)] + public int $id = 0; + + #[EvalAotAttributeInstance("constant", 4)] + public const LIMIT = 5; +} +echo eval('$methodAttr = (new ReflectionMethod("EvalAotAttributeInstanceTarget", "run"))->getAttributes()[0]; +echo $methodAttr->newInstance()->label() . ":"; +$propertyAttr = (new ReflectionProperty("EvalAotAttributeInstanceTarget", "id"))->getAttributes()[0]; +echo $propertyAttr->newInstance()->label() . ":"; +$constantAttr = (new ReflectionClassConstant("EvalAotAttributeInstanceTarget", "LIMIT"))->getAttributes()[0]; +echo $constantAttr->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "method:2:property:3:constant:4"); +} + +/// Verifies eval can probe generated/AOT method predicate metadata through +/// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. +#[test] +fn test_eval_reflection_method_for_aot_class() { + let out = compile_and_run_capture( + r#"hasMethod("RUN") ? "R" : "r"; +echo $class->hasMethod("BASESTATIC") ? "B" : "b"; +echo $class->hasMethod("missing") ? "M" : "m"; +$run = $class->getMethod("RUN"); +echo ":" . $run->getName(); +echo $run->isPublic() ? "U" : "u"; +echo $run->isStatic() ? "S" : "s"; +echo $run->getDeclaringClass()->getName(); +$base = $class->getMethod("baseStatic"); +echo ":" . ($base->isStatic() ? "S" : "s"); +echo $base->isProtected() ? "P" : "p"; +$locked = new ReflectionMethod("EvalAotReflectMethodBase", "LOCKED"); +echo ":" . $locked->getName(); +echo $locked->isFinal() ? "F" : "f"; +echo $locked->isPublic() ? "U" : "u"; +echo $locked->getDeclaringClass()->getName(); +$methods = $class->getMethods(); +$seenRun = false; +$seenBase = false; +$seenLocked = false; +foreach ($methods as $method) { + if (strtolower($method->getName()) === "run") { + $seenRun = $method->isPublic(); + } + if (strtolower($method->getName()) === "basestatic") { + $seenBase = $method->isStatic(); + } + if (strtolower($method->getName()) === "locked") { + $seenLocked = $method->isFinal(); + } +} +echo ":" . count($methods); +echo $seenRun ? "R" : "r"; +echo $seenBase ? "B" : "b"; +echo $seenLocked ? "L" : "l"; +$staticMethods = $class->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $class->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$seenStatic = false; +$seenHidden = false; +foreach ($staticMethods as $method) { + if (strtolower($method->getName()) === "basestatic") { + $seenStatic = $method->isProtected(); + } +} +foreach ($privateMethods as $method) { + if (strtolower($method->getName()) === "hidden") { + $seenHidden = $method->isPrivate(); + } +} +echo ":" . count($staticMethods) . ($seenStatic ? "S" : "s"); +echo ":" . count($privateMethods) . ($seenHidden ? "H" : "h"); +echo ":" . count($class->getMethods(0)); +try { + $class->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:4RBL:1S:1H:0:Method EvalAotReflectMethodChild::missing() does not exist" + ); +} + +/// Verifies eval reports declaring classes for inherited generated/AOT methods and constructors. +#[test] +fn test_eval_reflection_method_declaring_class_for_inherited_aot_members() { + let out = compile_and_run_capture( + r#"getMethod("inherited"); +echo $inherited->getDeclaringClass()->getName() . ":"; +$static = $class->getMethod("baseStatic"); +echo $static->getDeclaringClass()->getName() . ":"; +$own = $class->getMethod("own"); +echo $own->getDeclaringClass()->getName() . ":"; +$ctor = $class->getConstructor(); +echo $ctor->getDeclaringClass()->getName() . "/" . $ctor->getNumberOfParameters() . ":"; +$listed = null; +foreach ($class->getMethods() as $method) { + if ($method->getName() === "inherited") { + $listed = $method; + } +} +echo $listed->getDeclaringClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectDeclaringBase:EvalAotReflectDeclaringBase:EvalAotReflectDeclaringChild:EvalAotReflectDeclaringBase/1:EvalAotReflectDeclaringBase" + ); +} + +/// Verifies eval ReflectionMethod::invoke can dispatch generated/AOT methods. +#[test] +fn test_eval_reflection_method_invoke_calls_aot_method() { + let out = compile_and_run_capture( + r#"getMethod("who"); +echo $who->invoke($object) . ":"; +$static = new ReflectionMethod("EvalAotReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X") . ":"; +echo $static->invoke($object, "A") . ":"; +$join = new ReflectionMethod("EvalAotReflectInvokeChild", "join"); +echo $join->invoke($object, "Q") . ":"; +return $join->invokeArgs($object, ["b" => "2", "a" => "1"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectInvokeChild:EvalAotReflectInvokeBase:XY:EvalAotReflectInvokeBase:AS:QB:12" + ); +} + +/// Verifies eval ReflectionMethod::invokeArgs accepts runtime-built AOT argument arrays. +#[test] +fn test_eval_reflection_method_invoke_args_accepts_runtime_aot_arg_arrays() { + let out = compile_and_run_capture( + r#"invokeArgs($object, $args) . ":"; +$static = new ReflectionMethod("EvalAotReflectRuntimeInvokeArgsTarget", "make"); +$staticArgs = []; +$staticArgs["right"] = "Y"; +$staticArgs["left"] = "X"; +return $static->invokeArgs(null, $staticArgs);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "12:XY"); +} + +/// Verifies eval ReflectionMethod::invoke bypasses generated/AOT method visibility. +#[test] +fn test_eval_reflection_method_invoke_bypasses_aot_method_visibility() { + let out = compile_and_run_capture( + r#"invoke($object, 3) . ":" . $label->invoke(null, "x");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:Hx"); +} + +/// Verifies eval ReflectionMethod exposes registered generated/AOT parameter metadata. +#[test] +fn test_eval_reflection_method_exposes_aot_parameter_metadata() { + let out = compile_and_run_capture( + r#"getNumberOfParameters() . "/" . $method->getNumberOfRequiredParameters() . ":"; +foreach ($method->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + $default = $param->getDefaultValue(); + echo is_null($default) ? "null" : $default; + } + echo ";"; +} +$static = new ReflectionMethod("EvalAotReflectParamTarget", "sum"); +echo ":" . $static->getNumberOfParameters() . "/" . $static->getNumberOfRequiredParameters() . ":"; +foreach ($static->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + echo $param->getDefaultValue(); + } + echo ";"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectParamTarget"))->getMethods() as $candidate) { + if ($candidate->getName() === "join") { + $listed = $candidate; + } +} +echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3/1:leftr-;rightO=B;countO=null;:2/1:firstr-;secondO=2;:3/count" + ); +} + +/// Verifies direct ReflectionParameter construction accepts generated/AOT method targets. +#[test] +fn test_eval_reflection_parameter_accepts_aot_method_targets() { + let out = compile_and_run_capture( + r#"getName() . ":" . $direct->getPosition() . ":"; +echo $direct->getDeclaringClass()->getName() . ":"; +echo $direct->getDeclaringFunction()->getName() . ":"; +echo ($direct->isOptional() ? "O" : "r") . ":" . $direct->getDefaultValue() . ":"; +$objectParam = new ReflectionParameter([new EvalAotDirectParamTarget(), "join"], 0); +echo $objectParam->getName() . ":" . $objectParam->getType()->getName() . ":"; +$static = new ReflectionParameter(["EvalAotDirectParamTarget", "collect"], "items"); +echo $static->getName() . ":" . ($static->isVariadic() ? "V" : "v") . ":" . ($static->isOptional() ? "O" : "r");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "right:1:EvalAotDirectParamTarget:join:O:B:left:string:items:V:O" + ); +} + +/// Verifies eval ReflectionMethod exposes generated/AOT empty-array defaults. +#[test] +fn test_eval_reflection_method_exposes_aot_empty_array_default() { + let out = compile_and_run_capture( + r#"getParameters()[0]; +echo $param->isOptional() ? "O" : "r"; +echo $param->isDefaultValueAvailable() ? "=" : "-"; +$default = $param->getDefaultValue(); +echo is_array($default) ? count($default) : "bad"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "O=0"); +} + +/// Verifies eval materializes generated/AOT empty-array defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_empty_array_default() { + let out = compile_and_run( + r#"countItems();'); +"#, + ); + assert_eq!(out, "0"); +} + +/// Verifies eval materializes generated/AOT non-empty array defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_array_default_values() { + let out = compile_and_run_capture( + r#"getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . EvalAotArrayValueDefaultMethodTarget::describeStatic() . ":" . $default[0] . $default[1] . $default[2];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "4:5:6:7:8:9:456"); +} + +/// Verifies eval normalizes generated/AOT array-default keys with PHP rules. +#[test] +fn test_eval_aot_method_array_default_normalizes_keys() { + let out = compile_and_run_capture( + r#" "yes", + false => "no", + 2.8 => "float", + "3" => "numeric", + ]): string { + return $items[1] . ":" . $items[0] . ":" . $items[2] . ":" . $items[3]; + } + + public function reflected(array $items = [ + null => "nil", + "03" => "string", + -1.2 => "negative", + ]): void { + } +} + +echo eval('$obj = new EvalAotArrayKeyDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotArrayKeyDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$reflected = (new ReflectionMethod("EvalAotArrayKeyDefaultMethodTarget", "reflected"))->getParameters()[0]->getDefaultValue(); +return $obj->describe() . "|" . $default[1] . ":" . $default[0] . ":" . $default[2] . ":" . $default[3] . "|" . $reflected[""] . ":" . $reflected["03"] . ":" . $reflected[-1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "yes:no:float:numeric|yes:no:float:numeric|nil:string:negative" + ); +} + +/// Verifies eval materializes generated/AOT object defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_object_default() { + let out = compile_and_run( + r#"label = $left . $right . $third . $fourth; + } +} + +class EvalAotObjectDefaultMethodTarget { + public function describe(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("m", "e", "t", "h")): string { + return $dep->label; + } + + public static function describeStatic(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("s", "t", "a", "t")): string { + return $dep->label; + } +} + +echo eval('$obj = new EvalAotObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . EvalAotObjectDefaultMethodTarget::describeStatic() . ":" . $default->label;'); +"#, + ); + assert_eq!(out, "meth:stat:meth"); +} + +/// Verifies eval preserves named constructor args in generated/AOT object defaults. +#[test] +fn test_eval_aot_object_default_uses_named_constructor_args() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +class EvalAotNamedObjectDefaultMethodTarget { + public function describe(EvalAotNamedObjectDefaultDep $dep = new EvalAotNamedObjectDefaultDep(right: "R", left: "L")): string { + return $dep->label; + } +} + +class EvalAotNamedObjectDefaultCtorTarget { + public string $label = ""; + + public function __construct(EvalAotNamedObjectDefaultDep $dep = new EvalAotNamedObjectDefaultDep(right: "B", left: "A")) { + $this->label = $dep->label; + } +} + +echo eval('$obj = new EvalAotNamedObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotNamedObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$box = new EvalAotNamedObjectDefaultCtorTarget(); +return $obj->describe() . ":" . $default->label . ":" . $box->label;'); +"#, + ); + assert_eq!(out, "LR:LR:AB"); +} + +/// Verifies eval materializes nested generated/AOT object defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_nested_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalAotNestedObjectDefaultOuter { + public EvalAotNestedObjectDefaultInner $inner; + + public function __construct(EvalAotNestedObjectDefaultInner $inner = new EvalAotNestedObjectDefaultInner("outer")) { + $this->inner = $inner; + } +} + +class EvalAotNestedObjectDefaultMethodTarget { + public function describe(EvalAotNestedObjectDefaultOuter $outer = new EvalAotNestedObjectDefaultOuter(new EvalAotNestedObjectDefaultInner("method"))): string { + return $outer->inner->label; + } +} + +echo eval('$obj = new EvalAotNestedObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotNestedObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . $default->inner->label;'); +"#, + ); + assert_eq!(out, "method:method"); +} + +/// Verifies eval materializes generated/AOT object defaults with more than eight constructor args. +#[test] +fn test_eval_aot_object_default_uses_large_constructor_arg_list() { + let out = compile_and_run( + r#"label = implode("", $parts); + } +} + +class EvalAotLargeObjectDefaultMethodTarget { + public function describe(EvalAotLargeObjectDefaultDep $dep = new EvalAotLargeObjectDefaultDep("A", "B", "C", "D", "E", "F", "G", "H", "I")): string { + return $dep->label; + } +} + +class EvalAotLargeObjectDefaultCtorTarget { + public string $label = ""; + + public function __construct(EvalAotLargeObjectDefaultDep $dep = new EvalAotLargeObjectDefaultDep("J", "K", "L", "M", "N", "O", "P", "Q", "R")) { + $this->label = $dep->label; + } +} + +echo eval('$obj = new EvalAotLargeObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotLargeObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$box = new EvalAotLargeObjectDefaultCtorTarget(); +return $obj->describe() . ":" . $default->label . ":" . $box->label;'); +"#, + ); + assert_eq!(out, "ABCDEFGHI:ABCDEFGHI:JKLMNOPQR"); +} + +/// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. +#[test] +fn test_eval_reflection_method_exposes_aot_parameter_flags() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $method->getNumberOfParameters() . "/" . $method->getNumberOfRequiredParameters() . ":"; +foreach ($params as $param) { + echo $param->getName(); + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isOptional() ? "O" : "r"; + echo ";"; +} +$static = new ReflectionMethod("EvalAotReflectParamFlagsTarget", "collect"); +$item = $static->getParameters()[0]; +echo ":" . $item->getName(); +echo $item->isPassedByReference() ? "R" : "b"; +echo $item->isVariadic() ? "V" : "v"; +echo $static->getNumberOfRequiredParameters(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2/1:valueRvr;partsbVO;:itemsbV0"); +} + +/// Verifies eval can dispatch generated/AOT variadic methods and constructors. +#[test] +fn test_eval_aot_variadic_method_and_constructor_bridge() { + let out = compile_and_run( + r#"label = $head . ":" . count($items) . ":" . $items[0] . ":" . $items[1]; + } + + public function collect($head, ...$items): string { + return $head . ":" . count($items) . ":" . $items[0] . ":" . $items[1]; + } + + public static function collectStatic(...$items): string { + return count($items) . ":" . $items[0] . ":" . $items[1]; + } +} +echo eval('$target = new EvalAotVariadicBridgeTarget("C", "D", "E"); +return $target->collect("H", "A", "B") . "|" . EvalAotVariadicBridgeTarget::collectStatic("S", "T") . "|" . $target->label;'); +"#, + ); + assert_eq!(out, "H:2:A:B|2:S:T|C:2:D:E"); +} + +/// Verifies eval can dispatch generated/AOT methods and constructors beyond register-only arity. +#[test] +fn test_eval_aot_wide_method_and_constructor_bridge() { + let out = compile_and_run( + r#"label = $a . $b . $c . $d . $e . $f . $g . $h . $i; + } + + public function join($a, $b, $c, $d, $e, $f, $g, $h, $i): string { + return $a . $b . $c . $d . $e . $f . $g . $h . $i; + } + + public static function joinStatic($a, $b, $c, $d, $e, $f, $g, $h, $i): string { + return $a . $b . $c . $d . $e . $f . $g . $h . $i; + } +} +echo eval('$target = new EvalAotWideMethodBridgeTarget("A", "B", "C", "D", "E", "F", "G", "H", "I"); +return $target->join("1", "2", "3", "4", "5", "6", "7", "8", "9") + . "|" . EvalAotWideMethodBridgeTarget::joinStatic("a", "b", "c", "d", "e", "f", "g", "h", "i") + . "|" . $target->label;'); +"#, + ); + assert_eq!(out, "123456789|abcdefghi|ABCDEFGHI"); +} + +/// Verifies generated/AOT variadic bridge rejects named arguments captured by the tail. +#[test] +fn test_eval_aot_variadic_method_rejects_named_tail() { + let err = compile_and_run_expect_failure( + r#"collect(named: "A");'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + +/// Verifies eval ReflectionMethod exposes generated/AOT declared type metadata. +#[test] +fn test_eval_reflection_method_exposes_aot_type_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +$union = $params[0]->getType(); +echo "U" . count($union->getTypes()); +foreach ($union->getTypes() as $type) { + echo ":" . $type->getName() . ($type->isBuiltin() ? "B" : "C"); +} +$dep = $params[1]->getType(); +echo ":D" . ($dep->allowsNull() ? "?" : "!") . ":" . $dep->getName() . ($dep->isBuiltin() ? "B" : "C"); +$return = $method->getReturnType(); +echo ":R" . ($return->allowsNull() ? "?" : "!") . ":" . $return->getName() . ($return->isBuiltin() ? "B" : "C"); +$static = (new ReflectionMethod("EvalAotReflectTypeTarget", "factory"))->getReturnType(); +echo ":S" . ($static->allowsNull() ? "?" : "!") . ":" . $static->getName() . ($static->isBuiltin() ? "B" : "C"); +$intersection = (new ReflectionMethod("EvalAotReflectTypeTarget", "both"))->getParameters()[0]->getType(); +echo ":I" . count($intersection->getTypes()); +foreach ($intersection->getTypes() as $type) { + echo ":" . $type->getName() . ($type->isBuiltin() ? "B" : "C"); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "U2:intB:stringB:D?:EvalAotReflectTypeDepC:R?:stringB:S!:EvalAotReflectTypeDepC:I2:EvalAotReflectTypeLeftC:EvalAotReflectTypeRightC" + ); +} + +/// Verifies eval ReflectionClass::getConstructor exposes generated/AOT constructor metadata. +#[test] +fn test_eval_reflection_class_get_constructor_for_aot_class() { + let out = compile_and_run_capture( + r#"label = $left . $right . ($count ?? 0); + } +} +class EvalAotReflectCtorPlain {} +echo eval('$ctor = (new ReflectionClass("EvalAotReflectCtorParamTarget"))->getConstructor(); +echo ($ctor instanceof ReflectionMethod) ? "M:" : "m:"; +echo $ctor->getName() . "/" . $ctor->getDeclaringClass()->getName() . ":"; +echo $ctor->getNumberOfParameters() . "/" . $ctor->getNumberOfRequiredParameters() . ":"; +foreach ($ctor->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + $default = $param->getDefaultValue(); + echo is_null($default) ? "null" : $default; + } + $type = $param->getType(); + echo ":"; + echo $type === null ? "none" : $type->getName() . ($type->allowsNull() ? "?" : "!"); + echo ";"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectCtorParamTarget"))->getMethods() as $candidate) { + if ($candidate->getName() === "__construct") { + $listed = $candidate; + } +} +echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[0]->getName(); +$plain = (new ReflectionClass("EvalAotReflectCtorPlain"))->getConstructor(); +echo ":" . ($plain === null ? "null" : "bad"); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "M:__construct/EvalAotReflectCtorParamTarget:3/1:leftr-:string!;rightO=B:string!;countO=null:int?;:3/left:null" + ); +} + +/// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. +#[test] +fn test_eval_reflection_method_reports_constructor_and_destructor() { + let out = compile_and_run_capture( + r#"isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod("EvalReflectLifecycle", "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Cd:cD:cd:Cd"); +} + +/// Verifies eval ReflectionMethod keeps declared name case after case-insensitive lookup. +#[test] +fn test_eval_reflection_method_preserves_declared_name_case() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $direct->getShortName() . ":"; +echo $direct->invoke($object) . ":"; +$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); +echo $listed->getName() . ":"; +echo $listed->invoke($object);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "MiXeDCase:MiXeDCase:base:childCase:child"); +} + +/// Verifies eval ReflectionMethod accepts object targets through the bridge. +#[test] +fn test_eval_reflection_method_accepts_object_targets() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $inherited->getDeclaringClass()->getName() . ":"; +echo $inherited->invoke($object) . ":"; +$own = new ReflectionMethod($object, "CHILDCASE"); +echo $own->getName() . ":"; +echo $own->getDeclaringClass()->getName() . ":"; +echo $own->invoke($object) . "|"; +$aot = new EvalAotReflectMethodObjectChild(); +$aotInherited = new ReflectionMethod($aot, "aotbase"); +echo $aotInherited->getName() . ":"; +echo $aotInherited->getDeclaringClass()->getName() . ":"; +$aotOwn = new ReflectionMethod($aot, "aotchild"); +echo $aotOwn->getName() . ":"; +echo $aotOwn->getDeclaringClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child|aotbase:EvalAotReflectMethodObjectBase:aotchild:EvalAotReflectMethodObjectChild" + ); +} + +/// Verifies eval ReflectionMethod::createFromMethodName resolves eval and AOT method strings. +#[test] +fn test_eval_reflection_method_create_from_method_name() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo $ref->getName() . ":"; +echo $ref->invoke(new EvalReflectCreateMethodTarget()) . "|"; +$aot = ReflectionMethod::createFromMethodName("EvalAotReflectCreateMethodTarget::aotrun"); +echo $aot->getDeclaringClass()->getName() . ":"; +echo $aot->getName() . ":"; +echo $aot->invoke(new EvalAotReflectCreateMethodTarget());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalReflectCreateMethodTarget:MiXeDCase:ok|EvalAotReflectCreateMethodTarget:aotrun:aot" + ); +} + +/// Verifies eval ReflectionMethod accepts PHP's deprecated one-string constructor target. +#[test] +fn test_eval_reflection_method_accepts_single_method_string() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo $aot->getName() . ":"; +echo $aot->invoke(new EvalAotReflectCtorMethodTarget()) . "|"; +eval('class EvalReflectCtorMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName() . ":"; +echo $ref->getName() . ":"; +echo $ref->invoke(new EvalReflectCtorMethodTarget());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectCtorMethodTarget:aotrun:aot|EvalReflectCtorMethodTarget:MiXeDCase:ok" + ); +} + +/// Verifies eval ReflectionMethod construction errors are catchable ReflectionException objects. +#[test] +fn test_eval_reflection_method_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingMethodTarget {} +try { + new ReflectionMethod("EvalDynReflectMissingMethodTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalDynReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalDynReflectMissingClass", "run"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("not-a-method"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Method EvalAotReflectMissingMethodTarget::missing() does not exist|Method EvalDynReflectMissingMethodTarget::missing() does not exist|Method EvalDynReflectMissingMethodTarget::missing() does not exist|Class \"EvalDynReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" + ); +} + +/// Verifies eval ReflectionProperty construction errors are catchable ReflectionException objects. +#[test] +fn test_eval_reflection_property_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingPropertyTarget {} +$object = new EvalDynReflectMissingPropertyTarget(); +$object->dynamic = 1; +try { + new ReflectionProperty("EvalDynReflectMissingPropertyTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty("EvalDynReflectMissingPropertyClass", "value"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty($object, "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionProperty($object, "dynamic"))->getName() . ":"; +echo (new ReflectionProperty("EvalAotReflectMissingPropertyTarget", "known"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Property EvalAotReflectMissingPropertyTarget::$missing does not exist|Property EvalDynReflectMissingPropertyTarget::$missing does not exist|Class \"EvalDynReflectMissingPropertyClass\" does not exist|Property EvalDynReflectMissingPropertyTarget::$missing does not exist|dynamic:known" + ); +} + +/// Verifies eval ReflectionClassConstant construction errors are catchable objects. +#[test] +fn test_eval_reflection_class_constant_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingConstantTarget { + public const OK = 1; +} +try { + new ReflectionClassConstant("EvalDynReflectMissingConstantTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionClassConstant("EvalDynReflectMissingConstantClass", "VALUE"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionClassConstant("EvalDynReflectMissingConstantTarget", "OK"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalAotReflectMissingConstantTarget", "OK"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Constant EvalAotReflectMissingConstantTarget::missing does not exist|Constant EvalDynReflectMissingConstantTarget::missing does not exist|Class \"EvalDynReflectMissingConstantClass\" does not exist|OK:OK" + ); +} + +/// Verifies eval ReflectionEnumUnitCase/BackedCase construction errors are catchable objects. +#[test] +fn test_eval_reflection_enum_case_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalDynReflectMissingCaseClass", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalDynReflectMissingCaseUnit", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseUnit", "Ready"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseBacked", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseClass", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnumUnitCase("EvalDynReflectMissingCaseBacked", "Ready"))->getName() . ":"; +echo (new ReflectionEnumBackedCase("EvalDynReflectMissingCaseBacked", "Ready"))->getBackingValue(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Constant EvalDynReflectMissingCaseUnit::Missing does not exist|Constant EvalDynReflectMissingCaseClass::TOKEN is not a case|Constant EvalDynReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalDynReflectMissingCaseUnit::Ready is not a backed case|Constant EvalDynReflectMissingCaseBacked::Missing does not exist|Constant EvalDynReflectMissingCaseClass::Missing does not exist|Ready:ready" + ); +} + +/// Verifies eval-declared final properties cannot be redeclared by subclasses. +#[test] +fn test_eval_declared_final_property_override_fails() { + let err = compile_and_run_expect_failure( + r#"getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" + ); +} + +/// Verifies eval ReflectionClass getMethods/getProperties return member objects through the bridge. +#[test] +fn test_eval_reflection_class_lists_member_objects() { + let out = compile_and_run_capture( + r#"getMethods(); +$properties = $ref->getProperties(); +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +$nullMethods = $ref->getMethods(null); +$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); +$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); +$noProperties = $ref->getProperties(0); +echo count($methods) . ":" . count($properties) . ":"; +echo ReflectionMethod::IS_STATIC . ":" . ReflectionMethod::IS_PRIVATE . ":"; +$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); +echo "D" . $direct->getModifiers() . ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F" . count($method->getAttributes()); + echo "M" . $method->getModifiers(); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + echo "M" . $method->getModifiers(); + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V" . count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + echo "M" . $property->getModifiers(); + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + echo "M" . $property->getModifiers(); + } +} +echo ":"; +echo count($staticMethods) . $staticMethods[0]->getName() . ":"; +echo count($privateMethods) . $privateMethods[0]->getName() . ":"; +echo count($noMethods) . ":" . count($nullMethods) . ":"; +echo count($staticProperties) . $staticProperties[0]->getName() . ":"; +echo count($protectedProperties) . $protectedProperties[0]->getName() . ":"; +echo count($noProperties);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" + ); +} + +/// Verifies eval ReflectionClass getMethod/getProperty return single member objects. +#[test] +fn test_eval_reflection_class_get_method_and_property_lookup_members() { + let out = compile_and_run_capture( + r#"getMethod("FIRST"); +echo $method->getName() . ":"; +echo $method->isPublic() ? "U" : "u"; +echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; +echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName() . ":"; +echo $property->isProtected() ? "R" : "r"; +echo ":"; +try { + $ref->getProperty("Visible"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo ":"; +try { + $ref->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "first:U:PS:visible:R:Property EvalReflectLookupTarget::$Visible does not exist:Method EvalReflectLookupTarget::missing() does not exist" + ); +} + +/// Verifies eval ReflectionMethod materializes ReflectionParameter objects through the bridge. +#[test] +fn test_eval_reflection_method_lists_parameters() { + let out = compile_and_run_capture( + r#"getNumberOfParameters() . "/"; +echo $method->getNumberOfRequiredParameters() . ":"; +$params = $method->getParameters(); +foreach ($params as $param) { + echo $param->getName() . "@" . $param->getPosition(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->canBePassedByValue() ? "Y" : "N"; + echo $param->hasType() ? "T" : "t"; + echo $param->allowsNull() ? "N" : "n"; + echo $param->isArray() ? "A" : "a"; + echo $param->isCallable() ? "C" : "c"; + $type = $param->getType(); + if ($param->getName() == "union") { + echo ":union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($param->getName() == "both") { + echo ":intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo ":" . $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo ":null"; + } + $attrs = $param->getAttributes(); + echo ":A" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } + echo $param->isDefaultValueAvailable() ? ":D" : ":d"; + if ($param->isDefaultValueAvailable()) { + echo "="; + echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); + } + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "7/3:first@0rvRNTnac:int!B:A1:EvalParamTag:first:d|union@1rvbYTnac:union!:intB:stringB:A0:d|both@2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items@3OvbYTNAc:array?B:A0:D=null|callback@4OvbYTNaC:callable?B:A0:D=null|second@5OvbYTNac:App\\Name?C:A0:D=null|rest@6OVRNtNac:null:A0:d|" + ); +} + +/// Verifies eval ReflectionType objects stringify retained parameter metadata. +#[test] +fn test_eval_reflection_type_to_string_formats_retained_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName() . ":"; + echo $type->__toString() . "|"; +} +$unionType = (new ReflectionParameter(["EvalReflectTypeStringTarget", "run"], "union"))->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" + ); +} + +/// Verifies eval ReflectionParameter stringifies retained parameter metadata. +#[test] +fn test_eval_reflection_parameter_to_string() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->__toString(); + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" + ); +} + +/// Verifies eval ReflectionParameter::getClass() reports retained object type metadata. +#[test] +fn test_eval_reflection_parameter_get_class_reports_named_object_type() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $class = $param->getClass(); + echo $param->getName() . ":" . ($class ? $class->getName() : "null") . "|"; +} +$direct = new ReflectionParameter(["EvalReflectParamClassTarget", "run"], "nullable"); +echo "direct:" . $direct->getClass()->getName() . "|"; +$functionParam = new ReflectionParameter("eval_reflect_param_class_function", "dep"); +echo "function:" . $functionParam->getClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:EvalReflectParamClassDep|nullable:EvalReflectParamClassDep|id:null|unionObject:null|intersection:null|plain:null|direct:EvalReflectParamClassDep|function:EvalReflectParamClassDep" + ); +} + +/// Verifies eval direct ReflectionParameter construction accepts runtime object method targets. +#[test] +fn test_eval_reflection_parameter_accepts_object_expression_target() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $param->getPosition() . ":"; +echo $param->getDeclaringClass()->getName() . ":"; +echo $param->getDeclaringFunction()->getName() . ":"; +echo ($param->isOptional() ? "O" : "R") . ":"; +echo $param->getType()->getName() . ":"; +echo $param->allowsNull() ? "N" : "n"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "name:1:EvalDirectParamObjectTarget:run:O:string:N" + ); +} + +/// Verifies eval ReflectionParameter construction errors are catchable objects. +#[test] +fn test_eval_reflection_parameter_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], 3); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "missing"], "known"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], -1); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +echo (new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], "known"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalDynReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" + ); +} + +/// Verifies eval ReflectionParameter exposes PHP constant-default metadata. +#[test] +fn test_eval_reflection_parameter_reports_default_constant_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueAvailable()) { + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + } + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" + ); +} + +/// Verifies eval ReflectionParameter default magic constants use callable scopes through the bridge. +#[test] +fn test_eval_reflection_parameter_resolves_default_magic_constants() { + let out = compile_and_run_capture( + r#"getParameters() as $param) { + echo "[" . $param->getDefaultValue() . "]"; + } + echo ":"; +} +eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\\\eval_reflect_param_magic")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read"));'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + concat!( + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[][][EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", + "[own]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", + "[source]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", + "[read]", + "[]", + "[EvalReflectParamMagicNs]:" + ) + ); +} + +/// Verifies eval ReflectionMethod exposes eval-declared return type metadata. +#[test] +fn test_eval_reflection_method_reports_return_type_metadata() { + let out = compile_and_run_capture( + r#"getReturnType(); +echo ($iface->hasReturnType() ? "I" : "i") . ":"; +echo $ifaceType->getName() . ":"; +echo ($ifaceType->isBuiltin() ? "B" : "b") . ":"; +$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); +echo $self->getName() . ":"; +echo ($self->isBuiltin() ? "B" : "b") . ":"; +$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); +echo $void->getName() . ":"; +echo ($void->allowsNull() ? "N" : "n") . ":"; +echo $void->isBuiltin() ? "B" : "b";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "I:string:B:static:b:void:n:B"); +} + +/// Verifies eval ReflectionProperty materializes property get/set type metadata through the bridge. +#[test] +fn test_eval_reflection_property_get_type_metadata() { + let out = compile_and_run_capture( + r#"getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($property->getName() == "union") { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); +$directType = $direct->getType(); +echo "direct:"; +echo $direct->hasType() ? "T:" : "t:"; +echo $directType->getName(); +$directSettableType = $direct->getSettableType(); +echo ":set:" . $directSettableType->getName(); +$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); +echo ":plainSet:" . ($plain->getSettableType() === null ? "N" : "n"); +$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); +echo ":unionSet:" . count($directUnion->getSettableType()->getTypes());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" + ); +} + +/// Verifies eval ReflectionProperty uses explicit set-hook parameter metadata for settable type. +#[test] +fn test_eval_reflection_property_get_settable_type_uses_set_hook_parameter() { + let out = compile_and_run_capture( + r#" $this->value; + set(int|string $raw) => (string) $raw; + } +} +$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); +$type = $property->getType(); +$settable = $property->getSettableType(); +echo $type->getName() . ":"; +echo count($settable->getTypes()); +foreach ($settable->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; +} +$setHook = $property->getHook(PropertyHookType::Set); +$paramType = $setHook->getParameters()[0]->getType(); +echo ":" . count($paramType->getTypes()); +$box = new EvalReflectSettableTypeTarget(); +$box->value = 7; +echo ":" . $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "string:2:intB:stringB:2:7"); +} + +/// Verifies eval ReflectionProperty materializes property default metadata through the bridge. +#[test] +fn test_eval_reflection_property_get_default_value_metadata() { + let out = compile_and_run_capture( + r#"getName() . ":"; + echo $property->isDefault() ? "Y:" : "N:"; + echo $property->hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); +echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" + ); +} + +/// Verifies eval ReflectionProperty materializes dynamic object properties through the bridge. +#[test] +fn test_eval_reflection_property_supports_dynamic_properties() { + let out = compile_and_run_capture( + r#"dynamic = "first"; +$child = new EvalReflectDynamicBridgeChild(); +$child->dynamic = "child"; +$empty = new EvalReflectDynamicBridgeChild(); +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); echo ":"; +echo $property->isDynamic() ? "D" : "d"; echo ":"; +echo $property->isDefault() ? "Y" : "N"; echo ":"; +echo $property->getModifiers(); echo ":"; +echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; +echo is_null($property->getType()) ? "T" : "t"; echo ":"; +echo $property->isInitialized($object) ? "I" : "i"; echo ":"; +echo $property->getValue($object); echo ":"; +echo $property->getValue($child); echo ":"; +echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; +$property->setValue($empty, "filled"); +echo $property->getValue($empty); echo ":"; +$property->setRawValue($object, "raw"); +echo $property->getRawValue($object); echo ":"; +echo str_replace("\n", "\\n", $property->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dynamic:D:N:1:h:T:I:first:child:e:filled:raw:Property [ public $dynamic ]\n" + ); +} + +/// Verifies eval ReflectionProperty formats retained property metadata through `__toString()`. +#[test] +fn test_eval_reflection_property_to_string() { + let out = compile_and_run_capture( + r#" 1; + } +} +foreach (["id", "label", "implicit", "virtual"] as $name) { + echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" + ); +} + +/// Verifies eval ReflectionClass materializes property default metadata through the bridge. +#[test] +fn test_eval_reflection_class_get_default_properties_metadata() { + let out = compile_and_run_capture( + r#"getDefaultProperties(); +echo $defaults["childStatic"] . ":"; +echo $defaults["baseStatic"] . ":"; +echo $defaults["child"] . ":"; +echo $defaults["shadow"] . ":"; +echo $defaults["base"] . ":"; +echo $defaults["prot"] . ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo array_key_exists("typed", $defaults) ? "T" : "t";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:bs:5:9:1:p:I:N:t"); +} + +/// Verifies eval ReflectionProperty value APIs use current runtime object values. +#[test] +fn test_eval_reflection_property_gets_and_sets_values() { + let out = compile_and_run_capture( + r#" $this->raw * 2; + set { $this->raw = $value + 1; } + } + public $backed { + get { return $this->backed * 2; } + set { $this->backed = $value; } + } + public $virtual { + get => $this->raw + 100; + } + public function __construct() { + $this->backed = 2; + } +} +$child = new EvalReflectValueChild(); +$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); +echo $secret->getValue($child) . ":"; +$secret->setValue($child, "changed"); +echo $secret->getValue(object: $child) . ":"; +$name = new ReflectionProperty("EvalReflectValueChild", "name"); +echo $name->getValue($child) . ":"; +$name->setValue(objectOrValue: $child, value: "Grace"); +echo $name->getValue($child) . ":"; +$count = new ReflectionProperty("EvalReflectValueBase", "count"); +echo $count->getValue() . ":"; +$count->setValue(5); +echo EvalReflectValueChild::$count . ":"; +$count->setValue(null, 6); +echo $count->getValue($child) . ":"; +$hook = new EvalReflectValueHook(); +$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); +echo $doubled->getValue($hook) . ":"; +$doubled->setValue($hook, 4); +echo $hook->raw . ":"; +echo $doubled->getValue($hook) . ":"; +$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; +$backed->setValue($hook, 4); +echo $backed->getRawValue(object: $hook) . ":"; +echo $backed->getValue($hook) . ":"; +$backed->setRawValue(object: $hook, value: 7); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; +echo $backed->isLazy($hook) ? "L:" : "l:"; +$backed->skipLazyInitialization(object: $hook); +$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; +echo $backed->getModifiers() . ":"; +echo $backed->isVirtual() ? "V:" : "b:"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V:" : "b:"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" + ); +} + +/// Verifies eval ReflectionProperty raw APIs reject virtual property hooks. +#[test] +fn test_eval_reflection_property_virtual_raw_value_fails() { + let err = compile_and_run_expect_failure( + r#" $this->raw * 2; + } +} +$object = new EvalReflectVirtualRawHook(); +$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); +$property->getRawValue($object);'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + +/// Verifies eval ReflectionProperty reports instance and static initialization state. +#[test] +fn test_eval_reflection_property_is_initialized() { + let out = compile_and_run_capture( + r#" 42; + } +} +$object = new EvalReflectInitializedTarget(); +$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); +$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); +$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); +$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); +$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); +$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $plain->isInitialized(object: $object) ? "P:" : "p:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $staticPlain->isInitialized() ? "N:" : "n:"; +EvalReflectInitializedTarget::$staticTyped = 3; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +$object->typed = 5; +echo $typed->isInitialized($object) ? "T:" : "t:"; +unset($object->typed); +echo $typed->isInitialized($object) ? "T:" : "t:"; +$typed->setRawValue(object: $object, value: 9); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $nullable->isInitialized($object) ? "Y:" : "y:"; +$nullable->setValue($object, null); +echo $nullable->isInitialized($object) ? "Y:" : "y:"; +echo $virtual->isInitialized($object) ? "V" : "v";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "t:P:s:N:S:T:t:T:y:Y:V"); +} + +/// Verifies eval ReflectionProperty initialization checks bridge generated/AOT storage. +#[test] +fn test_eval_reflection_property_is_initialized_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . "," . self::$hidden; + } +} +$object = new EvalAotReflectInitializedTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectInitializedTarget", "name"); +$typed = new ReflectionProperty("EvalAotReflectInitializedTarget", "typed"); +$secret = new ReflectionProperty("EvalAotReflectInitializedTarget", "secret"); +$label = new ReflectionProperty("EvalAotReflectInitializedTarget", "label"); +$staticTyped = new ReflectionProperty("EvalAotReflectInitializedTarget", "staticTyped"); +$hidden = new ReflectionProperty("EvalAotReflectInitializedTarget", "hidden"); +echo $name->isInitialized($object) ? "N:" : "n:"; +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $secret->isInitialized($object) ? "P:" : "p:"; +echo $label->isInitialized() ? "L:" : "l:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $hidden->isInitialized() ? "H:" : "h:"; +$typed->setValue($object, 5); +$secret->setValue($object, 7); +$staticTyped->setValue(11); +$hidden->setValue(13); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $secret->isInitialized($object) ? "P:" : "p:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $hidden->isInitialized() ? "H:" : "h:"; +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "N:t:p:L:s:h:T:P:S:H:7,13"); +} + +/// Verifies eval ReflectionProperty getValue rejects uninitialized generated/AOT typed storage. +#[test] +fn test_eval_reflection_property_get_value_rejects_uninitialized_aot_storage() { + let out = compile_and_run_capture( + r#"getValue($object) . ":"; +echo $typed->getValue($object); +'); +"#, + ); + assert!( + !out.success, + "program unexpectedly succeeded: stdout={:?}", + out.stdout + ); + assert_eq!(out.stdout, "Ada:"); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); +} + +/// Verifies eval ReflectionProperty raw APIs bridge generated/AOT instance storage. +#[test] +fn test_eval_reflection_property_raw_value_apis_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . ":" . $this->typed; + } +} +$object = new EvalAotReflectRawPropertyTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "name"); +$secret = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "secret"); +$typed = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "typed"); +echo $name->getRawValue($object) . ":"; +$name->setRawValue($object, "Grace"); +echo $object->name . ":"; +echo $secret->getRawValue($object) . ":"; +$secret->setRawValue($object, 9); +echo $typed->isLazy($object) ? "L:" : "l:"; +$typed->skipLazyInitialization(object: $object); +$typed->setRawValueWithoutLazyInitialization(object: $object, value: 11); +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada:Grace:3:l:9:11"); +} + +/// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. +#[test] +fn test_eval_reflection_property_hook_metadata() { + let out = compile_and_run_capture( + r#"raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} +abstract class EvalReflectAbstractHookProperty { + abstract public int $contract { get; set; } +} +interface EvalReflectInterfaceHookProperty { + public int $iface { get; } +} +$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); +$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); +$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); +$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $getCase->name . ":" . $getCase->value . ":"; +$caseList = PropertyHookType::cases(); +echo count($caseList) . ":" . $caseList[0]->name . ":" . $caseList[1]->value . ":"; +echo PropertyHookType::from("set")->name . ":"; +echo (PropertyHookType::tryFrom("missing") === null ? "T" : "t") . ":"; +echo ($hooked->hasHooks() ? "H" : "h") . ":"; +echo ($hooked->hasHook($getCase) ? "G" : "g") . ":"; +echo ($hooked->hasHook(type: $setCase) ? "S" : "s") . ":"; +$hooks = $hooked->getHooks(); +echo count($hooks) . ":" . $hooks["get"]->getName() . ":" . $hooks["set"]->getName() . ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getDeclaringClass()->getName() . ":" . $get->getNumberOfParameters() . ":"; +echo $set->getNumberOfParameters() . ":" . $set->getParameters()[0]->getName() . ":"; +$box = new EvalReflectHookedProperty(); +echo $get->invoke($box) . ":"; +$set->invoke($box, 7); +echo $box->raw . ":"; +echo ($readonly->hasHook($getCase) ? "R" : "r") . ":"; +echo ($readonly->hasHook($setCase) ? "w" : "W") . ":"; +echo ($readonly->getHook($setCase) === null ? "N" : "n") . ":"; +echo ($plain->hasHooks() ? "bad" : "plain") . ":"; +echo count($plain->getHooks()) . ":"; +$abstractHooks = $abstract->getHooks(); +echo count($abstractHooks) . ":"; +echo ($abstract->hasHook($getCase) ? "AG" : "ag") . ":"; +echo ($abstract->hasHook($setCase) ? "AS" : "as") . ":"; +echo $abstractHooks["get"]->getName() . ":" . ($abstractHooks["get"]->isAbstract() ? "A" : "a") . ":"; +echo $abstractHooks["set"]->getName() . ":" . ($abstractHooks["set"]->isAbstract() ? "A" : "a") . ":"; +$ifaceHook = $iface->getHook($getCase); +echo count($iface->getHooks()) . ":"; +echo ($iface->hasHook($getCase) ? "IG" : "ig") . ":"; +echo ($iface->hasHook($setCase) ? "bad" : "is") . ":"; +echo $ifaceHook->isAbstract() ? "IA" : "ia";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" + ); +} + +/// Verifies eval ReflectionClass static-property APIs use current runtime values. +#[test] +fn test_eval_reflection_class_static_property_values() { + let out = compile_and_run_capture( + r#"getStaticProperties(); +echo count($statics) . ":"; +echo $statics["child"] . ":"; +echo $statics["base"] . ":"; +echo $statics["prot"] . ":"; +echo $statics["shadow"] . ":"; +echo $ref->getStaticPropertyValue("count") . ":"; +$ref->setStaticPropertyValue("shadow", "changed"); +echo $ref->getStaticPropertyValue("shadow") . ":"; +$ref->setStaticPropertyValue(name: "count", value: 5); +echo EvalReflectStaticChild::$count . ":"; +echo $ref->getStaticPropertyValue("instance", "fallback") . ":"; +echo $ref->getStaticPropertyValue("missing", "fallback") . ":"; +try { + $ref->getStaticPropertyValue("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" + ); +} + +/// Verifies eval ReflectionClass static-property APIs bridge generated/AOT values. +#[test] +fn test_eval_reflection_class_static_property_values_aot() { + let out = compile_and_run_capture( + r#"getStaticProperties(); +echo count($statics) . ":"; +echo $statics["label"] . ":"; +echo $statics["count"] . ":"; +echo $statics["secret"] . ":"; +echo $statics["hidden"] . ":"; +echo $ref->getStaticPropertyValue("label") . ":"; +$ref->setStaticPropertyValue("label", "changed"); +echo EvalAotReflectStaticPropertyTarget::$label . ":"; +$ref->setStaticPropertyValue(name: "count", value: 9); +echo $ref->getStaticPropertyValue("count") . ":"; +echo EvalAotReflectStaticPropertyTarget::$count . ":"; +echo $ref->getStaticPropertyValue("secret") . ":"; +$ref->setStaticPropertyValue("secret", "changed-prot"); +echo $ref->getStaticPropertyValue("secret") . ":"; +echo $ref->getStaticPropertyValue("hidden") . ":"; +$ref->setStaticPropertyValue("hidden", 8); +echo $ref->getStaticPropertyValue("hidden") . ":"; +echo $ref->getStaticPropertyValue("instance", "fallback") . ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "4:start:2:prot:4:start:changed:9:9:prot:changed-prot:4:8:fallback:S" + ); +} + +/// Verifies eval ReflectionProperty value APIs bridge generated/AOT storage. +#[test] +fn test_eval_reflection_property_value_apis_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . "," . self::$hidden; + } +} +$object = new EvalAotReflectPropertyValueTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "name"); +$secret = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "secret"); +$label = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "label"); +$hidden = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "hidden"); +echo $name->getValue($object) . ":"; +$name->setValue($object, "Grace"); +echo $object->name . ":"; +echo $secret->getValue($object) . ":"; +$secret->setValue($object, 9); +echo $object->reveal() . ":"; +echo $label->getValue() . ":"; +$label->setValue("changed"); +echo EvalAotReflectPropertyValueTarget::$label . ":"; +echo $hidden->getValue() . ":"; +$hidden->setValue(8); +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada:Grace:3:9,4:start:changed:4:9,8"); +} + +/// Verifies eval ReflectionParameter exposes the declaring class for method parameters. +#[test] +fn test_eval_reflection_parameter_reports_declaring_class() { + let out = compile_and_run_capture( + r#"getParameters()[0]; +echo $inherited->getDeclaringClass()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName() . ":"; +$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName() . ":"; +echo $listed->getDeclaringFunction()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" + ); +} + +/// Verifies eval ReflectionFunction materializes eval-declared function parameters. +#[test] +fn test_eval_reflection_function_reports_eval_function_parameters() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $ref->getName() . ":"; +echo $ref->getNumberOfParameters() . ":"; +echo $ref->getNumberOfRequiredParameters() . ":"; +echo count($params) . ":"; +echo $params[0]->getName() . ":"; +echo $params[1]->getPosition() . ":"; +$declaring = $params[0]->getDeclaringFunction(); +echo get_class($declaring) . ":"; +echo $declaring->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "eval_reflect_free:2:2:2:left:1:ReflectionFunction:eval_reflect_free" + ); +} + +/// Verifies eval ReflectionFunction preserves rich eval-declared function signatures. +#[test] +fn test_eval_reflection_function_reports_signature_metadata() { + let out = compile_and_run_capture( + r#"label = $label; } + public function label() { return $this->label; } +} +#[EvalFuncAttr("free")] +function eval_reflect_rich(#[EvalFuncAttr("first")] string $name, int $count = 3, &...$items) { + return $count; +} +$ref = new ReflectionFunction("eval_reflect_rich"); +$attrs = $ref->getAttributes(); +$params = $ref->getParameters(); +echo count($attrs) . ":"; +echo $attrs[0]->getName() . ":"; +echo $attrs[0]->newInstance()->label() . ":"; +echo $ref->getNumberOfParameters() . ":"; +echo $ref->getNumberOfRequiredParameters() . ":"; +echo ($params[0]->hasType() ? "T" : "t") . ":"; +echo $params[0]->getType()->getName() . ":"; +$paramAttrs = $params[0]->getAttributes(); +echo count($paramAttrs) . ":"; +echo $paramAttrs[0]->newInstance()->label() . ":"; +echo ($params[1]->isOptional() ? "O" : "o") . ":"; +echo $params[1]->getDefaultValue() . ":"; +echo ($params[2]->isVariadic() ? "V" : "v") . ":"; +echo $params[2]->isPassedByReference() ? "R" : "r";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalFuncAttr:free:3:1:T:string:1:first:O:3:V:R" + ); +} + +/// Verifies eval ReflectionFunction exposes eval-declared return type metadata. +#[test] +fn test_eval_reflection_function_reports_return_type_metadata() { + let out = compile_and_run_capture( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$union = (new ReflectionFunction("eval_reflect_return_union"))->getReturnType(); +echo count($union->getTypes()) . ":"; +foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; +} +echo ":"; +$never = (new ReflectionFunction("eval_reflect_return_never"))->getReturnType(); +echo $never->getName() . ":"; +echo ($never->allowsNull() ? "N" : "n") . ":"; +echo ($never->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionFunction("eval_reflect_return_plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "T:int:N:B:2:intBstringB:never:n:B:p:Q"); +} + +/// Verifies eval ReflectionFunction and ReflectionMethod stringify retained signatures. +#[test] +fn test_eval_reflection_function_method_to_string() { + let out = compile_and_run_capture( + r#"__toString()); +echo "::"; +$method = new ReflectionMethod("EvalReflectMethodStringTextTarget", "run"); +echo str_replace("\n", "|", $method->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Function [ function eval_reflect_string_text ] {| - Parameters [3] {| Parameter #0 [ string $name ]| Parameter #1 [ int $count = 3 ]| Parameter #2 [ &...$items ]| }| - Return [ ?string ]|}|::Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" + ); +} + +/// Verifies eval ReflectionClass stringifies retained class metadata sections. +#[test] +fn test_eval_reflection_class_to_string() { + let out = compile_and_run_capture( + r#"__toString()); +echo "::"; +echo str_replace("\n", "|", (new ReflectionObject(new EvalReflectClassStringTarget()))->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + let expected = "Class [ class EvalReflectClassStringTarget ] {| - Constants [1] {| Constant [ public int ANSWER ] { 42 }| }| - Properties [1] {| Property [ public int $id = 7 ]| }| - Methods [1] {| Method [ public method read ]| }|}|"; + assert_eq!(out.stdout, format!("{expected}::{expected}")); +} + +/// Verifies eval ReflectionObject lists dynamic public properties from its reflected instance. +#[test] +fn test_eval_reflection_object_lists_dynamic_properties() { + let out = compile_and_run_capture( + r#"dynamic = "value"; +$ref = new ReflectionObject($object); +$properties = $ref->getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo ($property->isDynamic() ? "D" : "d") . "|"; +} +echo ":"; +$dynamic = $ref->getProperty("dynamic"); +echo ($dynamic->isDynamic() ? "D" : "d") . ":"; +echo $dynamic->getValue($object) . ":"; +echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)) . ":"; +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)) . ":"; +echo $ref->hasProperty("dynamic") ? "H" : "h"; +echo $ref->hasProperty("declared") ? "D" : "d"; +echo $ref->hasProperty("missing") ? "M" : "m"; +echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "declared:d|dynamic:D|:D:value:2:0:HDmc"); +} + +/// Verifies eval ReflectionObject constructor type errors are catchable objects. +#[test] +fn test_eval_reflection_object_constructor_throws_type_errors() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionObject([]); + echo "bad"; +} catch (TypeError $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" + ); +} + +/// Verifies eval Reflection origin metadata APIs are present on supported owners. +#[test] +fn test_eval_reflection_origin_metadata_defaults() { + let out = compile_and_run_capture( + r#"getDocComment() === false) ? "C" : "c"; echo ":"; +echo ($function->getDocComment() === false) ? "F" : "f"; echo ":"; +echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; +echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; +echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; +echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; +echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; +echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($function->getExtensionName() === false) ? "N" : "n"; echo ":"; +echo ($method->getExtensionName() === false) ? "O" : "o"; echo ":"; +echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; +echo ($function->getExtension() === null) ? "Y" : "y"; echo ":"; +echo ($method->getExtension() === null) ? "Z" : "z";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "C:F:M:P:K:U:B:E:N:O:X:Y:Z"); +} + +/// Verifies eval ReflectionFunction/Method expose name and origin predicate metadata. +#[test] +fn test_eval_reflection_function_and_method_name_origin_predicates() { + let out = compile_and_run_capture( + r#"getShortName() . ":"; +echo $fn->getNamespaceName() . ":"; +echo ($fn->inNamespace() ? "Y" : "N") . ":"; +echo ($fn->isInternal() ? "I" : "i"); +echo ($fn->isUserDefined() ? "U" : "u") . ":"; +echo ($fn->isAnonymous() ? "A" : "a") . ":"; +echo ($fn->isClosure() ? "C" : "c") . ":"; +echo ($fn->isDeprecated() ? "D" : "d") . ":"; +echo ($fn->isStatic() ? "S" : "s") . ":"; +echo ($fn->returnsReference() ? "R" : "r") . ":"; +echo ($fn->hasReturnType() ? "T" : "t") . ":"; +echo ($fn->getReturnType() === null ? "N" : "n") . ":"; +echo ($fn->isGenerator() ? "G" : "g") . ":"; +echo ($fn->isVariadic() ? "V" : "v") . ":"; +echo ($fn->hasTentativeReturnType() ? "H" : "h") . ":"; +echo ($fn->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo count($fn->getClosureUsedVariables()) . ":"; +echo ($fn->getClosureThis() === null ? "T" : "t") . ":"; +echo ($fn->getClosureScopeClass() === null ? "S" : "s") . ":"; +echo ($fn->getClosureCalledClass() === null ? "L" : "l") . ":"; +echo ($fn->isDisabled() ? "X" : "x") . "|"; +echo $method->getShortName() . ":"; +echo $method->getNamespaceName() . ":"; +echo ($method->inNamespace() ? "Y" : "N") . ":"; +echo ($method->isInternal() ? "I" : "i"); +echo ($method->isUserDefined() ? "U" : "u") . ":"; +echo ($method->isClosure() ? "C" : "c") . ":"; +echo ($method->isDeprecated() ? "D" : "d") . ":"; +echo ($method->isStatic() ? "S" : "s") . ":"; +echo ($method->returnsReference() ? "R" : "r") . ":"; +echo ($method->hasReturnType() ? "T" : "t") . ":"; +echo ($method->getReturnType() === null ? "N" : "n") . ":"; +echo ($method->isGenerator() ? "G" : "g") . ":"; +echo ($method->isVariadic() ? "V" : "v") . ":"; +echo ($method->hasTentativeReturnType() ? "H" : "h") . ":"; +echo ($method->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo count($method->getClosureUsedVariables()) . ":"; +echo ($method->getClosureThis() === null ? "T" : "t") . ":"; +echo ($method->getClosureScopeClass() === null ? "S" : "s") . ":"; +echo ($method->getClosureCalledClass() === null ? "L" : "l") . ":"; +$static = new \ReflectionMethod(Target::class, "stat"); +echo ($static->isStatic() ? "S" : "s");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "sample:EvalReflectNameNs:Y:iU:a:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:x|run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S" + ); +} + +/// Verifies eval ReflectionFunction/Method derive deprecation predicates from `#[Deprecated]`. +#[test] +fn test_eval_reflection_function_and_method_deprecated_attributes() { + let out = compile_and_run_capture( + r#"isDeprecated() ? "D" : "d") . ":"; +echo ($plainFn->isDeprecated() ? "D" : "d") . ":"; +echo ($deprecatedMethod->isDeprecated() ? "D" : "d") . ":"; +echo ($plainMethod->isDeprecated() ? "D" : "d");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "D:d:D:d"); +} + +/// Verifies eval ReflectionFunction/Method expose static local variables through the bridge. +#[test] +fn test_eval_reflection_function_and_method_static_variables() { + let out = compile_and_run_capture( + r#"getStaticVariables(); +echo $beforeFn["count"] . ":" . $beforeFn["label"] . ":"; +echo eval_reflect_static_fn() . ":"; +$afterFn = $fn->getStaticVariables(); +echo $afterFn["count"] . ":" . $afterFn["label"] . "|"; +$object = new EvalReflectStaticMethodChild(); +$method = new ReflectionMethod("EvalReflectStaticMethodChild", "tick"); +$beforeMethod = $method->getStaticVariables(); +echo $beforeMethod["count"] . ":" . $beforeMethod["label"] . ":"; +echo $method->invoke($object) . ":"; +$afterMethod = $method->getStaticVariables(); +echo $afterMethod["count"] . ":" . $afterMethod["label"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:fn:2:2:fn|3:method:4:4:method"); +} + +/// Verifies eval ReflectionMethod hasPrototype/getPrototype follow PHP inheritance rules. +#[test] +fn test_eval_reflection_method_reports_eval_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = new ReflectionMethod("EvalProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$own = new ReflectionMethod("EvalProtoChild", "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" + ); +} + +/// Verifies eval ReflectionMethod hasPrototype/getPrototype expose generated/AOT prototypes. +#[test] +fn test_eval_reflection_method_reports_aot_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = new ReflectionMethod("EvalAotProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod("EvalAotProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$staticOverride = new ReflectionMethod("EvalAotProtoChild", "staticRun"); +$staticOverrideProto = $staticOverride->getPrototype(); +echo ($staticOverride->hasPrototype() ? "Y" : "N") . ":"; +echo $staticOverrideProto->getDeclaringClass()->getName() . "::"; +echo $staticOverrideProto->getName() . ":"; +$staticIface = new ReflectionMethod("EvalAotProtoChild", "staticIface"); +$staticIfaceProto = $staticIface->getPrototype(); +echo ($staticIface->hasPrototype() ? "Y" : "N") . ":"; +echo $staticIfaceProto->getDeclaringClass()->getName() . "::"; +echo $staticIfaceProto->getName() . ":"; +$own = new ReflectionMethod("EvalAotProtoChild", "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalAotProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +} catch (Throwable $e) { + echo "ERR:" . get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:EvalAotProtoBase::run:Y:EvalAotProtoIface::iface:EvalAotProtoParentIface::parented:Y:EvalAotProtoBase::staticrun:Y:EvalAotProtoIface::staticiface:N:E:N" + ); +} + +/// Verifies eval ReflectionMethod prototypes include inherited AOT interfaces. +#[test] +fn test_eval_reflection_method_reports_inherited_aot_interface_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($leaf->hasPrototype() ? "L" : "l") . ":"; +echo $leafProto->getDeclaringClass()->getName() . "::" . $leafProto->getName() . ":"; +$child = new ReflectionMethod("EvalProtoAotParentChild", "inheritedIface"); +$childProto = $child->getPrototype(); +echo ($child->hasPrototype() ? "C" : "c") . ":"; +echo $childProto->getDeclaringClass()->getName() . "::" . $childProto->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "L:EvalAotProtoRootIface::inheritediface:C:EvalAotProtoParentWithIface::inheritediface" + ); +} + +/// Verifies eval ReflectionMethod prototypes preserve staticness for AOT targets. +#[test] +fn test_eval_reflection_method_reports_static_aot_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($parent->hasPrototype() ? "P" : "p") . ":"; +echo $parentProto->getDeclaringClass()->getName() . "::" . $parentProto->getName() . ":"; +$iface = new ReflectionMethod("EvalStaticProtoImpl", "staticIface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "I" : "i") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::" . $ifaceProto->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "P:EvalAotStaticProtoParent::staticrun:I:EvalAotStaticProtoIface::staticiface" + ); +} + +/// Verifies eval-declared functions share method-style named/default/ref/variadic binding. +#[test] +fn test_eval_declared_function_rich_argument_binding() { + let out = compile_and_run_capture( + r#" "z", "name" => "cb"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "ok:2:1:6:cb:2:1:z"); +} + +/// Verifies eval ReflectionFunction::invoke and invokeArgs call eval-declared functions. +#[test] +fn test_eval_reflection_function_invoke_calls_eval_function() { + let out = compile_and_run_capture( + r#"invoke(right: "2", left: "1", extra: "X") . ":"; +echo $ref->invokeArgs(["extra" => "Y", "left" => "3", "right" => "4"]) . ":"; +$value = "Q"; +$mutate = new ReflectionFunction("eval_reflect_no_writeback"); +echo $mutate->invoke($value) . ":" . $value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "121X:341Y:Q!:Q"); +} + +/// Verifies eval ReflectionFunction::invokeArgs accepts runtime-built AOT argument arrays. +#[test] +fn test_eval_reflection_function_invoke_args_accepts_runtime_aot_arg_arrays() { + let out = compile_and_run_capture( + r#"invokeArgs($args) . ":"; +return $ref->invoke(left: "Q");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "XY:QB"); +} + +/// Verifies eval ReflectionParameter exposes generated/AOT function defaults. +#[test] +fn test_eval_reflection_parameter_exposes_aot_function_defaults() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $params[0]->getName() . ":"; +echo ($params[0]->isDefaultValueAvailable() ? "bad" : "required") . ":"; +echo $params[1]->getName() . ":"; +echo ($params[1]->isOptional() ? "O" : "r") . ":"; +echo ($params[1]->isDefaultValueAvailable() ? "D" : "d") . ":"; +echo $params[1]->getDefaultValue() . ":"; +$direct = new ReflectionParameter("eval_aot_reflect_default_function", "items"); +echo $direct->getName() . ":"; +echo ($direct->isOptional() ? "O" : "r") . ":"; +$default = $direct->getDefaultValue(); +return count($default) . ":" . $default[0] . ":" . $default[1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "left:required:right:O:D:B:items:O:2:1:2"); +} + +/// Verifies eval materializes generated/AOT constant-expression defaults. +#[test] +fn test_eval_aot_callable_constant_expression_defaults() { + let out = compile_and_run_capture( + r#" "two"], + int $global = EVAL_AOT_DEFAULT_GLOBAL_SUM + 1, + string $globalWord = EVAL_AOT_DEFAULT_GLOBAL_WORD . "H", + int $pathFlag = PATHINFO_EXTENSION, + ?string $maybe = null, + float $scale = 1.5 +): string { + return $sum . ":" . $word . ":" . ($flag ? "T" : "F") . ":" . + $items[0] . ":" . $items[1] . ":" . $items[2] . ":" . + $global . ":" . $globalWord . ":" . $pathFlag . ":" . + ($maybe === null ? "null" : "set") . ":" . ($scale > 1.0 ? "float" : "bad"); +} + +class EvalAotDefaultExpressionDep { + public string $label; + + public function __construct(string $label = "base") { + $this->label = $label; + } +} + +interface EvalAotDefaultExpressionIface { + public const IFACE_TAG = "I"; +} + +class EvalAotDefaultExpressionBase { + public const OFFSET = 4; + public const TAG = "B"; +} + +class EvalAotDefaultExpressionTarget extends EvalAotDefaultExpressionBase implements EvalAotDefaultExpressionIface { + public const LOCAL = 3; + public const LABEL = "L"; + + public string $label; + + public function __construct( + string $prefix = EvalAotDefaultExpressionTarget::LABEL . EvalAotDefaultExpressionBase::TAG, + int $count = EvalAotDefaultExpressionTarget::LOCAL + EvalAotDefaultExpressionBase::OFFSET + ) { + $this->label = $prefix . ":" . $count; + } + + public function describe( + EvalAotDefaultExpressionDep $dep = new EvalAotDefaultExpressionDep(EvalAotDefaultExpressionBase::TAG . "E"), + array $items = [ + EvalAotDefaultExpressionTarget::LOCAL + EvalAotDefaultExpressionBase::OFFSET, + EvalAotDefaultExpressionIface::IFACE_TAG + ] + ): string { + return $dep->label . ":" . $items[0] . ":" . $items[1]; + } + + public function className(string $name = EvalAotDefaultExpressionTarget::class): string { + return $name; + } +} + +echo eval('$target = new EvalAotDefaultExpressionTarget(); +$ref = new ReflectionFunction("eval_aot_default_expression_function"); +$items = $ref->getParameters()[3]->getDefaultValue(); +return eval_aot_default_expression_function() . "|" + . $target->describe() . "|" + . $target->label . "|" + . $target->className() . "|" + . $items[0] . ":" . $items[2];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3:AB:T:6:xy:two:10:GH:4:null:float|BE:7:I|LB:7|EvalAotDefaultExpressionTarget|6:two" + ); +} + +/// Verifies eval ReflectionParameter exposes generated/AOT function type flags. +#[test] +fn test_eval_reflection_parameter_exposes_aot_function_type_flags() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo ($param->hasType() ? "T" : "t") . ":"; + $type = $param->getType(); + echo ($type ? $type->getName() : "none") . ":"; + echo ($type && $type->allowsNull() ? "N" : "n") . ":"; + echo ($param->isVariadic() ? "V" : "v") . ":"; + echo ($param->isPassedByReference() ? "R" : "r") . "|"; +} +echo ":"; +echo ($ref->hasReturnType() ? "R" : "r") . ":"; +$return = $ref->getReturnType(); +return $return->getName() . ":" . ($return->allowsNull() ? "N" : "n");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int:n:v:R|name:T:string:N:v:r|items:T:array:n:V:r|:R:string:N" + ); +} + +/// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. +#[test] +fn test_eval_reflection_class_cloneable_predicate() { + let out = compile_and_run( + r#"isCloneable() ? "A" : "a"; +echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e";'); +"#, + ); + assert_eq!(out, "aPFvrUite"); +} + +/// Verifies eval `clone` shallow-copies eval-declared objects and runs `__clone()`. +#[test] +fn test_eval_clone_object_expression_runtime_and_hook() { + let out = compile_and_run( + r#"name = $name; } + public function __clone() { $this->name = $this->name . ":clone"; } +} +$first = new EvalCloneRuntimeBox("A"); +$second = clone $first; +echo $first->name; echo ":"; +echo $second->name; echo ":"; +$second->name = "B"; +echo $first->name; echo ":"; +echo $second->name;'); +"#, + ); + assert_eq!(out, "A:A:clone:A:B"); +} + +/// Verifies eval-declared `__destruct()` runs before final release of dynamic objects. +#[test] +fn test_eval_dynamic_object_runs_destructor_on_final_release() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalDestructRuntimeBox("A"); +unset($box); +new EvalDestructRuntimeBox("B"); +echo "after";'); +"#, + ); + assert_eq!(out, "drop:A:drop:B:after"); +} + +/// Verifies eval-declared object destructors run when objects escape eval and native code releases them. +#[test] +fn test_eval_dynamic_object_runs_destructor_after_native_release() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +return new EvalDestructEscapedBox("A");'); +echo "before:"; +unset($box); +echo "after"; +"#, + ); + assert_eq!(out, "before:drop:A:after"); +} + +/// Verifies eval-declared object destructors run when cycle collection releases them. +#[test] +fn test_eval_dynamic_object_runs_destructor_after_cycle_collection() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalCycleDropBox("A"); +$box->self = $box; +unset($box); +echo "after";'); +"#, + ); + assert_eq!(out, "drop:A:after"); +} + +/// Verifies eval-declared subclasses inherit generated/AOT destructors. +#[test] +fn test_eval_dynamic_subclass_runs_inherited_aot_destructor() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} + +eval('class EvalDestructAotChild extends EvalDestructAotParent {} +$box = new EvalDestructAotChild("C"); +echo "body:"; +unset($box); +echo "after";'); +"#, + ); + assert_eq!(out, "body:drop:C:after"); +} + +/// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. +#[test] +fn test_eval_clone_aot_object_expression() { + let out = compile_and_run( + r#"name = $name; + $this->count = $count; + } + + public function run(): void { + eval('$copy = clone $this; +$copy->name = $copy->name . ":copy"; +$copy->count = $copy->count + 10; +echo $this->name; echo ":"; +echo $this->count; echo ":"; +echo $copy->name; echo ":"; +echo $copy->count; echo ":"; +$plain = new stdClass(); +$plain->name = "S"; +$plainCopy = clone $plain; +$plainCopy->name = "S:copy"; +echo $plain->name; echo ":"; +echo $plainCopy->name;'); + } +} + +(new EvalCloneAotBox("A", 2))->run(); +"#, + ); + assert_eq!(out, "A:2:A:copy:12:S:S:copy"); +} + +/// Verifies eval `clone` invokes public AOT `__clone()` hooks after storage copying. +#[test] +fn test_eval_clone_aot_object_runs_clone_hook() { + let out = compile_and_run( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":hook"; + } + + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotHookBox("A"))->run(); +"#, + ); + assert_eq!(out, "A:A:hook"); +} + +/// Verifies eval `clone` invokes inherited AOT `__clone()` hooks for dynamic subclasses. +#[test] +fn test_eval_clone_dynamic_subclass_runs_inherited_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":aot"; + } +} +eval('class EvalCloneAotInheritedHookChild extends EvalCloneAotInheritedHookParent {} +$child = new EvalCloneAotInheritedHookChild("A"); +$childCopy = clone $child; +echo $child->name . ":" . $childCopy->name;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:A:aot"); +} + +/// Verifies an eval `__clone()` override on an AOT-backed subclass owns clone-hook dispatch. +#[test] +fn test_eval_clone_dynamic_subclass_override_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":aot"; + } +} +eval('class EvalCloneAotOverrideHookChild extends EvalCloneAotOverrideHookParent { + public function __clone(): void { + $this->name = $this->name . ":eval"; + } +} +$child = new EvalCloneAotOverrideHookChild("B"); +$childCopy = clone $child; +echo $child->name . ":" . $childCopy->name;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B:B:eval"); +} + +/// Verifies eval `clone` invokes private AOT `__clone()` hooks from the declaring scope. +#[test] +fn test_eval_clone_aot_object_runs_private_clone_hook_in_scope() { + let out = compile_and_run( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } + + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotPrivateHookBox("A"))->run(); +"#, + ); + assert_eq!(out, "A:A:private"); +} + +/// Verifies eval `clone` applies inherited private AOT clone visibility. +#[test] +fn test_eval_clone_dynamic_subclass_respects_private_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } + + public function copyInParent(): void { + eval('$copy = clone $this; +echo $copy->name;'); + } +} +eval('class EvalCloneAotInheritedPrivateChild extends EvalCloneAotInheritedPrivateParent {} +$child = new EvalCloneAotInheritedPrivateChild("A"); +try { + $copy = clone $child; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; +$child->copyInParent();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to private EvalCloneAotInheritedPrivateParent::__clone() from global scope:A:private" + ); +} + +/// Verifies eval subclasses can invoke inherited protected AOT `__clone()` hooks in child scope. +#[test] +fn test_eval_clone_dynamic_subclass_runs_protected_aot_clone_hook_in_child_scope() { + let out = compile_and_run_capture( + r#"name = $name; + } + + protected function __clone(): void { + $this->name = $this->name . ":protected"; + } +} +eval('class EvalCloneAotInheritedProtectedChild extends EvalCloneAotInheritedProtectedParent { + public function copySelf(): void { + $copy = clone $this; + echo $this->name . ":" . $copy->name; + } +} +$child = new EvalCloneAotInheritedProtectedChild("B"); +$child->copySelf();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B:B:protected"); +} + +/// Verifies eval `clone` invokes protected AOT `__clone()` hooks from child scopes. +#[test] +fn test_eval_clone_aot_object_runs_protected_clone_hook_in_child_scope() { + let out = compile_and_run( + r#"name = $name; + } + + protected function __clone(): void { + $this->name = $this->name . ":protected"; + } +} + +class EvalCloneAotProtectedHookChild extends EvalCloneAotProtectedHookBase { + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotProtectedHookChild("B"))->run(); +"#, + ); + assert_eq!(out, "B:B:protected"); +} + +/// Verifies eval `clone` rejects private AOT `__clone()` hooks outside allowed scopes. +#[test] +fn test_eval_clone_aot_object_rejects_private_clone_hook_outside_scope() { + let out = compile_and_run_capture( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } +} + +$object = new EvalCloneAotPrivateOutsideBox("A"); +eval('try { + $copy = clone $object; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to private EvalCloneAotPrivateOutsideBox::__clone() from global scope" + ); +} + +/// Verifies eval method calls cannot directly invoke private AOT `__clone()` out of scope. +#[test] +fn test_eval_rejects_direct_private_aot_clone_method_call_outside_scope() { + let out = compile_and_run_capture( + r#"name = "private"; + } +} + +$object = new EvalCloneAotPrivateDirectBox(); +eval('try { + $object->__clone(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to private method EvalCloneAotPrivateDirectBox::__clone() from global scope" + ); +} + +/// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. +#[test] +fn test_eval_reflection_class_iterable_predicate() { + let out = compile_and_run( + r#"isIterable() ? "P" : "p"; +$iter = new ReflectionClass("EvalIterableIterator"); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; +echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; +echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; +echo (new ReflectionClass("Iterator"))->isIterable() ? "T" : "t"; +echo (new ReflectionClass("ArrayIterator"))->isIterable() ? "R" : "r"; +echo (new ReflectionClass("stdClass"))->isIterable() ? "S" : "s"; +echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; +echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h";'); +"#, + ); + assert_eq!(out, "pIAGbftRseh"); +} + +/// Verifies eval ReflectionClass::isIterable sees interfaces inherited from AOT parents. +#[test] +fn test_eval_reflection_class_iterable_inherits_aot_parent_interface() { + let out = compile_and_run( + r#"isIterable() ? "R" : "r"; +$box = new EvalAotIterableChild(); +echo is_iterable($box) ? "I" : "i";'); +"#, + ); + assert_eq!(out, "RI"); +} + +/// Verifies eval ReflectionClass origin predicates distinguish eval symbols from built-ins. +#[test] +fn test_eval_reflection_class_internal_user_defined_predicates() { + let out = compile_and_run( + r#"isInternal() ? "I" : "i"; + echo $r->isUserDefined() ? "U" : "u"; + echo ":"; +} +eval_reflect_origin("EvalOriginClass"); +eval_reflect_origin("EvalOriginIface"); +eval_reflect_origin("EvalOriginTrait"); +eval_reflect_origin("EvalOriginEnum"); +eval_reflect_origin("stdClass"); +eval_reflect_origin("ReflectionClass"); +eval_reflect_origin("Iterator");'); +"#, + ); + assert_eq!(out, "iU:iU:iU:iU:Iu:Iu:Iu:"); +} + +/// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. +#[test] +fn test_eval_reflection_class_new_instance_constructs_eval_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label() { + return $this->label; + } +} +$ref = new ReflectionClass("EvalReflectNewTarget"); +$first = $ref->newInstance("E", "F"); +echo $first->label() . ":"; +$second = $ref->newInstance(...["G", "H"]); +echo $second->label() . ":"; +$third = $ref->newInstanceArgs(["right" => "J", "left" => "I"]); +echo $third->label() . ":"; +$fourth = $ref->newInstanceArgs(["K", "L"]); +echo $fourth->label();'); +"#, + ); + assert_eq!(out, "EF:GH:IJ:KL"); +} + +/// Verifies eval ReflectionClass::newInstance rejects non-public eval constructors like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_non_public_eval_constructors() { + let out = compile_and_run( + r#"newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" + ); +} + +/// Verifies eval ReflectionClass::newInstance rejects non-public AOT constructors like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_protected_aot_constructor_from_child_scope() { + let out = compile_and_run( + r#"newInstance(); + echo "bad"; + } catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); + }'); + } +} + +EvalReflectNewProtectedAotCtorChild::run(); +"#, + ); + assert_eq!( + out, + "ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedAotCtorBase" + ); +} + +/// Verifies eval ReflectionClass::newInstance constructs generated/AOT classes. +#[test] +fn test_eval_reflection_class_new_instance_constructs_aot_class() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +echo eval('$ref = new ReflectionClass("EvalReflectNewAotTarget"); +$first = $ref->newInstance("A"); +echo $first->label . ":"; +$second = $ref->newInstance(right: "Y", left: "X"); +return $second->label;'); +"#, + ); + assert_eq!(out, "AB:XY"); +} + +/// Verifies eval ReflectionClass::newInstance rejects non-instantiable AOT class-likes. +#[test] +fn test_eval_reflection_class_new_instance_rejects_aot_non_instantiable_class_likes() { + let cases = [ + ( + "abstract class EvalReflectNewAotAbstract {}", + "EvalReflectNewAotAbstract", + "Error:Cannot instantiate abstract class EvalReflectNewAotAbstract", + ), + ( + "interface EvalReflectNewAotIface {}", + "EvalReflectNewAotIface", + "Error:Cannot instantiate interface EvalReflectNewAotIface", + ), + ( + "trait EvalReflectNewAotTrait {}", + "EvalReflectNewAotTrait", + "Error:Cannot instantiate trait EvalReflectNewAotTrait", + ), + ( + "enum EvalReflectNewAotEnum { case Ready; }", + "EvalReflectNewAotEnum", + "Error:Cannot instantiate enum EvalReflectNewAotEnum", + ), + ]; + for (declaration, class_name, expected) in cases { + let source = format!( + r#"newInstance(); + echo "bad"; +}} catch (Error $e) {{ + echo get_class($e) . ":" . $e->getMessage(); +}}'); +"# + ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); + } +} + +/// Verifies eval ReflectionClass instantiation rejects eval non-instantiable class-likes like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { + let out = compile_and_run( + r#"newInstanceWithoutConstructor(); + } else { + $ref->newInstance(); + } + echo "bad"; + } catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); + } +} +eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; +eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", true);'); +"#, + ); + assert_eq!( + out, + "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" + ); +} + +/// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. +#[test] +fn test_eval_reflection_method_invoke_calls_eval_method() { + let out = compile_and_run( + r#"invoke($object, "X") . ":"; +$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); +echo $who->invoke($object) . ":"; +$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X") . ":"; +echo $static->invoke($object, "A") . ":"; +$join = null; +foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { + if ($method->getName() === "join") { + $join = $method; + } +} +$value = "Q"; +$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); +echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]) . ":"; +echo $mutate->invoke($object, $value) . ":" . $value;'); +"#, + ); + assert_eq!( + out, + "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" + ); +} + +/// Verifies eval ReflectionMethod::invoke throws on incompatible receivers. +#[test] +fn test_eval_reflection_method_invoke_rejects_wrong_object() { + let out = compile_and_run( + r#"invoke(new EvalReflectInvokeOther()); + echo "bad"; +} catch (ReflectionException $e) { + echo "caught"; +}'); +"#, + ); + assert_eq!(out, "caught"); +} + +/// Verifies eval ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. +#[test] +fn test_eval_reflection_set_accessible_is_noop() { + let out = compile_and_run( + r#"secret; + } +} +$object = new EvalReflectAccessTarget(); +$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); +echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; +echo $method->invoke($object); echo ":"; +$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); +echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; +echo $property->getValue($object);'); +"#, + ); + assert_eq!(out, "M:s:P:s"); +} + +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates without constructors. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_allocates_eval_class() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label() { + return $this->label; + } + public function secret() { + return $this->secret; + } +} +$ref = new ReflectionClass("EvalReflectNoCtorTarget"); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label() . ":"; +echo $without->secret() . ":"; +$with = $ref->newInstance(); +echo $with->label();'); +"#, + ); + assert_eq!(out, "default:hidden:ctor"); +} + +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates generated/AOT classes. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_allocates_aot_class() { + let out = compile_and_run( + r#"value = 9; + } +} + +echo eval('$ref = new ReflectionClass("EvalReflectNoCtorAotTarget"); +$object = $ref->newInstanceWithoutConstructor(); +echo $object->value . ":"; +echo $ref->isInstantiable() ? "I" : "i";'); +"#, + ); + assert_eq!(out, "4:i"); +} + +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor rejects non-allocatable AOT class-likes. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_rejects_aot_non_classes() { + let cases = [ + ( + "abstract class EvalReflectNoCtorAotAbstract {}", + "EvalReflectNoCtorAotAbstract", + "Error:Cannot instantiate abstract class EvalReflectNoCtorAotAbstract", + ), + ( + "interface EvalReflectNoCtorAotIface {}", + "EvalReflectNoCtorAotIface", + "Error:Cannot instantiate interface EvalReflectNoCtorAotIface", + ), + ( + "trait EvalReflectNoCtorAotTrait {}", + "EvalReflectNoCtorAotTrait", + "Error:Cannot instantiate trait EvalReflectNoCtorAotTrait", + ), + ( + "enum EvalReflectNoCtorAotEnum { case Ready; }", + "EvalReflectNoCtorAotEnum", + "Error:Cannot instantiate enum EvalReflectNoCtorAotEnum", + ), + ]; + for (declaration, class_name, expected) in cases { + let source = format!( + r#"newInstanceWithoutConstructor(); + echo "bad"; +}} catch (Error $e) {{ + echo get_class($e) . ":" . $e->getMessage(); +}}'); +"# + ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); + } +} + +/// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. +#[test] +fn test_eval_reflection_constant_and_enum_case_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + } + public function label() { + return $this->name; + } +} +class EvalConstReflectTarget { + #[EvalConstMarker("const")] + public const ANSWER = 42; +} +enum EvalCaseReflectTarget: string { + #[EvalConstMarker("case")] + case Ready = "ready"; +} +$constAttrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); +echo count($constAttrs) . ":" . (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo $constAttrs[0]->getName() . ":" . $constAttrs[0]->getArguments()[0] . ":"; +echo $constAttrs[0]->newInstance()->label() . ":"; +$caseAttrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo count($caseAttrs) . ":" . (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo $caseAttrs[0]->getName() . ":" . $caseAttrs[0]->getArguments()[0] . ":"; +$unitAttrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; +echo $unitAttrs[0]->newInstance()->label() . ":"; +$backedAttrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue() . ":"; +echo $backedAttrs[0]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:ANSWER:plain:EvalConstMarker:const:const:1:Ready:enum:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" + ); +} + +/// Verifies eval ReflectionClassConstant/EnumCase expose PHP's untyped metadata defaults. +#[test] +fn test_eval_reflection_constant_type_metadata_defaults() { + let out = compile_and_run_capture( + r#"isDeprecated() ? "D" : "d"; echo ":"; +echo $constant->hasType() ? "T" : "t"; echo ":"; +echo $constant->getType() === null ? "N" : "n"; echo ":"; +$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); +echo $case->isDeprecated() ? "D" : "d"; echo ":"; +echo $case->hasType() ? "T" : "t"; echo ":"; +echo $case->getType() === null ? "N" : "n"; echo ":"; +$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); +echo $unit->isDeprecated() ? "D" : "d"; echo ":"; +echo $unit->hasType() ? "T" : "t"; echo ":"; +echo $unit->getType() === null ? "N" : "n"; echo ":"; +$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); +echo $backed->isDeprecated() ? "D" : "d"; echo ":"; +echo $backed->hasType() ? "T" : "t"; echo ":"; +echo $backed->getType() === null ? "N" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "d:t:N:d:t:N:d:t:N:d:t:N"); +} + +/// Verifies eval ReflectionClassConstant/EnumCase stringify retained metadata. +#[test] +fn test_eval_reflection_constant_to_string() { + let out = compile_and_run_capture( + r#"__toString()); + echo "|"; +} +echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Constant [ public int ANSWER ] { 42 }\n|Constant [ final protected int LIMIT ] { 7 }\n|Constant [ private bool FLAG ] { 1 }\n|Constant [ public string LABEL ] { ok }\n|Constant [ public null NOTHING ] { }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n" + ); +} + +/// Verifies eval ReflectionClassConstant exposes visibility predicates and modifiers. +#[test] +fn test_eval_reflection_class_constant_visibility_and_modifiers() { + let out = compile_and_run_capture( + r#"isPrivate() ? "R" : "r"; +echo $secret->isProtected() ? "P" : "p"; +echo $secret->isPublic() ? "U" : "u"; +echo $secret->isFinal() ? "F" : "f"; +echo ":" . $secret->getModifiers() . "\n"; +$limit = new ReflectionClassConstant("EvalConstVisibilityTarget", "LIMIT"); +echo "LIMIT:"; +echo $limit->isPrivate() ? "R" : "r"; +echo $limit->isProtected() ? "P" : "p"; +echo $limit->isPublic() ? "U" : "u"; +echo $limit->isFinal() ? "F" : "f"; +echo ":" . $limit->getModifiers() . "\n"; +$answer = new ReflectionClassConstant("EvalConstVisibilityTarget", "ANSWER"); +echo "ANSWER:"; +echo $answer->isPrivate() ? "R" : "r"; +echo $answer->isProtected() ? "P" : "p"; +echo $answer->isPublic() ? "U" : "u"; +echo $answer->isFinal() ? "F" : "f"; +echo ":" . $answer->getModifiers() . "\n"; +$case = new ReflectionClassConstant("EvalConstVisibilityEnum", "Ready"); +echo "Ready:"; +echo $case->isPrivate() ? "R" : "r"; +echo $case->isProtected() ? "P" : "p"; +echo $case->isPublic() ? "U" : "u"; +echo $case->isFinal() ? "F" : "f"; +echo ":" . $case->getModifiers() . "\n"; +echo "VALUES:" . $secret->getValue() . ":" . $limit->getValue() . ":" . $answer->getValue() . ":"; +echo $case->getValue() === EvalConstVisibilityEnum::Ready ? "E" : "e"; +echo "\n"; +foreach ((new ReflectionClass("EvalConstVisibilityTarget"))->getReflectionConstants() as $constant) { + if ($constant->getName() === "ANSWER") { + echo "LIST:" . $constant->getValue() . "\n"; + } +}'); +echo ReflectionClassConstant::IS_PUBLIC . ":"; +echo ReflectionClassConstant::IS_PROTECTED . ":"; +echo ReflectionClassConstant::IS_PRIVATE . ":"; +echo ReflectionClassConstant::IS_FINAL; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\nVALUES:1:2:3:E\nLIST:3\n1:2:4:32" + ); +} + +/// Verifies eval and AOT enum-case reflectors expose inherited constant predicates. +#[test] +fn test_eval_reflection_enum_case_visibility_and_modifiers() { + let out = compile_and_run_capture( + r#"isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers() . ":"; +} +eval('enum EvalEnumCaseVisibility: string { + case Ready = "ready"; +} +$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); +echo "\nEVAL:"; +foreach ([$unit, $backed] as $case) { + echo $case->isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers() . ":"; +} +echo ReflectionEnumUnitCase::IS_PUBLIC . ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED . ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE . ":"; +echo ReflectionEnumUnitCase::IS_FINAL . ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC . ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED . ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE . ":"; +echo ReflectionEnumBackedCase::IS_FINAL;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "AOT:ErpUf1:ErpUf1:\nEVAL:ErpUf1:ErpUf1:1:2:4:32:1:2:4:32" + ); +} + +/// Verifies ReflectionEnum methods work for enums declared inside eval. +#[test] +fn test_eval_reflection_enum_owner_methods() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo ($pure->isBacked() ? "B" : "b") . ":"; +echo ($pure->getBackingType() === null ? "N" : "n") . ":"; +echo ($pure->hasCase("Ready") ? "R" : "r"); +echo ($pure->hasCase("Missing") ? "M" : "m") . ":"; +$case = $pure->getCase("Done"); +echo $case->getName() . ":"; +echo $case->getEnum()->getName() . ":"; +$cases = $pure->getCases(); +echo count($cases) . ":"; +echo $cases[0]->getName() . ":"; +echo $cases[1]->getEnum()->getName() . ":"; +$backed = new ReflectionEnum("EvalBridgeBacked"); +$type = $backed->getBackingType(); +echo ($backed->isBacked() ? "B" : "b") . ":"; +echo $type->getName() . ":"; +echo ($type->isBuiltin() ? "I" : "i") . ":"; +$backedCase = $backed->getCase("Ready"); +echo $backedCase->getName() . ":"; +echo $backedCase->getBackingValue() . ":"; +echo ($backedCase->getEnum()->isBacked() ? "E" : "e") . ":"; +$backedCases = $backed->getCases(); +echo count($backedCases) . ":"; +echo $backedCases[1]->getBackingValue() . ":"; +echo $backedCases[0]->getEnum()->getBackingType()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalBridgePure:b:N:Rm:Done:EvalBridgePure:2:Ready:EvalBridgePure:B:string:I:Ready:ready:E:2:done:string" + ); +} + +/// Verifies eval ReflectionEnum construction errors are catchable objects. +#[test] +fn test_eval_reflection_enum_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectNotEnumIface"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectNotEnumTrait"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectMissingEnum"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnum("EvalDynReflectActualEnum"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Class \"EvalDynReflectNotEnumClass\" is not an enum|Class \"EvalDynReflectNotEnumIface\" is not an enum|Class \"EvalDynReflectNotEnumTrait\" is not an enum|Class \"EvalDynReflectMissingEnum\" does not exist|EvalDynReflectActualEnum" + ); +} + +/// Verifies eval interface and trait constants work through the bridge. +#[test] +fn test_eval_declared_interface_and_trait_constants() { + let out = compile_and_run_capture( + r#"secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateAccessBox::$secret" + ); +} + +/// Verifies eval throws Error for inaccessible eval-declared method calls. +#[test] +fn test_eval_declared_inaccessible_method_access_throws_error() { + let out = compile_and_run( + r#"hidden(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo EvalPrivateMethodAccessBox::seed(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Call to private method EvalPrivateMethodAccessBox::hidden() from global scope|Error:Call to protected method EvalPrivateMethodAccessBox::seed() from global scope" + ); +} + +/// Verifies eval throws Error for protected class constant access from outside the declaring class. +#[test] +fn test_eval_declared_protected_class_constant_access_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Cannot access protected constant EvalProtectedConstAccessBox::SECRET" + ); +} + +/// Verifies eval throws Error for private static member access from outside the declaring class. +#[test] +fn test_eval_declared_private_static_member_access_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateStaticAccessBox::$secret" + ); +} + +/// Verifies duplicate eval-declared functions fail through the runtime bridge. +#[test] +fn test_eval_duplicate_declared_function_fails() { + let err = compile_and_run_expect_failure( + r#"x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +}'); +echo eval('$box = new DynEvalSupported(5); +echo get_class($box) . ":"; +echo $box->bump(4) . ":"; +echo is_a($box, "DynEvalSupported") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1) . ":"; +echo call_user_func_array($call, [2]) . ":"; +return $box->x;'); +"#, + ); + assert_eq!(out, "DynEvalSupported:9:Y10:12:12"); +} + +/// Verifies eval-declared methods support PHP static syntax for `$this` instance calls. +#[test] +fn test_eval_declared_class_static_syntax_calls_instance_methods() { + let out = compile_and_run( + r#"run();'); +"#, + ); + assert_eq!(out, "base:child"); +} + +/// Verifies native object construction can use eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_constructs_object_natively_after_barrier() { + let out = compile_and_run( + r#"x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +}'); +$box = new DynEvalNativeSupported(5); +echo $box->bump(4) . ":"; +$call = [$box, "bump"]; +echo call_user_func($call, 1) . ":"; +echo call_user_func_array($call, [2]) . ":"; +echo $box->x; +"#, + ); + assert_eq!(out, "9:10:12:12"); +} + +/// Verifies native dynamic `new $class` can instantiate eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_dynamic_new_natively_after_barrier() { + let out = compile_and_run( + r#"label = $label; } + public function read() { return $this->label; } +} +eval('class DynEvalNativeDynamicNew { + public int $label; + public function __construct($label) { $this->label = $label; } + public function read() { return $this->label; } +}'); +$class = "DynEvalNativeDynamicNew"; +$box = new $class(5); +echo $box->read() . ":"; +$aotClass = "DynEvalNativeDynamicNewAot"; +$aotBox = new $aotClass(7); +echo $aotBox->read(); +"#, + ); + assert_eq!(out, "5:7"); +} + +/// Verifies native property writes can update eval-created objects after the barrier. +#[test] +fn test_eval_declared_class_native_property_write_after_barrier() { + let out = compile_and_run( + r#"x = 8; +$box->label = "new"; +echo $box->label . ":"; +echo $box->x; +"#, + ); + assert_eq!(out, "new:8"); +} + +/// Verifies native introspection sees eval-declared object metadata after the barrier. +#[test] +fn test_eval_declared_class_native_introspection_after_barrier() { + let out = compile_and_run( + r#"value + 1; } + private function hidden() { return $this->secret; } +}'); +$box = new DynEvalNativeMemberProbe(); +echo method_exists($box, "bump") ? "M" : "m"; +echo ":"; +echo method_exists("DynEvalNativeMemberProbe", "bump") ? "C" : "c"; +echo ":"; +echo method_exists($box, "missing") ? "x" : "X"; +echo ":"; +echo property_exists($box, "value") ? "P" : "p"; +echo ":"; +echo property_exists("DynEvalNativeMemberProbe", "value") ? "S" : "s"; +echo ":"; +echo property_exists($box, "missing") ? "y" : "Y"; +echo ":"; +echo function_exists("method_exists") ? "F" : "f"; +echo function_exists("property_exists") ? "G" : "g"; +"#, + ); + assert_eq!(out, "M:C:X:P:S:Y:FG"); +} + +/// Verifies native class-relation probes see eval-declared metadata after the barrier. +#[test] +fn test_eval_declared_class_native_relations_after_barrier() { + let out = compile_and_run( + r#" "old"]; +}'); +DynEvalNativeStaticArrayWrite::$items[] = 4; +DynEvalNativeStaticArrayWrite::$items[0] = 5; +DynEvalNativeStaticArrayWrite::$map["a"] = "new"; +DynEvalNativeStaticArrayWrite::$map["b"] = "bee"; +echo DynEvalNativeStaticArrayWrite::$items[0] . ":"; +echo DynEvalNativeStaticArrayWrite::$items[1] . ":"; +echo DynEvalNativeStaticArrayWrite::$map["a"] . ":"; +echo DynEvalNativeStaticArrayWrite::$map["b"]; +"#, + ); + assert_eq!(out, "5:4:new:bee"); +} + +/// Verifies eval class declarations from a namespace are registered globally. +#[test] +fn test_eval_declared_class_in_namespace_is_global() { + let out = compile_and_run( + r#"label(); +"#, + ); + assert_eq!(out, "0:1:global"); +} + +/// Verifies eval-declared by-reference promoted properties remain aliased after construction. +#[test] +fn test_eval_declared_class_aliases_by_reference_promoted_property() { + let out = compile_and_run( + r#"value = 5; +echo $value . ":"; +$value = 7; +return $box->value;'); +"#, + ); + assert_eq!(out, "5:7"); +} + +/// Verifies eval promoted by-reference properties alias static and nested property targets. +#[test] +fn test_eval_declared_class_aliases_by_reference_promoted_static_and_nested_properties() { + let out = compile_and_run( + r#"value = 5; +echo DynEvalPromotedStaticRefHolder::$value . ":"; +DynEvalPromotedStaticRefHolder::$value = 7; +echo $box->value . ":"; +$holder = new DynEvalPromotedStaticRefHolder(); +$itemBox = new DynEvalPromotedStaticRefSupported($holder->items[0]); +$itemBox->value = 11; +echo $holder->items[0] . ":"; +$holder->items[0] = 13; +echo $itemBox->value . ":"; +$staticItemBox = new DynEvalPromotedStaticRefSupported(DynEvalPromotedStaticRefHolder::$staticItems[0]); +$staticItemBox->value = 17; +echo DynEvalPromotedStaticRefHolder::$staticItems[0] . ":"; +DynEvalPromotedStaticRefHolder::$staticItems[0] = 19; +return $staticItemBox->value;'); +"#, + ); + assert_eq!(out, "5:7:11:13:17:19"); +} + +/// Verifies eval `class_alias()` supports class-like interface, trait, enum, and class targets. +#[test] +fn test_eval_class_alias_supports_class_like_targets() { + let out = compile_and_run( + r#"isInterface() ? "IR" : "ir"; echo ":"; +echo class_alias("UnitEnum", "EvalAliasUnitEnum") ? "U" : "u"; echo ":"; +echo interface_exists("EvalAliasUnitEnum") ? "UE" : "ue"; echo ":"; +echo class_exists("EvalAliasUnitEnum") ? "bad" : "UC"; echo ":"; +echo class_alias("EvalAliasTrait", "EvalAliasTraitCopy") ? "T" : "t"; echo ":"; +echo trait_exists("EvalAliasTraitCopy") ? "TE" : "te"; echo ":"; +echo class_exists("EvalAliasTraitCopy") ? "bad" : "TC"; echo ":"; +echo is_a("EvalAliasTraitCopy", "EvalAliasTrait", true) ? "TI" : "ti"; echo ":"; +echo class_alias("EvalAliasEnum", "EvalAliasEnumCopy") ? "E" : "e"; echo ":"; +echo enum_exists("EvalAliasEnumCopy") ? "EE" : "ee"; echo ":"; +echo class_exists("EvalAliasEnumCopy") ? "EC" : "bad"; echo ":"; +echo (new ReflectionClass("EvalAliasEnumCopy"))->getName(); echo ":"; +echo EvalAliasEnumCopy::Ready->value; echo ":"; +echo class_alias("EvalAliasClass", "EvalAliasClassCopy") ? "C" : "c"; echo ":"; +echo class_exists("EvalAliasClassCopy") ? "CE" : "ce"; echo ":"; +$declaredClasses = get_declared_classes(); +$classDeclared = false; +$enumDeclared = false; +$classAliasDeclared = false; +$enumAliasDeclared = false; +foreach ($declaredClasses as $name) { + if ($name === "EvalAliasClass") { $classDeclared = true; } + if ($name === "EvalAliasEnum") { $enumDeclared = true; } + if ($name === "EvalAliasClassCopy") { $classAliasDeclared = true; } + if ($name === "EvalAliasEnumCopy") { $enumAliasDeclared = true; } +} +echo ($classDeclared && $enumDeclared && !$classAliasDeclared && !$enumAliasDeclared) ? "DC" : "dc"; echo ":"; +$declaredInterfaces = get_declared_interfaces(); +$ifaceDeclared = false; +$ifaceAliasDeclared = false; +foreach ($declaredInterfaces as $name) { + if ($name === "EvalAliasIface") { $ifaceDeclared = true; } + if ($name === "EvalAliasIfaceCopy") { $ifaceAliasDeclared = true; } +} +echo ($ifaceDeclared && !$ifaceAliasDeclared) ? "DI" : "di"; echo ":"; +$declaredTraits = get_declared_traits(); +$traitDeclared = false; +$traitAliasDeclared = false; +foreach ($declaredTraits as $name) { + if ($name === "EvalAliasTrait") { $traitDeclared = true; } + if ($name === "EvalAliasTraitCopy") { $traitAliasDeclared = true; } +} +return ($traitDeclared && !$traitAliasDeclared) ? "DT" : "dt";'); +"#, + ); + assert_eq!( + out, + "I:IE:IC:II:IR:U:UE:UC:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:DC:DI:DT" + ); +} + +/// Verifies eval can construct an AOT class with no declared constructor. +#[test] +fn test_eval_dynamic_new_constructs_aot_class() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies eval object construction runs an AOT zero-argument constructor. +#[test] +fn test_eval_dynamic_new_runs_zero_arg_constructor() { + let out = compile_and_run( + r#"x = 9; } +} +echo eval('$box = new EvalDynamicNewZeroArgCtor(); return $box->x;'); +"#, + ); + assert_eq!(out, "9"); +} + +/// Verifies eval object construction passes positional arguments to an AOT constructor. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_arg() { + let out = compile_and_run( + r#"x = $x; } +} +echo eval('$box = new EvalDynamicNewOneArgCtor(11); return $box->x;'); +"#, + ); + assert_eq!(out, "11"); +} + +/// Verifies eval dispatches generated/AOT constructors with untyped by-reference params. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#"getMessage() . ":"; +} +return gettype($value) . ":" . $value;'); +"#, + ); + assert_eq!(out, "Exception:ctor-fail:integer:20"); +} + +/// Verifies eval writes nullable scalar by-reference AOT constructor results back to eval variables. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_nullable_scalar_by_ref_args() { + let out = compile_and_run( + r#"value = 11; + } +} + +echo eval('$payload = new EvalDynamicNewObjectRefCtorPayload(); +$box = new EvalDynamicNewObjectRefCtor($payload); +return $payload->value;'); +"#, + ); + assert_eq!(out, "11"); +} + +/// Verifies eval dispatches generated/AOT constructors with iterable by-reference params. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#"x = $x + 2; } + + public static function run(): void { + $box = eval('return new EvalDynamicNewPrivateCtor(3);'); + echo $box->x; + } +} + +EvalDynamicNewPrivateCtor::run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval object construction rejects private AOT constructors outside the declaring scope. +#[test] +fn test_eval_dynamic_new_rejects_private_constructor_from_child_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +EvalDynamicNewPrivateCtorChild::run(); +"#, + ); + assert_eq!( + out, + "Error:Call to private EvalDynamicNewPrivateCtorBase::__construct() from scope EvalDynamicNewPrivateCtorChild" + ); +} + +/// Verifies eval object construction can call protected AOT constructors from child scopes. +#[test] +fn test_eval_dynamic_new_runs_protected_constructor_from_child_scope() { + let out = compile_and_run( + r#"x = $x + 2; } +} + +class EvalDynamicNewProtectedCtorChild extends EvalDynamicNewProtectedCtorBase { + public static function run(): void { + $box = eval('return new EvalDynamicNewProtectedCtorBase(3);'); + echo $box->x; + } +} + +EvalDynamicNewProtectedCtorChild::run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval object construction rejects protected AOT constructors between sibling scopes. +#[test] +fn test_eval_dynamic_new_rejects_protected_constructor_from_sibling_scope() { + let out = compile_and_run( + r#"getMessage(); + }'); + } +} + +EvalDynamicNewProtectedCtorRight::run(); +"#, + ); + assert_eq!( + out, + "Error:Call to protected EvalDynamicNewProtectedCtorLeft::__construct() from scope EvalDynamicNewProtectedCtorRight" + ); +} + +/// Verifies eval object construction fills registered AOT constructor defaults. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_default_arg() { + let out = compile_and_run( + r#"label = $left . $right; + } +} +echo eval('$first = new EvalDynamicNewDefaultCtor("A"); +echo $first->label . ":"; +$second = new EvalDynamicNewDefaultCtor(right: "Y", left: "X"); +return $second->label;'); +"#, + ); + assert_eq!(out, "AB:XY"); +} + +/// Verifies eval materializes generated/AOT empty-array defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_empty_array_default() { + let out = compile_and_run( + r#"count = is_array($items) ? 0 : 9; + } +} +echo eval('$box = new EvalDynamicNewArrayDefaultCtor(); +return $box->count;'); +"#, + ); + assert_eq!(out, "0"); +} + +/// Verifies eval materializes generated/AOT non-empty array defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_array_default_values() { + let out = compile_and_run( + r#"label = $items[0] . ":" . $items[1] . ":" . $items[2]; + } +} +echo eval('$box = new EvalDynamicNewArrayValueDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "4:5:6"); +} + +/// Verifies eval materializes generated/AOT object defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_object_default() { + let out = compile_and_run( + r#"label = $left . $right . $third . $fourth; + } +} + +class EvalDynamicNewObjectDefaultCtor { + public string $label = ""; + public function __construct(EvalDynamicNewObjectDefaultDep $dep = new EvalDynamicNewObjectDefaultDep("c", "t", "o", "r")) { + $this->label = $dep->label; + } +} + +echo eval('$box = new EvalDynamicNewObjectDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "ctor"); +} + +/// Verifies eval materializes nested generated/AOT object defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_nested_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalDynamicNewNestedDefaultOuter { + public EvalDynamicNewNestedDefaultInner $inner; + + public function __construct(EvalDynamicNewNestedDefaultInner $inner = new EvalDynamicNewNestedDefaultInner("outer")) { + $this->inner = $inner; + } +} + +class EvalDynamicNewNestedDefaultCtor { + public string $label = ""; + + public function __construct(EvalDynamicNewNestedDefaultOuter $outer = new EvalDynamicNewNestedDefaultOuter(new EvalDynamicNewNestedDefaultInner("ctor"))) { + $this->label = $outer->inner->label; + } +} + +echo eval('$box = new EvalDynamicNewNestedDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "ctor"); +} + +/// Verifies eval object construction passes more than two arguments to an AOT constructor. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_many_args() { + let out = compile_and_run( + r#"label = ($a + $b + $c) . $suffix; + } +} +echo eval('$box = new EvalDynamicNewManyArgCtor(1, 2, 3, "!"); return $box->label;'); +"#, + ); + assert_eq!(out, "6!"); +} + +/// Verifies inherited AOT methods returning eval results keep the boxed Mixed return ABI. +#[test] +fn test_eval_fragment_in_inherited_aot_method_returns_late_static_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalInheritedAotScopeReturnChild"); +} + +/// Verifies eval ReflectionClass::newInstanceArgs forwards named args to AOT constructors. +#[test] +fn test_eval_reflection_class_new_instance_args_constructs_aot_class() { + let out = compile_and_run( + r#"label = $left . $right; + } +} +echo eval('$ref = new ReflectionClass("EvalReflectNewArgsAotTarget"); +$first = $ref->newInstanceArgs(["right" => "Y", "left" => "X"]); +echo $first->label . ":"; +$second = $ref->newInstanceArgs(["Q", "R"]); +echo $second->label . ":"; +$args = []; +$args["right"] = "N"; +$args["left"] = "M"; +$third = $ref->newInstanceArgs($args); +return $third->label;'); +"#, + ); + assert_eq!(out, "XY:QR:MN"); +} + +/// Verifies eval object construction passes AOT constructor arguments on the caller stack. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_stack_string_arg() { + let out = compile_and_run( + r#"label = $a . $b . $c . $d; + } +} +echo eval('$box = new EvalDynamicNewStackStringCtor("Q", "R", "S", "T"); return $box->label;'); +"#, + ); + assert_eq!(out, "QRST"); +} + +/// Verifies eval follows PHP by accepting constructor arguments when no constructor exists. +#[test] +fn test_eval_dynamic_new_accepts_args_without_constructor() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "4"); +} + +/// Verifies eval object construction fails when no AOT class matches the name. +#[test] +fn test_eval_dynamic_new_missing_class_fails() { + let err = compile_and_run_expect_failure("x;'); +"#, + ); + assert_eq!(out, "13"); +} + +/// Verifies eval namespace imports resolve functions, constants, and AOT class aliases. +#[test] +fn test_eval_fragment_namespace_use_imports() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "6:17"); +} + +/// Verifies eval grouped namespace imports resolve functions, constants, and AOT class aliases. +#[test] +fn test_eval_fragment_grouped_namespace_use_imports() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "8:19"); +} + +/// Verifies eval include executes PHP files through the bridge and shares caller scope. +#[test] +fn test_eval_fragment_include_executes_php_file_and_returns_value() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:eval boom"); +} + +/// Verifies Throwable objects thrown by eval-declared functions cross native call sites. +#[test] +fn test_eval_declared_function_throw_crosses_native_try_catch() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:dyn boom"); +} + +/// Verifies Throwable objects thrown by nested eval calls keep the original catch target. +#[test] +fn test_eval_nested_throw_crosses_caller_try_catch() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:nested boom"); +} + +/// Verifies eval-internal try/catch consumes a thrown Throwable before returning. +#[test] +fn test_eval_try_catch_catches_throwable_inside_eval() { + let out = compile_and_run( + r#" String { + let mut literal = String::with_capacity(value.len() + 2); + literal.push('\''); + for ch in value.chars() { + match ch { + '\\' => literal.push_str("\\\\"), + '\'' => literal.push_str("\\'"), + _ => literal.push(ch), + } + } + literal.push('\''); + literal +} + +/// Verifies namespaced function calls fall back to builtins in AOT and eval code. +#[test] +fn test_namespaced_calls_fall_back_to_builtin_before_and_after_eval() { + let out = compile_and_run( + r#" 1], depth: 512);'); +"#, + ); + + assert_eq!(out, " x:{\"a\":1}"); +} + +/// Verifies eval named builtin calls preserve variadic and by-reference behavior. +#[test] +fn test_eval_named_builtin_arguments_support_variadic_and_by_ref() { + let out = compile_and_run( + r#"items]) . ":"; +echo implode(",", $box->items) . "|"; + +echo call_user_func_array("settype", [&EvalBuiltinRefBridgeBox::$typed, "integer"]) ? "P" : "p"; +echo gettype(EvalBuiltinRefBridgeBox::$typed) . ":" . EvalBuiltinRefBridgeBox::$typed;'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|2:3,1|Pinteger:123"); +} + +/// Verifies eval string-callable ref-like builtins write back through lvalue targets. +#[test] +fn test_eval_string_callable_ref_like_builtins_write_back_aliases() { + let out = compile_and_run( + r#"items) . ":" . implode(",", $box->items) . "|"; + +$setter = "settype"; +echo $setter(EvalStringBuiltinRefBridgeBox::$typed, "integer") ? "P" : "p"; +echo gettype(EvalStringBuiltinRefBridgeBox::$typed) . ":" . EvalStringBuiltinRefBridgeBox::$typed;'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|2:3,1|Pinteger:77"); +} + +/// Verifies eval `call_user_func_array()` preserves named ref-like builtin targets. +#[test] +fn test_eval_call_user_func_array_ref_like_builtins_write_back_named_aliases() { + let out = compile_and_run( + r#" "/(a)(b)/", "subject" => "ab", "matches" => &$matches] +); +echo ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . "|"; + +$items = ["b" => 2, "a" => 1]; +echo call_user_func_array("ksort", ["array" => &$items]) ? "K" : "k"; +foreach ($items as $key => $value) { + echo $key . $value; +}'); +"#, + ); + + assert_eq!(out, "1:ab:a:b|Ka1b2"); +} + +/// Verifies eval first-class and Closure builtin callables preserve ref-like parameters. +#[test] +fn test_eval_ref_like_builtin_closures_write_back_aliases() { + let out = compile_and_run( + r#" 2, "a" => 1]; +echo call_user_func_array($ksort, ["array" => &$assoc]) ? "K" : "k"; +foreach ($assoc as $key => $entry) { + echo $key . $entry; +}'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|1:ab:a:b|Ka1b2"); +} + +/// Verifies eval `call_user_func()` keeps ref-like builtin Closure args by value. +#[test] +fn test_eval_call_user_func_ref_like_builtin_closures_use_by_value_args() { + let out = compile_and_run( + r#" &$letters, "offset" => 1, "length" => 2, "replacement" => ["x", "y"]] +); +echo implode(",", $removed) . ":" . implode(",", $letters) . "|"; + +$walk = Closure::fromCallable("array_walk"); +$walked = [1, 2]; +$callback = function (&$value, $key) { $value = ($value * 10) + $key; }; +echo $walk($walked, $callback) ? "W:" : "w:"; +echo implode(",", $walked) . "|"; + +$pregAll = preg_match_all(...); +$matches = []; +echo $pregAll("/a(.)/", "ab ac", $matches); +echo ":" . implode(",", $matches[0]) . ":" . implode(",", $matches[1]);'); +"#, + ); + + assert_eq!(out, "3:1,2,3|2:a,b|b,c:a,x,y,d|W:10,21|2:ab,ac:b,c"); +} + +/// Verifies ref-like builtin callbacks preserve writeback through AOT callable parameters. +#[test] +fn test_eval_ref_like_builtin_callables_pass_to_aot_callable_params() { + let out = compile_and_run_capture( + r#"value = $label . ":" . ($ok ? "T" : "F") . ":" . implode(",", $items); + } + + public function convert(callable $callback, string $label): string { + $value = $label === "never" ? "x" : 42; + $ok = $callback($value, "string"); + return $label . ":" . ($ok ? "T" : "F") . ":" . gettype($value) . ":" . $value; + } + + public static function match(callable $callback, string $label): string { + $matches = [0, ""]; + $count = $callback("/(a)(b)/", "ab", $matches); + return $label . ":" . $count . ":" . implode(":", $matches); + } + + public function push(callable $callback, string $label): string { + $items = $label === "never" ? [0] : ["a"]; + $count = $callback($items, "b"); + return $label . ":" . $count . ":" . implode(",", $items); + } +} + +echo eval('$out = []; +$box = new EvalRefLikeBuiltinCallableBridge("sort", "s"); +$out[] = $box->value; +$box = new EvalRefLikeBuiltinCallableBridge(sort(...), "f"); +$out[] = $box->value; +$box = new EvalRefLikeBuiltinCallableBridge(Closure::fromCallable("sort"), "c"); +$out[] = $box->value; + +$box = new EvalRefLikeBuiltinCallableBridge("sort", "seed"); +$out[] = $box->convert("settype", "s"); +$out[] = $box->convert(settype(...), "f"); +$out[] = $box->convert(Closure::fromCallable("settype"), "c"); + +$out[] = EvalRefLikeBuiltinCallableBridge::match("preg_match", "s"); +$out[] = EvalRefLikeBuiltinCallableBridge::match(preg_match(...), "f"); +$out[] = EvalRefLikeBuiltinCallableBridge::match(Closure::fromCallable("preg_match"), "c"); + +$out[] = $box->push("array_push", "s"); +$out[] = $box->push(array_push(...), "f"); +$out[] = $box->push(Closure::fromCallable("array_push"), "c"); + +return implode("|", $out);'); +"#, + ); + + assert!( + out.success, + "stdout:\n{}\nstderr:\n{}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "s:T:1,2,3|f:T:1,2,3|c:T:1,2,3|\ +s:T:string:42|f:T:string:42|c:T:string:42|\ +s:1:ab:a:b|f:1:ab:a:b|c:1:ab:a:b|\ +s:2:a,b|f:2:a,b|c:2:a,b" + ); +} diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs new file mode 100644 index 0000000000..43430dc3aa --- /dev/null +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -0,0 +1,717 @@ +//! Purpose: +//! End-to-end regressions for eval callable by-reference writeback on error paths. +//! Covers callable values crossing eval into generated/AOT and eval-declared methods. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_callable_ref_errors` through Rust's test harness. +//! +//! Key details: +//! - Fixtures verify caller-side by-reference values are written back before a +//! method callable's catchable throw is returned through the eval bridge. + +use crate::support::{compile_and_run, compile_and_run_capture}; + +/// Verifies AOT function callables write back by-reference args before catchable throws. +#[test] +fn test_eval_aot_function_callables_write_back_by_ref_args_before_throw() { + let out = compile_and_run_capture( + r#"getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$first = eval_aot_throw_ref_add(...); +$b = "4"; +try { + $first($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$closure = Closure::fromCallable("eval_aot_throw_ref_add"); +$c = "6"; +try { + $closure($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Exception:aot-function:integer:5|Exception:aot-function:integer:9|Exception:aot-function:integer:13|Exception:aot-function:integer:17" + ); +} + +/// Verifies AOT function argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_function_by_ref_arg_prep_fatal_cleans_up_stack() { + let cases = [ + ( + "string callable", + r#"base + $delta; + throw new Exception("aot-instance"); + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + throw new Exception("aot-static"); + } +} + +echo eval('$box = new EvalAotThrowCallableBridge(); + +$array = [$box, "bump"]; +$a = "2"; +try { + $array($a, 3); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$string = "EvalAotThrowCallableBridge::add"; +$b = "4"; +try { + $string($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$first = $box->bump(...); +$c = "6"; +try { + $first($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$closure = Closure::fromCallable(["EvalAotThrowCallableBridge", "add"]); +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Exception:aot-instance:integer:15|Exception:aot-static:integer:9|Exception:aot-instance:integer:23|Exception:aot-static:integer:17" + ); +} + +/// Verifies eval-declared method callable by-reference args write back before catchable throws. +#[test] +fn test_eval_declared_method_callables_write_back_by_ref_args_before_throw() { + let out = compile_and_run( + r#"base + $delta; + throw new Exception("eval-instance"); + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + throw new Exception("eval-static"); + } +} + +$box = new EvalDeclaredThrowCallableBridge(); + +$array = [$box, "bump"]; +$a = "2"; +try { + $array($a, 3); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$string = "EvalDeclaredThrowCallableBridge::add"; +$b = "4"; +try { + $string($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$first = $box->bump(...); +$c = "6"; +try { + $first($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$closure = Closure::fromCallable(["EvalDeclaredThrowCallableBridge", "add"]); +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert_eq!( + out, + "Exception:eval-instance:integer:25|Exception:eval-static:integer:9|Exception:eval-instance:integer:33|Exception:eval-static:integer:17" + ); +} + +/// Verifies AOT instance method argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_instance_method_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#"bump(...); +$value = "2"; +$callback(need: 123, value: $value); +echo "bad";'); +"#, + ), + ( + "Closure::fromCallable instance", + r#"bump(...); +$value = "2"; +$callback($value, 123); +echo "bad";'); +"#, + ); + + assert!( + !out.success, + "expected eval runtime fatal, stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, ""); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); + assert!( + !out.stderr.contains("panicked at") && !out.stderr.contains("thread '"), + "stderr leaked a Rust panic: {}", + out.stderr + ); +} + +/// Verifies AOT `Closure::fromCallable()` method argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_closure_from_callable_method_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableArrayRefBox(); +$instance = [$box, "bump"]; +$a = "2"; +echo $instance($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$b = "4"; +echo call_user_func_array($instance, [&$b, 5]) . ":" . gettype($b) . ":" . $b . "|"; +$static = ["EvalAotCallableArrayRefBox", "add"]; +$c = "7"; +echo $static($c, 6) . ":" . gettype($c) . ":" . $c . "|"; +$d = "8"; +return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|13:integer:13|17:integer:17" + ); +} + +/// Verifies eval first-class AOT method callables preserve by-ref writeback. +#[test] +fn test_eval_aot_first_class_method_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotFirstClassRefBox(); +$method = $box->bump(...); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$static = EvalAotFirstClassRefBox::add(...); +$b = "4"; +echo $static($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$name = "EvalAotFirstClassRefBox::add"; +$c = "6"; +echo $name($c, 7) . ":" . gettype($c) . ":" . $c . "|"; +$d = "8"; +return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "25:integer:25|9:integer:9|13:integer:13|17:integer:17" + ); +} + +/// Verifies eval AOT callables preserve named by-ref argument writeback. +#[test] +fn test_eval_aot_callable_named_ref_args_preserve_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableNamedRefBox(); + +$array = [$box, "bump"]; +$a = "2"; +echo $array(value: $a, delta: 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = $box->bump(...); +$b = "4"; +echo $first(delta: 5, value: $b) . ":" . gettype($b) . ":" . $b . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$c = "6"; +echo $closure(delta: 7, value: $c) . ":" . gettype($c) . ":" . $c . "|"; + +$string = "EvalAotCallableNamedRefBox::add"; +$d = "8"; +echo $string(delta: 9, value: $d) . ":" . gettype($d) . ":" . $d . "|"; + +$static = EvalAotCallableNamedRefBox::add(...); +$e = "10"; +echo $static(value: $e, delta: 11) . ":" . gettype($e) . ":" . $e . "|"; + +$invokable = new EvalAotCallableNamedRefBox(); +$f = "12"; +return $invokable(delta: 13, value: $f) . ":" . gettype($f) . ":" . $f;'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|23:integer:23|17:integer:17|21:integer:21|35:integer:35" + ); +} + +/// Verifies eval `call_user_func_array()` preserves named AOT by-ref argument aliases. +#[test] +fn test_eval_call_user_func_array_aot_callable_named_ref_args_preserve_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$function = "eval_call_array_aot_named_ref_add"; +$a = "2"; +echo call_user_func_array($function, ["delta" => 3, "value" => &$a]) . + ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_array_aot_named_ref_add(...); +$b = "4"; +echo call_user_func_array($first, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallArrayAotNamedRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func_array($array, ["delta" => 7, "value" => &$c]) . + ":" . gettype($c) . ":" . $c . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$d = "8"; +echo call_user_func_array($closure, ["value" => &$d, "delta" => 9]) . + ":" . gettype($d) . ":" . $d . "|"; + +$string = "EvalCallArrayAotNamedRefBox::add"; +$e = "10"; +echo call_user_func_array($string, ["delta" => 11, "value" => &$e]) . + ":" . gettype($e) . ":" . $e . "|"; + +$static = EvalCallArrayAotNamedRefBox::add(...); +$f = "12"; +echo call_user_func_array($static, ["value" => &$f, "delta" => 13]) . + ":" . gettype($f) . ":" . $f . "|"; + +$invokable = new EvalCallArrayAotNamedRefBox(); +$g = "14"; +return call_user_func_array($invokable, ["delta" => 15, "value" => &$g]) . + ":" . gettype($g) . ":" . $g;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|9:integer:9|23:integer:23|27:integer:27|21:integer:21|25:integer:25|39:integer:39" + ); +} + +/// Verifies eval `call_user_func_array()` preserves named eval-declared by-ref aliases. +#[test] +fn test_eval_call_user_func_array_declared_callable_named_ref_args_preserve_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +$function = "eval_call_array_declared_named_ref_add"; +$a = "2"; +echo call_user_func_array($function, ["delta" => 3, "value" => &$a]) . + ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_array_declared_named_ref_add(...); +$b = "4"; +echo call_user_func_array($first, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallArrayDeclaredNamedRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func_array($array, ["delta" => 7, "value" => &$c]) . + ":" . gettype($c) . ":" . $c . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$d = "8"; +echo call_user_func_array($closure, ["value" => &$d, "delta" => 9]) . + ":" . gettype($d) . ":" . $d . "|"; + +$string = "EvalCallArrayDeclaredNamedRefBox::add"; +$e = "10"; +echo call_user_func_array($string, ["delta" => 11, "value" => &$e]) . + ":" . gettype($e) . ":" . $e . "|"; + +$static = EvalCallArrayDeclaredNamedRefBox::add(...); +$f = "12"; +echo call_user_func_array($static, ["value" => &$f, "delta" => 13]) . + ":" . gettype($f) . ":" . $f . "|"; + +$invokable = new EvalCallArrayDeclaredNamedRefBox(); +$g = "14"; +return call_user_func_array($invokable, ["delta" => 15, "value" => &$g]) . + ":" . gettype($g) . ":" . $g;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|9:integer:9|23:integer:23|27:integer:27|21:integer:21|25:integer:25|39:integer:39" + ); +} + +/// Verifies eval `call_user_func()` keeps AOT callable by-reference args by value. +#[test] +fn test_eval_call_user_func_aot_callable_forms_use_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$string = "eval_call_user_func_aot_ref_add"; +$a = "2"; +echo call_user_func($string, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_user_func_aot_ref_add(...); +$b = "4"; +echo call_user_func($first, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallUserFuncAotRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func($array, $c, 7) . ":" . gettype($c) . ":" . $c . "|"; + +$staticArray = ["EvalCallUserFuncAotRefBox", "add"]; +$d = "8"; +echo call_user_func($staticArray, $d, 9) . ":" . gettype($d) . ":" . $d . "|"; + +$staticString = "EvalCallUserFuncAotRefBox::add"; +$e = "10"; +echo call_user_func($staticString, $e, 11) . ":" . gettype($e) . ":" . $e . "|"; + +$invokable = new EvalCallUserFuncAotRefBox(); +$f = "12"; +return call_user_func($invokable, $f, 13) . ":" . gettype($f) . ":" . $f;'); +"#, + ); + + assert_eq!( + out, + "5:string:2|9:string:4|23:string:6|17:string:8|21:string:10|35:string:12" + ); +} + +/// Verifies eval-declared callable forms preserve by-ref writeback. +#[test] +fn test_eval_declared_callable_forms_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +$string = "eval_declared_ref_add"; +$a = "2"; +echo $string($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_declared_ref_add(...); +$b = "4"; +echo $first($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$c = "6"; +echo call_user_func_array($first, [&$c, 7]) . ":" . gettype($c) . ":" . $c . "|"; + +$box = new EvalDeclaredRefCallableBox(); +$instance = [$box, "bump"]; +$d = "8"; +echo $instance($d, 4) . ":" . gettype($d) . ":" . $d . "|"; +$e = "1"; +echo call_user_func_array($instance, [&$e, 5]) . ":" . gettype($e) . ":" . $e . "|"; + +$static = ["EvalDeclaredRefCallableBox", "add"]; +$f = "7"; +echo $static($f, 6) . ":" . gettype($f) . ":" . $f . "|"; + +$closureFunction = Closure::fromCallable("eval_declared_ref_add"); +$g = "3"; +echo $closureFunction($g, 4) . ":" . gettype($g) . ":" . $g . "|"; + +$closureInstance = Closure::fromCallable([$box, "bump"]); +$h = "2"; +echo $closureInstance($h, 6) . ":" . gettype($h) . ":" . $h . "|"; + +$closureStatic = Closure::fromCallable(["EvalDeclaredRefCallableBox", "add"]); +$i = "5"; +echo $closureStatic($i, 8) . ":" . gettype($i) . ":" . $i . "|"; + +$closureNamedStatic = Closure::fromCallable("EvalDeclaredRefCallableBox::add"); +$j = "6"; +return call_user_func_array($closureNamedStatic, [&$j, 9]) . ":" . gettype($j) . ":" . $j;'); +"#, + ); + + assert_eq!( + out, + concat!( + "5:integer:5|9:integer:9|13:integer:13|22:integer:22|16:integer:16|", + "13:integer:13|7:integer:7|18:integer:18|13:integer:13|15:integer:15" + ) + ); +} + +/// Verifies eval-declared callable forms preserve by-ref variadic element writeback. +#[test] +fn test_eval_declared_callable_forms_preserve_by_ref_variadic_writeback() { + let out = compile_and_run( + r#"collect(...); +$c = "C"; +$d = "D"; +echo $first($c, named: $d) . ":" . $c . ":" . $d . "|"; + +$closure = Closure::fromCallable([$box, "collect"]); +$e = "E"; +$f = "F"; +echo $closure($e, named: $f) . ":" . $e . ":" . $f . "|"; + +$string = "EvalDeclaredVariadicRefCallableBox::collectStatic"; +$g = "G"; +$h = "H"; +echo $string($g, named: $h) . ":" . $g . ":" . $h . "|"; + +$i = "I"; +$j = "J"; +$args = [&$i, "named" => &$j]; +return call_user_func_array($closure, $args) . ":" . $i . ":" . $j . ":" . + $args[0] . ":" . $args["named"];'); +"#, + ); + + assert_eq!( + out, + concat!( + "A-i:B-n:A-i:B-n|C-i:D-n:C-i:D-n|E-i:F-n:E-i:F-n|", + "G-s:H-sn:G-s:H-sn|I-i:J-n:I-i:J-n:I-i:J-n" + ) + ); +} + +/// Verifies AOT function callable forms preserve by-ref variadic element writeback. +#[test] +fn test_eval_aot_function_callable_forms_preserve_by_ref_variadic_writeback() { + let out = compile_and_run_capture( + r#"collect(...); +$c = "C"; +$d = "D"; +echo $first($c, $d) . ":" . $c . ":" . $d . "|"; + +$closure = Closure::fromCallable([$box, "collect"]); +$e = "E"; +$f = "F"; +echo $closure($e, $f) . ":" . $e . ":" . $f . "|"; + +$string = "EvalAotVariadicRefCallableBox::collectStatic"; +$g = "G"; +$h = "H"; +echo $string($g, $h) . ":" . $g . ":" . $h . "|"; + +$static = EvalAotVariadicRefCallableBox::collectStatic(...); +$i = "I"; +$j = "J"; +echo $static($i, $j) . ":" . $i . ":" . $j . "|"; + +$k = "K"; +$l = "L"; +$args = [&$k, &$l]; +return call_user_func_array($closure, $args) . ":" . $k . ":" . $l . ":" . + $args[0] . ":" . $args[1];'); +"#, + ); + + assert_eq!( + out, + concat!( + "A-i:B-j:A-i:B-j|C-i:D-j:C-i:D-j|E-i:F-j:E-i:F-j|", + "G-s:H-t:G-s:H-t|I-s:J-t:I-s:J-t|K-i:L-j:K-i:L-j:K-i:L-j" + ) + ); +} + +/// Verifies eval first-class callables are PHP-visible `Closure` objects and remain invokable. +#[test] +fn test_eval_first_class_callables_are_php_closure_objects() { + let out = compile_and_run( + r#"m(...); +$s = EvalAotFirstClassObjectBox::s(...); +$class = "EvalAotFirstClassObjectBox"; +$ds = $class::s(...); +$i = $box(...); +foreach ([$f, $m, $s, $ds, $i] as $cb) { + echo is_object($cb) ? "O" : "o"; + echo get_class($cb); + echo $cb instanceof Closure ? "I" : "i"; + echo is_callable($cb) ? "C" : "c"; + echo "|"; +} +return $f("1") . ":" . $m("2") . ":" . $s("3") . ":" . $ds("4") . ":" . $i("5");'); +"#, + ); + + assert_eq!( + out, + "OClosureIC|OClosureIC|OClosureIC|OClosureIC|OClosureIC|F1:M2:S3:S4:I5" + ); +} + +/// Verifies namespaced eval first-class function callables use PHP builtin fallback rules. +#[test] +fn test_eval_first_class_function_callables_follow_namespace_fallback() { + let out = compile_and_run( + r#"missing(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + $box->hidden(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::missing(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::inst(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::secret(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$ok = EvalFirstClassInvalidTargets::ok(...); +echo $ok();'); +"#, + ); + + assert_eq!( + out, + "Error:Call to undefined method EvalFirstClassInvalidTargets::missing()|\ +Error:Call to private method EvalFirstClassInvalidTargets::hidden() from global scope|\ +Error:Call to undefined method EvalFirstClassInvalidTargets::missing()|\ +Error:Non-static method EvalFirstClassInvalidTargets::inst() cannot be called statically|\ +Error:Call to private method EvalFirstClassInvalidTargets::secret() from global scope|OK" + ); +} + +/// Verifies eval first-class callables preserve PHP magic-method fallback. +#[test] +fn test_eval_first_class_callable_validation_preserves_magic_fallback() { + let out = compile_and_run( + r#"hidden(...); +echo $hidden("a") . "|"; +$missing = $box->missing(...); +echo $missing("b") . "|"; +$secret = EvalFirstClassMagicTargets::secret(...); +echo $secret("c") . "|"; +$staticMissing = EvalFirstClassMagicTargets::missingStatic(...); +echo $staticMissing("d");'); +"#, + ); + + assert_eq!(out, "I:hidden:a|I:missing:b|S:secret:c|S:missingStatic:d"); +} + +/// Verifies `self::method(...)` inside an instance eval method captures `$this`. +#[test] +fn test_eval_first_class_callable_validation_static_syntax_instance_method_captures_this() { + let out = compile_and_run( + r#"base + $value; + } + + public static function makeStatic() { + try { + self::add(...); + return "bad"; + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + } + } +} + +$box = new EvalFirstClassStaticSyntaxThis(); +echo $box->make() . "|"; +echo EvalFirstClassStaticSyntaxThis::makeStatic();'); +"#, + ); + + assert_eq!( + out, + "12|Error:Non-static method EvalFirstClassStaticSyntaxThis::add() cannot be called statically" + ); +} + +/// Verifies eval string callbacks resolve special class names through method scope. +#[test] +fn test_eval_string_special_class_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run() { + $self = "self::add"; + $first = "2"; + echo is_callable($self) ? "C:" : "c:"; + echo call_user_func_array($self, [&$first, 3]) . ":" . gettype($first) . ":" . $first . "|"; + + $static = "static::add"; + $second = "4"; + echo call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = "parent::bump"; + $third = "6"; + echo call_user_func_array($parent, [&$third, 7]) . ":" . gettype($third) . ":" . $third; + } +} + +$box = new EvalStringSpecialCallableChild(); +$box->run();'); +"#, + ); + + assert_eq!(out, "C:15:integer:15|19:integer:19|13:integer:13"); +} + +/// Verifies eval first-class callbacks resolve special class names and preserve by-ref writeback. +#[test] +fn test_eval_first_class_special_class_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run() { + $self = self::add(...); + $first = "2"; + echo $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + + $static = static::add(...); + $second = "4"; + echo call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = parent::bump(...); + $third = "6"; + echo $parent($third, 7) . ":" . gettype($third) . ":" . $third; + } +} + +$box = new EvalFirstClassSpecialCallableChild(); +$box->run();'); +"#, + ); + + assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); +} + +/// Verifies `Closure::fromCallable()` resolves special string callables through method scope. +#[test] +fn test_eval_closure_from_callable_special_string_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run(): string { + $self = Closure::fromCallable("self::add"); + $first = "2"; + $out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + + $static = Closure::fromCallable("static::add"); + $second = "4"; + $out .= call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = Closure::fromCallable("parent::bump"); + $third = "6"; + $out .= $parent($third, 7) . ":" . gettype($third) . ":" . $third; + + return $out; + } +} + +return (new EvalFromCallableSpecialStringChild())->run();'); +"#, + ); + + assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); +} + +/// Verifies special first-class instance closures retain `$this` after method scope exits. +#[test] +fn test_eval_first_class_special_instance_callables_persist_bound_receiver() { + let out = compile_and_run( + r#"base + $delta; + return "C:" . get_called_class() . ":" . get_class($this) . ":" . $value; + } + + public function make(): array { + return [ + self::add(...), + static::add(...), + parent::base(...), + ]; + } +} + +$closures = (new EvalFirstClassPersistentInstanceChild())->make(); +$self = $closures[0]; +$static = $closures[1]; +$parent = $closures[2]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "3"; +$out .= call_user_func_array($static, [&$second, 3]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "4"; +$out .= $parent($third, 3) . ":" . gettype($third) . ":" . $third; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:15:integer:15|\ +C:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:16:integer:16|\ +B:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:7:integer:7" + ); +} + +/// Verifies special static string closures remain callable after leaving method scope. +#[test] +fn test_eval_closure_from_callable_special_static_string_callables_persist_resolved_scope() { + let out = compile_and_run( + r#"make(); +$self = $closures[0]; +$static = $closures[1]; +$parent = $closures[2]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "4"; +$out .= call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "6"; +$out .= $parent($third, 7) . ":" . gettype($third) . ":" . $third; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFromCallablePersistentSpecialChild:5:integer:5|\ +C:EvalFromCallablePersistentSpecialChild:9:integer:9|\ +B:EvalFromCallablePersistentSpecialChild:13:integer:13" + ); +} + +/// Verifies special static array-callable closures remain callable after method scope exits. +#[test] +fn test_eval_closure_from_callable_special_static_array_callables_persist_resolved_scope() { + let out = compile_and_run( + r#"base + $delta; + return "C:" . get_called_class() . ":" . get_class($this) . ":" . $value; + } + + public function make(): array { + return [ + Closure::fromCallable("self::add"), + Closure::fromCallable(["static", "add"]), + Closure::fromCallable("parent::base"), + Closure::fromCallable(["parent", "base"]), + ]; + } +} + +$closures = (new EvalFromCallablePersistentInstanceChild())->make(); +$self = $closures[0]; +$static = $closures[1]; +$parentString = $closures[2]; +$parentArray = $closures[3]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "3"; +$out .= call_user_func_array($static, [&$second, 3]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "4"; +$out .= $parentString($third, 3) . ":" . gettype($third) . ":" . $third . "|"; + +$fourth = "5"; +$out .= call_user_func_array($parentArray, [&$fourth, 3]) . ":" . gettype($fourth) . ":" . $fourth; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:15:integer:15|\ +C:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:16:integer:16|\ +B:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:7:integer:7|\ +B:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:8:integer:8" + ); +} + +/// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. +#[test] +fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { + let out = compile_and_run( + r#" 1; + } + + public function reduceValue(string $carry, int $value): string { + return $carry . $value . ":" . get_class($this) . ";"; + } + + public function walkValue(string &$value, int $key) { + $value = $value . $key . ":" . get_class($this); + } + + public function compareDesc(int $left, int $right): int { + return $right - $left; + } + + public function replaceMatch(array $matches): string { + return "R" . $matches[0] . ":" . get_class($this); + } + + public function run(): string { + $out = ""; + $name = "seed"; + $out .= is_callable(["self", "selfStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["self", "selfStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["static", "selfStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["static", "selfStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["parent", "parentStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["parent", "parentStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["self", "selfInstance"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["self", "selfInstance"]) . "|"; + + $name = "seed"; + $out .= is_callable(["parent", "parentInstance"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["parent", "parentInstance"]) . "|"; + + $fromInstance = Closure::fromCallable(["self", "selfInstance"]); + $out .= $fromInstance() . "|"; + + $fromStatic = Closure::fromCallable(["parent", "parentStatic"]); + $out .= $fromStatic(); + + $out .= "|" . implode(",", array_map(["static", "mapStatic"], [3])); + $out .= "|" . implode(",", array_map(["self", "mapInstance"], [4])); + $out .= "|" . implode(",", array_filter([1, 2], ["self", "keepValue"])); + $out .= "|" . array_reduce([1, 2], ["self", "reduceValue"], ""); + + $walk = ["x"]; + array_walk($walk, ["self", "walkValue"]); + $out .= "|" . $walk[0]; + + $sort = [1, 3, 2]; + usort($sort, ["self", "compareDesc"]); + $out .= "|" . implode(",", $sort); + + $out .= "|" . preg_replace_callback("/a/", ["self", "replaceMatch"], "a"); + + try { + $direct = ["self", "selfStatic"]; + $out .= "|" . $direct(); + } catch (Error $e) { + $out .= "|" . get_class($e) . ":" . $e->getMessage(); + } + + return $out; + } +} + +return (new EvalSpecialCallableArrayChild())->run();'); +"#, + ); + + assert_eq!( + out, + "Y:self::selfStatic:S:EvalSpecialCallableArrayChild|\ +Y:static::selfStatic:S:EvalSpecialCallableArrayChild|\ +Y:parent::parentStatic:P:EvalSpecialCallableArrayChild|\ +Y:self::selfInstance:I:EvalSpecialCallableArrayChild|\ +Y:parent::parentInstance:PI:EvalSpecialCallableArrayChild|\ +I:EvalSpecialCallableArrayChild|\ +P:EvalSpecialCallableArrayChild|\ +MS3:EvalSpecialCallableArrayChild|\ +MI4:EvalSpecialCallableArrayChild|\ +2|\ +1:EvalSpecialCallableArrayChild;2:EvalSpecialCallableArrayChild;|\ +x0:EvalSpecialCallableArrayChild|\ +3,2,1|\ +Ra:EvalSpecialCallableArrayChild|\ +Error:Class \"self\" not found" + ); +} + +/// Verifies eval special-class callable arrays preserve by-reference writeback. +#[test] +fn test_eval_special_class_callable_arrays_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run(): string { + $out = ""; + + $first = "2"; + $out .= call_user_func_array(["self", "add"], [&$first, 3]) . ":" . gettype($first) . ":" . $first . "|"; + + $second = "4"; + $static = Closure::fromCallable(["static", "add"]); + $out .= $static($second, 5) . ":" . gettype($second) . ":" . $second . "|"; + + $third = "6"; + $out .= call_user_func_array(["parent", "bump"], [&$third, 7]) . ":" . gettype($third) . ":" . $third . "|"; + + $fourth = "8"; + $parent = Closure::fromCallable(["parent", "bump"]); + $out .= $parent($fourth, 9) . ":" . gettype($fourth) . ":" . $fourth; + + return $out; + } +} + +return (new EvalSpecialArrayRefChild())->run();'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|13:integer:13|17:integer:17" + ); +} + +/// Verifies `Closure::fromCallable()` normalizes eval string and array callables to Closure objects. +#[test] +fn test_eval_closure_from_callable_normalizes_string_and_array_callables() { + let out = compile_and_run( + r#" $cb) { + echo is_object($cb) ? "O" : "o"; + echo get_class($cb); + echo $cb instanceof Closure ? "I" : "i"; + echo is_callable($cb) ? "C" : "c"; + echo $cb($index + 1); + echo "|"; +}'); +"#, + ); + + assert_eq!( + out, + "OClosureICF1|OClosureICM2|OClosureICS3|OClosureICS4|" + ); +} + +/// Verifies eval string callback APIs reject missing functions with PHP TypeErrors. +#[test] +fn test_eval_callable_validation_missing_string_callables_raise_type_errors() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array("OtherMissingEvalCallback", []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable("ThirdMissingEvalCallback"); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + + assert_eq!( + out, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, function \"MiSsInG_Eval_Callback\" not found or invalid function name|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, function \"OtherMissingEvalCallback\" not found or invalid function name|\ +TypeError:Failed to create closure from callable: function \"ThirdMissingEvalCallback\" not found or invalid function name" + ); +} + +/// Verifies `Closure::fromCallable()` rejects invalid object and method targets. +#[test] +fn test_eval_callable_validation_closure_from_callable_rejects_invalid_targets() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + Closure::fromCallable([new EvalFromCallableMissing(), "MiSsInG"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable([new EvalFromCallablePrivate(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable(["EvalFromCallableInstance", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + + assert_eq!( + out, + "TypeError:Failed to create closure from callable: no array or string given|\ +TypeError:Failed to create closure from callable: class EvalFromCallableMissing does not have a method \"MiSsInG\"|\ +TypeError:Failed to create closure from callable: cannot access private method EvalFromCallablePrivate::hidden()|\ +TypeError:Failed to create closure from callable: non-static method EvalFromCallableInstance::inst() cannot be called statically" + ); +} + +/// Verifies `Closure::fromCallable()` preserves by-ref writeback for AOT call targets. +#[test] +fn test_eval_closure_from_callable_preserves_aot_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalFromCallableRefBox(); +$function = Closure::fromCallable("eval_from_callable_ref_add"); +$a = "2"; +echo $function($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$instance = Closure::fromCallable([$box, "bump"]); +$b = "4"; +echo $instance($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$static = Closure::fromCallable(["EvalFromCallableRefBox", "add"]); +$c = "7"; +echo $static($c, 6) . ":" . gettype($c) . ":" . $c . "|"; +$named = Closure::fromCallable("EvalFromCallableRefBox::add"); +$d = "8"; +return call_user_func_array($named, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|19:integer:19|13:integer:13|17:integer:17" + ); +} + +/// Verifies `call_user_func()` invokes `Closure::fromCallable()` targets by value. +#[test] +fn test_eval_closure_from_callable_call_user_func_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$function = Closure::fromCallable("eval_from_callable_call_user_func_ref_add"); +$a = "2"; +echo call_user_func($function, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$box = new EvalFromCallableCallUserFuncRefBox(); +$method = Closure::fromCallable([$box, "bump"]); +$b = "4"; +echo call_user_func($method, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$invoke = Closure::fromCallable($box); +$c = "6"; +echo call_user_func($invoke, $c, 7) . ":" . gettype($c) . ":" . $c . "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallUserFuncRefBox", "add"]); +$d = "8"; +return call_user_func($static, $d, 9) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!(out, "5:string:2|19:string:4|23:string:6|17:string:8"); +} + +/// Verifies `call_user_func_array()` degrades non-reference `Closure::fromCallable()` args by value. +#[test] +fn test_eval_closure_from_callable_call_user_func_array_degrades_non_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$function = Closure::fromCallable("eval_from_callable_call_user_func_array_ref_add"); +$a = "2"; +$aArgs = [$a, 3]; +echo call_user_func_array($function, $aArgs) . ":" . gettype($a) . ":" . $a . ":" . gettype($aArgs[0]) . ":" . $aArgs[0] . "|"; + +$b = "4"; +$bArgs = [&$b, 5]; +echo call_user_func_array($function, $bArgs) . ":" . gettype($b) . ":" . $b . ":" . gettype($bArgs[0]) . ":" . $bArgs[0] . "|"; + +$box = new EvalFromCallableCallUserFuncArrayRefBox(); +$method = Closure::fromCallable([$box, "bump"]); +$c = "6"; +$cArgs = [$c, 7]; +echo call_user_func_array($method, $cArgs) . ":" . gettype($c) . ":" . $c . ":" . gettype($cArgs[0]) . ":" . $cArgs[0] . "|"; + +$d = "8"; +$dArgs = [&$d, 9]; +echo call_user_func_array($method, $dArgs) . ":" . gettype($d) . ":" . $d . ":" . gettype($dArgs[0]) . ":" . $dArgs[0] . "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallUserFuncArrayRefBox", "add"]); +$e = "10"; +$eArgs = [$e, 11]; +return call_user_func_array($static, $eArgs) . ":" . gettype($e) . ":" . $e . ":" . gettype($eArgs[0]) . ":" . $eArgs[0];'); +"#, + ); + + assert_eq!( + out, + "5:string:2:string:2|9:integer:9:integer:9|23:string:6:string:6|27:integer:27:integer:27|21:string:10:string:10" + ); +} + +/// Verifies `Closure::fromCallable()` values can cross eval into AOT callable parameters. +#[test] +fn test_eval_closure_from_callable_values_pass_to_aot_callable_params() { + let out = compile_and_run( + r##"value = $callback("C"); + } + + public function apply(callable $callback) { + return $callback("M"); + } + + public static function applyStatic(callable $callback) { + return $callback("S"); + } +} + +echo eval('$target = new EvalClosureBridgeTarget(); +$cases = [ + Closure::fromCallable("eval_closure_bridge_suffix"), + Closure::fromCallable([EvalClosureBridgeTarget::class, "suffix"]), + Closure::fromCallable("EvalClosureBridgeTarget::suffix"), + Closure::fromCallable([$target, "instanceSuffix"]), + Closure::fromCallable($target), +]; +$out = []; +foreach ($cases as $callback) { + $box = new EvalClosureBridgeBox($callback); + $out[] = $box->value . ":" . $box->apply($callback) . ":" . + EvalClosureBridgeBox::applyStatic($callback); +} +return implode("|", $out);'); +"##, + ); + + assert_eq!( + out, + "C!:M!:S!|C?:M?:S?|C?:M?:S?|C~:M~:S~|C#:M#:S#" + ); +} + +/// Verifies eval dynamic callable descriptors preserve AOT caller by-ref variables. +#[test] +fn test_eval_dynamic_callable_params_write_back_aot_by_ref_args() { + let out = compile_and_run( + r#"value = $callback($value, 3) . ":" . gettype($value) . ":" . $value; + } + + public function apply(callable $callback): string { + $value = 4; + return $callback($value, 5) . ":" . gettype($value) . ":" . $value; + } + + public static function applyStatic(callable $callback): string { + $value = 6; + return $callback($value, 7) . ":" . gettype($value) . ":" . $value; + } +} + +echo eval('$callback = Closure::fromCallable("eval_dynamic_callable_ref_add"); +$box = new EvalDynamicCallableRefBridgeBox($callback); +return $box->value . "|" . $box->apply($callback) . "|" . + EvalDynamicCallableRefBridgeBox::applyStatic($callback);'); +"#, + ); + + assert_eq!(out, "5:integer:5|9:integer:9|13:integer:13"); +} + +/// Verifies eval dynamic callable descriptors preserve AOT mixed by-ref slots. +#[test] +fn test_eval_dynamic_callable_params_write_back_aot_mixed_by_ref_args() { + let out = compile_and_run( + r#"value = $callback($value, "ctor") . ":" . gettype($value) . ":" . $value; + } + + public function apply(callable $callback, mixed $seed): string { + $value = $seed; + return $callback($value, "method") . ":" . gettype($value) . ":" . $value; + } + + public static function applyStatic(callable $callback, mixed $seed): string { + $value = $seed; + return $callback($value, "static") . ":" . gettype($value) . ":" . $value; + } +} + +echo eval('$callback = Closure::fromCallable("eval_dynamic_callable_mixed_replace"); +$box = new EvalDynamicCallableMixedRefBridgeBox($callback, 2); +return $box->value . "|" . $box->apply($callback, 4) . "|" . + EvalDynamicCallableMixedRefBridgeBox::applyStatic($callback, 6);'); +"#, + ); + + assert_eq!( + out, + "string:ctor:string:ctor|string:method:string:method|string:static:string:static" + ); +} + +/// Verifies `Closure::call()` rebinds method closures but passes later args by value. +#[test] +fn test_eval_closure_from_callable_call_rebinds_targets_and_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalFromCallableCallBox(); +$original->base = 10; +$bound = new EvalFromCallableCallBox(); +$bound->base = 20; + +$method = Closure::fromCallable([$original, "bump"]); +$a = "2"; +echo $method->call($bound, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($original); +$b = "4"; +echo $invoke->call($bound, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$function = Closure::fromCallable("eval_from_callable_call_fn"); +echo is_null($function->call($bound)) ? "F" : "f"; echo "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallBox", "add"]); +echo is_null($static->call($bound, 1)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:string:2|29:string:4|F|S"); +} + +/// Verifies `Closure::call()` preserves named argument mapping while using by-value args. +#[test] +fn test_eval_closure_from_callable_call_named_args_use_by_value_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$original = new EvalFromCallableCallNamedBox(); +$original->base = 10; +$bound = new EvalFromCallableCallNamedBox(); +$bound->base = 20; + +$method = Closure::fromCallable([$original, "bump"]); +$a = "2"; +echo $method->call(newThis: $bound, delta: 3, value: $a) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($original); +$b = "4"; +return $invoke->call(newThis: $bound, value: $b, delta: 5) . ":" . gettype($b) . ":" . $b;'); +"#, + ); + + assert_eq!(out, "25:string:2|29:string:4"); +} + +/// Verifies `Closure::bindTo()` persists rebinding for method and invokable callable targets. +#[test] +fn test_eval_closure_bind_from_callable_persists_method_and_invokable_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalFromCallableBindBox(); +$original->base = 10; +$bound = new EvalFromCallableBindBox(); +$bound->base = 20; + +$rawMethod = Closure::fromCallable([$original, "bump"]); +$method = $rawMethod->bindTo($bound); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$rawInvoke = Closure::fromCallable($original); +$invoke = $rawInvoke->bindTo($bound); +$b = "4"; +echo call_user_func_array($invoke, [&$b, 5]) . ":" . gettype($b) . ":" . $b . "|"; + +$static = Closure::fromCallable(["EvalFromCallableBindBox", "add"]); +echo is_null($static->bindTo($bound)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|S"); +} + +/// Verifies static `Closure::bind()` persists rebinding for `fromCallable()` targets. +#[test] +fn test_eval_static_closure_bind_from_callable_persists_method_and_invokable_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalStaticFromCallableBindBox(); +$original->base = 10; +$bound = new EvalStaticFromCallableBindBox(); +$bound->base = 20; + +$rawMethod = Closure::fromCallable([$original, "bump"]); +$method = Closure::bind(closure: $rawMethod, newThis: $bound); +$a = "2"; +echo $method(delta: 3, value: $a) . ":" . gettype($a) . ":" . $a . "|"; + +$rawInvoke = Closure::fromCallable($original); +$invoke = Closure::bind($rawInvoke, $bound); +$b = "4"; +echo call_user_func_array($invoke, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$static = Closure::fromCallable(["EvalStaticFromCallableBindBox", "add"]); +echo is_null(Closure::bind($static, $bound)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|S"); +} + +/// Verifies eval `Closure::bind()` can bind inherited method closures to compatible subclasses. +#[test] +fn test_eval_closure_bind_from_callable_accepts_inherited_eval_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalInheritedBindChild extends EvalInheritedBindBase {} + +class EvalInheritedBindOverrideChild extends EvalInheritedBindBase { + public function bump(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } +} + +$base = new EvalInheritedBindBase(); +$child = new EvalInheritedBindChild(); +$child->base = 20; + +$method = Closure::fromCallable([$base, "bump"])->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($base)->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$override = new EvalInheritedBindOverrideChild(); +echo is_null(Closure::fromCallable([$override, "bump"])->bindTo($base)) ? "M" : "m"; +echo is_null(Closure::fromCallable($override)->bindTo($base)) ? "I" : "i";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|MI"); +} + +/// Verifies eval `Closure::bind()` can bind inherited generated/AOT method closures to subclasses. +#[test] +fn test_eval_closure_bind_from_callable_accepts_inherited_aot_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalAotInheritedBindChild extends EvalAotInheritedBindBase {} + +echo eval('$base = new EvalAotInheritedBindBase(); +$child = new EvalAotInheritedBindChild(); +$child->base = 20; + +$method = Closure::fromCallable([$base, "bump"])->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($base)->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b;'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29"); +} + +/// Verifies first-class eval method closures bind inherited method targets to subclasses. +#[test] +fn test_eval_first_class_closure_bind_accepts_inherited_eval_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalFirstClassInheritedBindChild extends EvalFirstClassInheritedBindBase {} + +class EvalFirstClassInheritedBindOverrideChild extends EvalFirstClassInheritedBindBase { + public function bump(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } +} + +$base = new EvalFirstClassInheritedBindBase(); +$child = new EvalFirstClassInheritedBindChild(); +$child->base = 20; + +$methodSource = $base->bump(...); +$method = $methodSource->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invokeSource = $base(...); +$invoke = $invokeSource->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$callMethod = $base->bump(...); +$c = "6"; +echo is_null($callMethod->call($child, $c, 7)) ? "C:" : "c:"; +echo gettype($c) . ":" . $c . "|"; + +$callInvoke = $base(...); +$d = "8"; +echo is_null($callInvoke->call($child, $d, 9)) ? "I:" : "i:"; +echo gettype($d) . ":" . $d . "|"; + +$override = new EvalFirstClassInheritedBindOverrideChild(); +$overrideMethod = $override->bump(...); +echo is_null($overrideMethod->bindTo($base)) ? "M" : "m"; +$overrideInvoke = $override(...); +echo is_null($overrideInvoke->bindTo($base)) ? "I" : "i";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|C:string:6|I:string:8|MI"); +} + +/// Verifies first-class AOT method closures bind inherited method targets to subclasses. +#[test] +fn test_eval_first_class_closure_bind_accepts_inherited_aot_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalAotFirstClassInheritedBindChild extends EvalAotFirstClassInheritedBindBase {} + +echo eval('$base = new EvalAotFirstClassInheritedBindBase(); +$child = new EvalAotFirstClassInheritedBindChild(); +$child->base = 20; + +$methodSource = $base->bump(...); +$method = $methodSource->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invokeSource = $base(...); +$invoke = $invokeSource->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$callMethod = $base->bump(...); +$c = "6"; +echo is_null($callMethod->call($child, $c, 7)) ? "C:" : "c:"; +echo gettype($c) . ":" . $c . "|"; + +$callInvoke = $base(...); +$d = "8"; +echo is_null($callInvoke->call($child, $d, 9)) ? "I:" : "i:"; +echo gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|C:string:6|I:string:8"); +} + +/// Verifies binding function `fromCallable()` closures returns callable closures. +#[test] +fn test_eval_closure_bind_from_callable_function_targets_remain_callable() { + let out = compile_and_run( + r#"bindTo($box); +echo is_object($aotBoundTo) ? get_class($aotBoundTo) . ":" . $aotBoundTo("x") : "bad"; +echo "|"; + +$aotBound = Closure::bind(closure: $aot, newThis: $box); +echo is_object($aotBound) ? get_class($aotBound) . ":" . $aotBound("y") : "bad"; +echo "|"; + +$eval = Closure::fromCallable("eval_declared_bind_from_callable_function_target"); +$evalBoundTo = $eval->bindTo($box); +echo is_object($evalBoundTo) ? get_class($evalBoundTo) . ":" . $evalBoundTo("u") : "bad"; +echo "|"; + +$evalBound = Closure::bind($eval, $box); +return is_object($evalBound) ? get_class($evalBound) . ":" . $evalBound("v") : "bad";'); +"#, + ); + + assert_eq!(out, "Closure:A:x|Closure:A:y|Closure:E:u|Closure:E:v"); +} + +/// Verifies function `fromCallable()` closures reject explicit scope rebinding. +#[test] +fn test_eval_closure_bind_from_callable_function_targets_reject_explicit_scope() { + let out = compile_and_run( + r#"bindTo($box, null); +echo is_object($aotNullScope) ? $aotNullScope("x") : "bad"; +echo "|"; +$aotStaticScope = Closure::bind($aot, $box, "static"); +echo is_object($aotStaticScope) ? $aotStaticScope("y") : "bad"; +echo "|"; +echo is_null($aot->bindTo($box, "EvalBindFromCallableScopeBox")) ? "a" : "A"; +echo "|"; +echo is_null(Closure::bind($aot, null, "EvalBindFromCallableScopeBox")) ? "b" : "B"; +echo "|"; + +$eval = Closure::fromCallable("eval_declared_bind_from_callable_scope_function_target"); +$evalNullScope = Closure::bind($eval, $box, null); +echo is_object($evalNullScope) ? $evalNullScope("u") : "bad"; +echo "|"; +$evalStaticScope = $eval->bindTo($box, "static"); +echo is_object($evalStaticScope) ? $evalStaticScope("v") : "bad"; +echo "|"; +echo is_null($eval->bindTo($box, "EvalBindFromCallableScopeBox")) ? "e" : "E"; +echo "|"; +return is_null(Closure::bind($eval, null, "EvalBindFromCallableScopeBox")) ? "f" : "F";'); +"#, + ); + + assert_eq!(out, "A:x|A:y|a|b|E:u|E:v|e|f"); +} + +/// Verifies ReflectionFunction reports retained metadata for Closure callables. +#[test] +fn test_eval_reflection_function_reports_from_callable_closure_metadata() { + let out = compile_and_run( + r#"getClosureThis(); + $scope = $ref->getClosureScopeClass(); + $called = $ref->getClosureCalledClass(); + return $label . ":" . + (is_object($this) ? get_class($this) : "null") . ":" . + ($scope ? $scope->getName() : "null") . ":" . + ($called ? $called->getName() : "null") . ":" . + ($ref->isStatic() ? "S" : "s"); + } catch (Throwable $e) { + return $label . ":ERR:" . get_class($e) . ":" . $e->getMessage(); + } +} + +function eval_reflect_closure_meta_eval_function(): string { + return "eval"; +} + +class EvalReflectClosureMetaEvalBox { + public function method(): string { return "method"; } + public static function stat(): string { return "static"; } + public function __invoke(): string { return "invoke"; } +} + +$aot = new EvalReflectClosureMetaAotBox(); +$eval = new EvalReflectClosureMetaEvalBox(); + +$out = []; +$out[] = eval_reflect_closure_meta_dump( + "aot-fn", + Closure::bind(Closure::fromCallable("eval_reflect_closure_meta_aot_function"), $aot) +); +$out[] = eval_reflect_closure_meta_dump( + "eval-fn", + Closure::bind(Closure::fromCallable("eval_reflect_closure_meta_eval_function"), $eval) +); +$out[] = eval_reflect_closure_meta_dump("aot-method", Closure::fromCallable([$aot, "method"])); +$out[] = eval_reflect_closure_meta_dump("eval-method", Closure::fromCallable([$eval, "method"])); +$out[] = eval_reflect_closure_meta_dump("aot-invoke", Closure::fromCallable($aot)); +$out[] = eval_reflect_closure_meta_dump( + "eval-static", + Closure::fromCallable(["EvalReflectClosureMetaEvalBox", "stat"]) +); +return implode("|", $out);'); +"#, + ); + + assert_eq!( + out, + "aot-fn:EvalReflectClosureMetaAotBox:Closure:EvalReflectClosureMetaAotBox:s|\ +eval-fn:EvalReflectClosureMetaEvalBox:Closure:EvalReflectClosureMetaEvalBox:s|\ +aot-method:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:s|\ +eval-method:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:s|\ +aot-invoke:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:s|\ +eval-static:null:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:S" + ); +} diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs new file mode 100644 index 0000000000..9ac288d3c0 --- /dev/null +++ b/tests/codegen/eval_closures.rs @@ -0,0 +1,343 @@ +//! Purpose: +//! End-to-end regressions for closure literals executed inside runtime eval. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_closure` through Rust's test harness. +//! +//! Key details: +//! - Fixtures compile PHP to native code, enter the eval bridge, and execute +//! closure callable paths through elephc-magician. + +use crate::support::{compile_and_run, compile_and_run_capture}; + +/// Verifies eval closure literals dispatch through direct calls and call_user_func_array. +#[test] +fn test_eval_closure_literal_dispatches_direct_and_call_user_func_array() { + let out = compile_and_run( + r#" 6, "left" => 5]);'); +"#, + ); + + assert_eq!(out, "5:11"); +} + +/// Verifies eval closure literals are exposed as PHP `Closure` objects. +#[test] +fn test_eval_closure_literal_is_php_closure_object() { + let out = compile_and_run( + r#"isClosure() ? "C" : "c"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $staticRef->isClosure() ? "C" : "c"; echo ":"; +echo $staticRef->isStatic() ? "S" : "s"; echo ":"; +$vars = $ref->getClosureUsedVariables(); +echo count($vars); echo ":"; +echo $vars["seed"]; echo ":"; +echo $ref->invoke(3); echo ":"; +echo $ref->invokeArgs(["delta" => 5]);'); +"#, + ); + + assert_eq!(out, "C:A:s:C:S:1:4:7:9"); +} + +/// Verifies eval `Closure::call()` binds `$this` and passes later args by value. +#[test] +fn test_eval_closure_call_binds_this_and_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$seed = "2"; +echo $fn->call($box, $seed, 3); +echo ":"; +echo gettype($seed); +echo ":"; +echo $seed;'); +"#, + ); + + assert_eq!(out, "15:string:2"); +} + +/// Verifies eval `call_user_func()` invokes closures with by-value argument semantics. +#[test] +fn test_eval_closure_call_user_func_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$bound = $method->bindTo($box); +$second = "4"; +echo call_user_func($bound, $second, 5); +echo ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!(out, "5:string:2|19:string:4"); +} + +/// Verifies eval `call_user_func_array()` degrades non-reference closure args by value. +#[test] +fn test_eval_closure_call_user_func_array_degrades_non_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$bound = $method->bindTo($box); +$third = "6"; +$thirdArgs = [$third, 7]; +echo call_user_func_array($bound, $thirdArgs); +echo ":" . gettype($third) . ":" . $third; +echo ":" . gettype($thirdArgs[0]) . ":" . $thirdArgs[0] . "|"; + +$fourth = "8"; +$fourthArgs = [&$fourth, 9]; +echo call_user_func_array($bound, $fourthArgs); +echo ":" . gettype($fourth) . ":" . $fourth; +echo ":" . gettype($fourthArgs[0]) . ":" . $fourthArgs[0];'); +"#, + ); + + assert_eq!( + out, + "5:string:2:string:2|9:integer:9:integer:9|23:string:6:string:6|27:integer:27:integer:27" + ); +} + +/// Verifies eval `Closure::bind()` and `bindTo()` persist `$this` across later calls. +#[test] +fn test_eval_closure_bind_persists_this_and_by_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; + +$bound = $fn->bindTo($box); +$seed = "2"; +echo is_object($bound) ? "O:" : "o:"; +echo $bound($seed, 3) . ":" . gettype($seed) . ":" . $seed . "|"; + +$other = "4"; +echo call_user_func_array($bound, [&$other, 5]) . ":" . gettype($other) . ":" . $other . "|"; + +$staticBound = Closure::bind($fn, $box); +$third = "6"; +echo $staticBound($third, 7) . ":" . gettype($third) . ":" . $third . "|"; + +$static = static function() { return "bad"; }; +echo is_null($static->bindTo($box)) ? "N" : "n";'); +"#, + ); + + assert_eq!(out, "O:15:integer:15|19:integer:19|23:integer:23|N"); +} + +/// Verifies eval `Closure::bind()` and `bindTo()` honor explicit private-access scope. +#[test] +fn test_eval_closure_bind_honors_explicit_scope_and_by_ref_args() { + let out = compile_and_run( + r#"secret + $delta; + return $value . ":" . get_called_class(); + }; + } +} + +$fn = (new EvalClosureScopeFactory())->make(); +$child = new EvalClosureScopeChild(); + +$bound = $fn->bindTo($child, "EvalClosureScopeBase"); +$first = "1"; +echo $bound($first, 1) . ":" . gettype($first) . ":" . $first . "|"; + +$staticBound = Closure::bind($fn, $child, "EvalClosureScopeBase"); +$second = "2"; +echo call_user_func_array($staticBound, [&$second, 2]) . ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!( + out, + "42:EvalClosureScopeChild:integer:42|44:EvalClosureScopeChild:integer:44" + ); +} + +/// Verifies eval Closure binding to `null` preserves explicit class scope and by-ref args. +#[test] +fn test_eval_closure_bind_null_receiver_preserves_explicit_scope_and_by_ref_args() { + let out = compile_and_run( + r#"bindTo(null, "EvalClosureNullScopeBox"); +$second = "3"; +echo call_user_func_array($boundTo, [&$second, 4]) . ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!(out, "43:integer:43|47:integer:47"); +} + +/// Verifies eval Closure `__invoke` works as an array callable and preserves by-ref args. +#[test] +fn test_eval_closure_invoke_array_callable_preserves_by_ref_args() { + let out = compile_and_run_capture( + r#"__invoke($fourth, 9) . ":" . gettype($fourth) . ":" . $fourth;'); +"#, + ); + + assert!(out.success, "stdout={} stderr={}", out.stdout, out.stderr); + assert_eq!( + out.stdout, + "C:5:integer:5|9:integer:9|13:integer:13|17:integer:17" + ); +} diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs new file mode 100644 index 0000000000..1d7b056706 --- /dev/null +++ b/tests/codegen/eval_constructors.rs @@ -0,0 +1,460 @@ +//! Purpose: +//! End-to-end regressions for runtime eval object construction through AOT classes. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_constructor` through Rust's test harness. +//! +//! Key details: +//! - Fixtures focus on constructor bridge argument binding and by-reference +//! writeback for non-variable eval caller targets. + +use crate::support::{compile_and_run, compile_and_run_capture}; + +/// Verifies AOT constructor by-reference args write back to eval lvalue targets. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_writes_back_to_lvalue_targets() { + let out = compile_and_run( + r#" "1"]; +new EvalCtorRefTargetBridge($items["x"]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$nested = ["outer" => ["inner" => "2"]]; +new EvalCtorRefTargetBridge($nested["outer"]["inner"]); +echo gettype($nested["outer"]["inner"]) . ":" . $nested["outer"]["inner"] . "|"; + +$box = new EvalCtorRefTargetBox(); +new EvalCtorRefTargetBridge($box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalCtorRefTargetStatic::$value = "4"; +new EvalCtorRefTargetBridge(EvalCtorRefTargetStatic::$value); +return gettype(EvalCtorRefTargetStatic::$value) . ":" . EvalCtorRefTargetStatic::$value;'); +"#, + ); + + assert_eq!(out, "integer:6|integer:7|integer:8|integer:9"); +} + +/// Verifies AOT constructor by-reference args write back through named and unpacked calls. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_named_and_spread_writeback() { + let out = compile_and_run( + r#" "4"]; +new $class(...["value" => &$items["x"], "delta" => 5]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalCtorNamedRefTargetBox(); +new $class(delta: 6, value: $box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalCtorNamedRefTargetStatic::$value = "8"; +new $class(delta: 7, value: EvalCtorNamedRefTargetStatic::$value); +return gettype(EvalCtorNamedRefTargetStatic::$value) . ":" . EvalCtorNamedRefTargetStatic::$value;'); +"#, + ); + + assert_eq!(out, "integer:5|integer:9|integer:10|integer:15"); +} + +/// Verifies ReflectionClass construction uses PHP by-ref semantics for eval and AOT constructors. +#[test] +fn test_eval_reflection_class_constructor_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"newInstance($direct); +echo gettype($direct) . ":" . $direct . "|"; + +$argsValue = "2"; +$aotRef->newInstanceArgs([&$argsValue]); +echo gettype($argsValue) . ":" . $argsValue . "|"; + +$argsCopy = "5"; +$aotRef->newInstanceArgs([$argsCopy]); +echo gettype($argsCopy) . ":" . $argsCopy . "|"; + +class EvalReflectDeclaredCtorRefBridge { + public function __construct(int &$value) { + $value = $value + 7; + } +} + +$evalRef = new ReflectionClass("EvalReflectDeclaredCtorRefBridge"); +$evalDirect = "3"; +$evalRef->newInstance($evalDirect); +echo gettype($evalDirect) . ":" . $evalDirect . "|"; + +$evalArgsValue = "4"; +$evalRef->newInstanceArgs([&$evalArgsValue]); +echo gettype($evalArgsValue) . ":" . $evalArgsValue . "|"; + +$evalArgsCopy = "6"; +$evalRef->newInstanceArgs([$evalArgsCopy]); +return gettype($evalArgsCopy) . ":" . $evalArgsCopy;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "string:1|integer:7|string:5|string:3|integer:11|string:6" + ); + for warning in [ + "EvalReflectAotCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectDeclaredCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", + ] { + let count = out.stderr.matches(warning).count(); + assert!( + count >= 2, + "expected at least two by-ref warnings {warning:?}, saw {count}: {}", + out.stderr + ); + } +} + +/// Verifies AOT constructor by-reference args write back refcounted string, array, and object values. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_refcounted_writeback() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalCtorStringRefBridge { + public function __construct(string &$value) { + $value = $value . "-ctor"; + } +} + +class EvalCtorArrayRefBridge { + public function __construct(array &$items) { + $items[0] = $items[0] . "-head"; + $items[] = "tail"; + } +} + +class EvalCtorObjectRefBridge { + public function __construct(EvalCtorRefcountedPayload &$box) { + $box = new EvalCtorRefcountedPayload($box->name . "-ctor"); + } +} + +echo eval('$text = "A"; +new EvalCtorStringRefBridge($text); +echo $text . "|"; + +$items = ["B"]; +new EvalCtorArrayRefBridge($items); +echo $items[0] . ":" . $items[1] . "|"; + +$box = new EvalCtorRefcountedPayload("C"); +new EvalCtorObjectRefBridge($box); +return $box->name;'); +"#, + ); + + assert_eq!(out, "A-ctor|B-head:tail|C-ctor"); +} + +/// Verifies AOT constructor by-reference variadic args write back caller variables. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_variadic_writeback() { + let out = compile_and_run( + r#"label = $items[0] . ":" . $items[1]; + } +} + +echo eval('$a = "A"; +$b = "B"; +$box = new EvalCtorVariadicRefBridge($a, $b); +echo $box->label . "|"; +return $a . ":" . $b;'); +"#, + ); + + assert_eq!(out, "A-ctor:B-tail|A-ctor:B-tail"); +} + +/// Verifies AOT constructor by-reference writeback happens before a catchable throw. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_lvalue_writeback_before_throw() { + let out = compile_and_run( + r#" "1"]; +try { + new EvalCtorThrowRefTargetBridge($items["x"]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalCtorThrowRefTargetBox(); +try { + new EvalCtorThrowRefTargetBridge($box->value); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +return gettype($box->value) . ":" . $box->value;'); +"#, + ); + + assert_eq!( + out, + "Exception:ctor-lvalue:integer:12|Exception:ctor-lvalue:integer:16" + ); +} + +/// Verifies AOT constructor argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#" &$value, "need" => 123]); +echo "bad";'); +"#, + ), + ]; + + for (label, source) in cases { + let out = compile_and_run_capture(source); + assert!( + !out.success, + "{label}: expected eval runtime fatal, stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "", "{label}: unexpected stdout"); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "{label}: stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); + assert!( + !out.stderr.contains("panicked at") && !out.stderr.contains("thread '"), + "{label}: stderr leaked a Rust panic: {}", + out.stderr + ); + } +} + +/// Verifies eval-declared constructor by-reference args write back to lvalue targets. +#[test] +fn test_eval_declared_constructor_by_ref_writes_back_to_lvalue_targets() { + let out = compile_and_run( + r#" "2"]; +new EvalDeclaredCtorRefTargetBridge($items["x"]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$nested = ["outer" => ["inner" => "3"]]; +new EvalDeclaredCtorRefTargetBridge($nested["outer"]["inner"]); +echo gettype($nested["outer"]["inner"]) . ":" . $nested["outer"]["inner"] . "|"; + +$box = new EvalDeclaredCtorRefTargetBox(); +new EvalDeclaredCtorRefTargetBridge($box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalDeclaredCtorRefTargetStatic::$value = "5"; +new EvalDeclaredCtorRefTargetBridge(EvalDeclaredCtorRefTargetStatic::$value); +return gettype(EvalDeclaredCtorRefTargetStatic::$value) . ":" . EvalDeclaredCtorRefTargetStatic::$value;'); +"#, + ); + + assert_eq!( + out, + "integer:6|integer:7|integer:8|integer:8|integer:10" + ); +} + +/// Verifies eval-declared constructor by-reference writeback happens before catchable throw. +#[test] +fn test_eval_declared_constructor_by_ref_lvalue_writeback_before_throw() { + let out = compile_and_run( + r#" "1"]; +try { + new EvalDeclaredCtorThrowRefTargetBridge($items["x"]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalDeclaredCtorThrowRefTargetBox(); +try { + new EvalDeclaredCtorThrowRefTargetBridge($box->value); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +return gettype($box->value) . ":" . $box->value;'); +"#, + ); + + assert_eq!( + out, + "Exception:eval-ctor-lvalue:integer:12|Exception:eval-ctor-lvalue:integer:16" + ); +} diff --git a/tests/codegen/eval_reflection_invocation.rs b/tests/codegen/eval_reflection_invocation.rs new file mode 100644 index 0000000000..cb263e0272 --- /dev/null +++ b/tests/codegen/eval_reflection_invocation.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! End-to-end regressions for eval ReflectionFunction/ReflectionMethod invocation. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_reflection_invocation` through Rust's test harness. +//! +//! Key details: +//! - Fixtures distinguish PHP's by-value `invoke()` forwarding from by-reference +//! `invokeArgs([&$value])` forwarding for eval-declared and generated/AOT callables. + +use crate::support::compile_and_run_capture; + +/// Verifies ReflectionFunction preserves PHP by-ref semantics for invoke and invokeArgs. +#[test] +fn test_eval_reflection_function_invoke_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"invoke($direct) . ":" . gettype($direct) . ":" . $direct . "|"; + +$argsValue = "2"; +echo $evalRef->invokeArgs([&$argsValue]) . ":" . gettype($argsValue) . ":" . $argsValue . "|"; + +$aotRef = new ReflectionFunction("eval_reflect_aot_invoke_ref_fn"); +$aotDirect = "3"; +echo $aotRef->invoke($aotDirect) . ":" . gettype($aotDirect) . ":" . $aotDirect . "|"; + +$aotArgsValue = "4"; +return $aotRef->invokeArgs([&$aotArgsValue]) . ":" . gettype($aotArgsValue) . ":" . $aotArgsValue;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "4:string:1|5:integer:5|10:string:3|11:integer:11"); + for warning in [ + "eval_reflect_invoke_ref_fn(): Argument #1 ($value) must be passed by reference, value given", + "eval_reflect_aot_invoke_ref_fn(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies ReflectionMethod preserves PHP by-ref semantics for invoke and invokeArgs. +#[test] +fn test_eval_reflection_method_invoke_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"invoke($evalBox, $evalDirect) . ":" . gettype($evalDirect) . ":" . $evalDirect . "|"; + +$evalArgsValue = "2"; +echo $evalMethod->invokeArgs($evalBox, [&$evalArgsValue]) . ":" . gettype($evalArgsValue) . ":" . $evalArgsValue . "|"; + +$evalStatic = new ReflectionMethod("EvalReflectInvokeRefMethodBox", "add"); +$evalStaticDirect = "3"; +echo $evalStatic->invoke(null, $evalStaticDirect) . ":" . gettype($evalStaticDirect) . ":" . $evalStaticDirect . "|"; + +$evalStaticArgsValue = "4"; +echo $evalStatic->invokeArgs(null, [&$evalStaticArgsValue]) . ":" . gettype($evalStaticArgsValue) . ":" . $evalStaticArgsValue . "|"; + +$aotBox = new EvalReflectAotInvokeRefMethodBox(); +$aotMethod = new ReflectionMethod("EvalReflectAotInvokeRefMethodBox", "bump"); +$aotDirect = "5"; +echo $aotMethod->invoke($aotBox, $aotDirect) . ":" . gettype($aotDirect) . ":" . $aotDirect . "|"; + +$aotArgsValue = "6"; +echo $aotMethod->invokeArgs($aotBox, [&$aotArgsValue]) . ":" . gettype($aotArgsValue) . ":" . $aotArgsValue . "|"; + +$aotStatic = new ReflectionMethod("EvalReflectAotInvokeRefMethodBox", "add"); +$aotStaticDirect = "7"; +echo $aotStatic->invoke(null, $aotStaticDirect) . ":" . gettype($aotStaticDirect) . ":" . $aotStaticDirect . "|"; + +$aotStaticArgsValue = "8"; +return $aotStatic->invokeArgs(null, [&$aotStaticArgsValue]) . ":" . gettype($aotStaticArgsValue) . ":" . $aotStaticArgsValue;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "6:string:1|7:integer:7|12:string:3|13:integer:13|16:string:5|17:integer:17|20:string:7|21:integer:21" + ); + for warning in [ + "EvalReflectInvokeRefMethodBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectInvokeRefMethodBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectAotInvokeRefMethodBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectAotInvokeRefMethodBox::add(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 58e17e9f99..714e5fdadc 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -62,6 +62,32 @@ fn test_builtin_exception_message_api() { assert_eq!(out, "boom:boom"); } +/// Verifies `getMessage()` returns a caller-owned string without consuming the +/// builtin Throwable payload used by later reads and `__toString()`. +#[test] +fn test_builtin_exception_get_message_does_not_consume_payload() { + let out = compile_and_run( + "getMessage(); echo \":\"; echo $e->getMessage(); echo \":\"; echo $e->__toString();", + ); + assert_eq!(out, "boom:boom:boom"); +} + +/// Checks that Exception messages built from temporary string results survive the throw. +#[test] +fn test_builtin_exception_message_persists_concatenated_temporary() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "dynamic boom"); +} + /// Verifies builtin throwable catches exception. #[test] fn test_builtin_throwable_catches_exception() { @@ -75,8 +101,7 @@ fn test_builtin_throwable_catches_exception() { #[test] fn test_builtin_throwable_catches_error() { // Throwable (the root interface) catches a builtin Error. - let out = - compile_and_run("tag; #[test] fn test_class_dynamic_instantiation() { // `new $variable()` dispatches known class names through the same AOT - // allocation path as `new ClassName()`. A known name yields an object - // Mixed cell; an unknown name currently preserves the legacy PHP null - // fallback until the missing-class fatal path is tightened. + // allocation path as `new ClassName()`. Known names yield object Mixed cells. let out = compile_and_run( r#"x; assert_eq!(out, "object:12"); } -/// Verifies compiled PHP output for dynamic instantiation missing class skips propinit. +/// Verifies `new ClassName();` is valid as a standalone expression statement and preserves +/// constructor side effects. +#[test] +fn test_new_object_expression_statement_runs_constructor() { + let out = compile_and_run( + r#"x; "#, ); - assert_eq!(out, "NULL|9"); + assert!(err.contains("Fatal error: Uncaught Error: Class \"Nope\" not found"), "{err}"); +} + +/// Verifies a standalone dynamic-new statement uses the same class-not-found fatal. +#[test] +fn test_dynamic_new_expression_statement_missing_class_is_fatal() { + let err = compile_and_run_expect_failure( + r#"reveal(); assert_eq!(out, "ok"); } +/// Verifies native `method_exists()` and `property_exists()` use AOT class metadata. +#[test] +fn test_class_member_exists_builtin_uses_native_metadata() { + let out = compile_and_run( + r#"n = 2; +$b->label = "two"; +echo $a->n . ":" . $a->label . "|" . $b->n . ":" . $b->label; +"#, + ); + assert_eq!(out, "1:one|2:two"); +} + +/// Verifies `__clone()` is invoked after the shallow copy and mutates the clone, not the source. +#[test] +fn test_clone_invokes_magic_clone_on_the_copy() { + let out = compile_and_run( + r#"n = $this->n + 10; + } +} +$a = new Counter(); +$b = clone $a; +echo $a->n . "|" . $b->n; +"#, + ); + assert_eq!(out, "hook;1|11"); +} + +/// Verifies `__clone()` can replace a string property without corrupting the source object. +#[test] +fn test_clone_persists_string_property_before_magic_clone_mutation() { + let out = compile_and_run( + r#"label = $this->label . ":copy"; + } +} +$a = new LabelBox(); +$b = clone $a; +echo $a->label . "|" . $b->label; +"#, + ); + assert_eq!(out, "A|A:copy"); +} + +/// Verifies object-valued properties are shallow-copied, so nested object mutations remain shared. +#[test] +fn test_clone_keeps_nested_objects_shared() { + let out = compile_and_run( + r#"child = new Child(); + } +} +$a = new Boxed(); +$b = clone $a; +$b->child->x = 7; +echo $a->child->x . "|" . $b->child->x; +"#, + ); + assert_eq!(out, "7|7"); +} + +/// Verifies stdClass dynamic properties are copied into a separate hash table during cloning. +#[test] +fn test_clone_copies_stdclass_dynamic_properties_independently() { + let out = compile_and_run( + r#"name = "source"; +$b = clone $a; +$b->name = "copy"; +$b->extra = "new"; +echo $a->name . "|" . $b->name . "|" . (isset($a->extra) ? "Y" : "N"); +"#, + ); + assert_eq!(out, "source|copy|N"); +} diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index 336d31e6e0..cfeb44a4e3 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -185,3 +185,38 @@ echo read_value(); ); assert_eq!(out, "n:n:n"); } + +/// Verifies ReflectionParameter reports constructor-promoted parameters. +#[test] +fn test_reflection_parameter_is_promoted() { + let out = compile_and_run( + r#"getParameters(); +echo $params[0]->isPromoted() ? "I" : "i"; +echo $params[1]->isPromoted() ? "N" : "n"; +$direct = new ReflectionParameter([ReflectPromotedParamUser::class, "__construct"], "id"); +echo $direct->isPromoted() ? "D" : "d"; +$run = new ReflectionParameter([ReflectPromotedParamUser::class, "run"], "id"); +echo $run->isPromoted() ? "R" : "r"; +$inline = new ReflectionParameter([new ReflectPromotedParamUser(1), "run"], 0); +echo ":" . $inline->getName(); +$factory = new ReflectPromotedParamFactory(); +$fromReturn = new ReflectionParameter([$factory->make(), "run"], 0); +echo ":" . $fromReturn->getName(); +"#, + ); + assert_eq!(out, "InDrC:idFC:id"); +} diff --git a/tests/codegen/objects/magic_methods.rs b/tests/codegen/objects/magic_methods.rs index 118883edba..147f2671f4 100644 --- a/tests/codegen/objects/magic_methods.rs +++ b/tests/codegen/objects/magic_methods.rs @@ -119,6 +119,136 @@ echo $m->answer; assert_eq!(out, "answer:99|answer"); } +/// Verifies `__isset` is invoked for undefined property probes and its result is coerced to bool. +#[test] +fn test_magic_isset_handles_missing_property_probes() { + let out = compile_and_run( + r#"enabled) ? "Y" : "N"; +echo ":"; +echo isset($p->disabled) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "check:enabled;Y:check:disabled;N"); +} + +/// Verifies `isset()` does not read an undefined property through `__get`. +#[test] +fn test_magic_isset_missing_property_does_not_call_magic_get() { + let out = compile_and_run( + r#"missing) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "N"); +} + +/// Verifies `__isset` is invoked for inaccessible property probes from outside the class. +#[test] +fn test_magic_isset_handles_inaccessible_property_probes() { + let out = compile_and_run( + r#"token) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "magic:token;Y"); +} + +/// Verifies `__unset` is invoked for undefined property unsets. +#[test] +fn test_magic_unset_handles_missing_property_targets() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "unset:missing;done"); +} + +/// Verifies `unset()` does not read an undefined property through `__get`. +#[test] +fn test_magic_unset_missing_property_does_not_call_magic_get() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "done"); +} + +/// Verifies `__unset` is invoked for inaccessible property unsets from outside the class. +#[test] +fn test_magic_unset_handles_inaccessible_property_targets() { + let out = compile_and_run( + r#"token); +echo "done"; +"#, + ); + assert_eq!(out, "magic:token;done"); +} + +/// Verifies unsetting an undefined property without `__unset` is a no-op. +#[test] +fn test_magic_unset_missing_property_without_magic_is_noop() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "done"); +} + /// Verifies `__invoke` is called when an object is invoked as a function via a variable holding the object. #[test] fn test_magic_invoke_handles_variable_object_call() { @@ -200,6 +330,28 @@ echo User::where("active", 1), "|", User::first(); assert_eq!(out, "where(2)|first(0)"); } +/// Verifies `__callStatic` does not intercept an existing static method. +#[test] +fn test_magic_callstatic_leaves_existing_static_method_direct() { + let out = compile_and_run( + r#"prop)` on an undeclared property dispatches to `__isset` /// and uses its boolean result for both present and absent names. #[test] diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index a3d303b00e..3ed007614b 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -43,3 +43,11 @@ mod abstract_properties; mod property_hooks; #[path = "oop/datetime.rs"] mod datetime; +#[path = "oop/reflection_properties.rs"] +mod reflection_properties; +#[path = "oop/reflection_functions.rs"] +mod reflection_functions; +#[path = "oop/reflection_methods.rs"] +mod reflection_methods; +#[path = "oop/reflection_construction.rs"] +mod reflection_construction; diff --git a/tests/codegen/oop/anonymous_classes.rs b/tests/codegen/oop/anonymous_classes.rs index 715d2bec63..209126fa68 100644 --- a/tests/codegen/oop/anonymous_classes.rs +++ b/tests/codegen/oop/anonymous_classes.rs @@ -119,6 +119,29 @@ fn test_readonly_anonymous_class() { assert_eq!(out, "frozen"); } +/// Verifies `ReflectionClass::isAnonymous()` reports anonymous-class metadata without relying on +/// the parser-global synthetic class counter spelling. +#[test] +fn test_reflection_class_reports_anonymous_class() { + let out = compile_and_run( + "isAnonymous() ? \"A\" : \"a\"; + echo $directRef->isAnonymous() ? \"D\" : \"d\"; + echo $namedRef->isAnonymous() ? \"N\" : \"n\"; + ", + ); + assert_eq!(out, "ADn"); +} + /// Compiles and runs the checked-in `examples/anonymous-classes/main.php` fixture, covering /// interface-implementing anonymous classes (with and without constructor args) and one that /// extends an abstract base. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 2a8c495dbc..4dc363663c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -613,6 +613,137 @@ echo $args[2]; assert_eq!(out, "3/kept,42,also-kept"); } +/// Verifies that float literal attribute args are preserved through class +/// helpers, ReflectionAttribute arguments, and attribute instantiation. +#[test] +fn test_class_attribute_args_preserves_float_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo $attrArgs[0]; +echo ":"; +echo $attrArgs["named"]; + +$instance = $attr->newInstance(); +echo ":"; +echo $instance->ratio; +echo ":"; +echo $instance->named; +"#, + ); + assert_eq!(out, "2:1.5:-2.25:1.5:-2.25:1.5:-2.25"); +} + +/// Verifies positional array literal attribute args survive helper reads and instantiation. +#[test] +fn test_class_attribute_args_preserves_array_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo count($attrArgs[0]); +echo ":"; +echo $attrArgs[0][0]; +echo ":"; +echo $attrArgs[0][1]; +echo ":"; +echo $attrArgs[0][2] ? "T" : "F"; +echo ":"; +echo is_null($attrArgs[0][3]) ? "N" : "bad"; + +$instance = $attr->newInstance(); +echo ":"; +echo count($instance->items); +echo ":"; +echo $instance->items[0]; +echo ":"; +echo $instance->items[1]; +echo ":"; +echo $instance->items[2] ? "T" : "F"; +echo ":"; +echo is_null($instance->items[3]) ? "N" : "bad"; +"#, + ); + assert_eq!(out, "4:one:2:T:N:4:one:2:T:N:4:one:2:T:N"); +} + +/// Verifies that `ClassName::class` attribute args are retained as strings. +#[test] +fn test_class_attribute_args_preserves_class_name_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo $attrArgs[0]; +echo ":"; +echo $attrArgs["named"]; + +$instance = $attr->newInstance(); +echo ":"; +echo $instance->target; +echo ":"; +echo $instance->named; +"#, + ); + assert_eq!( + out, + "2:ClassNameArgTarget:ClassNameArgOther:ClassNameArgTarget:ClassNameArgOther:ClassNameArgTarget:ClassNameArgOther" + ); +} + /// Verifies that boolean (`true`, `false`) and `null` literal arguments are /// preserved by `class_attribute_args()` as strings. Booleans render as /// PHP echo would (true → "1", false → ""), null renders as empty string, @@ -697,6 +828,31 @@ echo $args[0]; assert_eq!(out, "/x"); } +/// Verifies that named PHP attribute arguments are returned under string keys +/// while positional arguments keep integer keys. +#[test] +fn test_class_attribute_args_preserves_named_literal_arguments() { + let out = compile_and_run( + r#"path = $path; + $this->method = $method; + } +} +#[Route(method: "POST", path: "/submit")] +class Controller {} +$attr = (new ReflectionClass(Controller::class))->getAttributes()[0]; +$args = $attr->getArguments(); +echo count($args); +echo ":"; +echo $args["path"]; +echo ":"; +echo $args["method"]; +echo ":"; +$instance = $attr->newInstance(); +echo $instance->path; +echo ":"; +echo $instance->method; +"#, + ); + assert_eq!(out, "2:/submit:POST:/submit:POST"); +} + +/// Verifies that `ReflectionAttribute::getTarget()` and `isRepeated()` report +/// PHP-compatible owner target bits and duplicate-owner metadata. +#[test] +fn test_reflection_attribute_target_and_repetition_metadata() { + let out = compile_and_run( + r#"getAttributes(); +echo $classAttrs[0]->getTarget() . "/" . ($classAttrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $classAttrs[1]->getTarget() . "/" . ($classAttrs[1]->isRepeated() ? "R" : "r") . ":"; +$methodAttr = (new ReflectionMethod(ReflectAttributeTarget::class, "run"))->getAttributes()[0]; +echo $methodAttr->getTarget() . "/" . ($methodAttr->isRepeated() ? "R" : "r") . ":"; +$propertyAttr = (new ReflectionProperty(ReflectAttributeTarget::class, "id"))->getAttributes()[0]; +echo $propertyAttr->getTarget() . "/" . ($propertyAttr->isRepeated() ? "R" : "r") . ":"; +$paramAttr = (new ReflectionMethod(ReflectAttributeTarget::class, "run"))->getParameters()[0]->getAttributes()[0]; +echo $paramAttr->getTarget() . "/" . ($paramAttr->isRepeated() ? "R" : "r") . ":"; +$constAttr = (new ReflectionClassConstant(ReflectAttributeTarget::class, "ANSWER"))->getAttributes()[0]; +echo $constAttr->getTarget() . "/" . ($constAttr->isRepeated() ? "R" : "r") . ":"; +$caseAttr = (new ReflectionEnumUnitCase(ReflectAttributeEnum::class, "Ready"))->getAttributes()[0]; +echo $caseAttr->getTarget() . "/" . ($caseAttr->isRepeated() ? "R" : "r"); +"#, ); + assert_eq!(out, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); } /// Verifies that `class_get_attributes()` returns an empty array for a @@ -1099,193 +1325,2716 @@ fn test_reflection_class_get_name_returns_class_name() { r#"getName(); +echo $ref->getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N"; "#, ); - assert_eq!(out, "Plain"); + assert_eq!(out, "Plain:Plain::N"); } -/// Verifies that `ReflectionClass::getName()` returns the canonical declared -/// name after case-insensitive class-string construction. +/// Verifies that `ReflectionClass` reports final and abstract flags for static metadata. #[test] -fn test_reflection_class_get_name_uses_resolved_class_case() { +fn test_reflection_class_reports_modifier_flags() { let out = compile_and_run( r#"getName(); +abstract class StaticAbstractReflect {} +final class StaticFinalReflect {} +interface StaticIfaceReflect {} +trait StaticTraitReflect {} +enum StaticEnumReflect { case Ready; } +echo (new ReflectionClass(StaticAbstractReflect::class))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isEnum() ? "E" : "e"; +echo ":"; +echo (new ReflectionClass(StaticFinalReflect::class))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticFinalReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticFinalReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticFinalReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticFinalReflect::class))->isEnum() ? "E" : "e"; +echo ":"; +echo (new ReflectionClass(StaticEnumReflect::class))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticEnumReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticEnumReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticEnumReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticEnumReflect::class))->isEnum() ? "E" : "e"; +echo ":"; +$iface = new ReflectionClass("staticifacereflect"); +echo $iface->getName() . "/"; +echo $iface->isAbstract() ? "A" : "a"; +echo $iface->isFinal() ? "F" : "f"; +echo $iface->isInterface() ? "I" : "i"; +echo $iface->isTrait() ? "T" : "t"; +echo $iface->isEnum() ? "E" : "e"; +echo ":"; +$trait = new ReflectionClass("STATICTRAITREFLECT"); +echo $trait->getName() . "/"; +echo $trait->isAbstract() ? "A" : "a"; +echo $trait->isFinal() ? "F" : "f"; +echo $trait->isInterface() ? "I" : "i"; +echo $trait->isTrait() ? "T" : "t"; +echo $trait->isEnum() ? "E" : "e"; "#, ); - assert_eq!(out, "Demo\\Thing"); + assert_eq!( + out, + "Afite:aFite:aFitE:StaticIfaceReflect/afIte:StaticTraitReflect/afiTe" + ); } -/// Verifies that `ReflectionClass::getName()` works for a class discovered -/// through `class_exists()` autoload resolution before the reflector is built. +/// Verifies that `ReflectionClass::getModifiers()` reports PHP modifier bitmasks. #[test] -fn test_reflection_class_get_name_for_autoloaded_class() { - let out = compile_and_run_files( - &[ - ( - "DemoThing.php", - "getName(); -} +fn test_reflection_class_get_modifiers_reports_php_bitmask() { + let out = compile_and_run( + r#"getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierFinal::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierReadonly::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierFinalReadonly::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierEnum::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierIface::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierTrait::class))->getModifiers() . ":"; +echo ReflectionClass::IS_IMPLICIT_ABSTRACT . ":"; +echo ReflectionClass::IS_FINAL . ":"; +echo ReflectionClass::IS_EXPLICIT_ABSTRACT . ":"; +echo ReflectionClass::IS_READONLY; "#, - ), - ], - "main.php", ); - assert_eq!(out, "Demo\\Thing"); + assert_eq!(out, "64:32:65536:65568:32:0:0:16:32:64:65536"); } -/// Verifies that a lowercased `class_exists()` autoload demand, an -/// differently-cased `ReflectionClass` constructor, and attribute lookup all -/// resolve to the same autoloaded class declaration. +/// Verifies that `ReflectionProperty::IS_*` constants use PHP modifier values. #[test] -fn test_autoload_reflection_case_insensitive_class_lookup_reads_attributes() { - let out = compile_and_run_files( - &[ - ( - "DemoThing.php", - "getAttributes(); - echo $r->getName() . "\n"; - echo count($attrs) . "\n"; - echo $attrs[0]->getName() . "\n"; +/// Verifies that property asymmetric visibility contributes PHP modifier bits. +#[test] +fn test_reflection_property_get_modifiers_reports_asymmetric_visibility() { + let out = compile_and_run( + r#"isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers() . ":"; +$protected = new ReflectionProperty(ReflectSetVisibility::class, "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers(); +echo ":"; +echo $protected->isDynamic() ? "D" : "d"; +echo ":"; +$object = new ReflectSetVisibility(); +echo $protected->isLazy($object) ? "L" : "l"; +$protected->skipLazyInitialization($object); +echo ":ok"; "#, - ), - ], - "main.php", ); - assert_eq!(out, "Demo\\Thing\n1\nDemo\\Marker\n"); + assert_eq!(out, "Pt4129:pT2049:d:l:ok"); } -/// Verifies that `ReflectionClass::getAttributes()` works when called on -/// a temporary `ReflectionClass` object directly (without storing the -/// reflector in a variable), and returns the correct attribute name and -/// argument. +/// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. #[test] -fn test_reflection_get_attributes_survives_temporary_reflector() { +fn test_reflection_class_is_readonly_reports_class_metadata() { let out = compile_and_run( r#"getAttributes(); -echo $attrs[0]->getName() . "/"; -echo $attrs[0]->getArguments()[0]; +class StaticReadonlyPlain {} +readonly class StaticReadonlyReflect {} +final readonly class StaticReadonlyFinalReflect {} +enum StaticReadonlyEnumReflect { case Ready; } +interface StaticReadonlyIface {} +trait StaticReadonlyTrait {} +echo (new ReflectionClass(StaticReadonlyPlain::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyFinalReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyEnumReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyIface::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyTrait::class))->isReadOnly() ? "R" : "r"; "#, ); - assert_eq!(out, "Marker/owned"); + assert_eq!(out, "rRRrrr"); } -/// Verifies that `ReflectionMethod::getAttributes()` returns attribute -/// name and arguments for a method decorated with `#[Route("/home", "GET")]`. +/// Verifies that `ReflectionClass::isInstantiable()` reports PHP constructor +/// visibility and class-kind rules for static metadata. #[test] -fn test_reflection_method_get_attributes_returns_method_attributes() { +fn test_reflection_class_is_instantiable() { let out = compile_and_run( r#"getAttributes(); -echo count($attrs) . "/"; -echo $attrs[0]->getName() . "/"; -echo $attrs[0]->getArguments()[0] . "/"; -echo $attrs[0]->getArguments()[1]; +abstract class ReflectInstAbstract {} +class ReflectInstPublic {} +final class ReflectInstFinal {} +class ReflectInstPrivate { private function __construct() {} } +class ReflectInstProtected { protected function __construct() {} } +interface ReflectInstIface {} +trait ReflectInstTrait {} +enum ReflectInstEnum { case Ready; } +echo (new ReflectionClass(ReflectInstAbstract::class))->isInstantiable() ? "A" : "a"; +echo (new ReflectionClass(ReflectInstPublic::class))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass(ReflectInstFinal::class))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass(ReflectInstPrivate::class))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass(ReflectInstProtected::class))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass(ReflectInstIface::class))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass(ReflectInstTrait::class))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass(ReflectInstEnum::class))->isInstantiable() ? "E" : "e"; "#, ); - assert_eq!(out, "1/Route//home/GET"); + assert_eq!(out, "aBCprite"); } -/// Verifies that `ReflectionMethod`'s constructor accepts named arguments -/// (`method_name:`, `class_name:`) and `getAttributes()` returns the -/// correct attribute data. +/// Verifies that `ReflectionClass::isCloneable()` reports PHP clone visibility, +/// class-kind, and elephc runtime-storage rules for static metadata. #[test] -fn test_reflection_method_constructor_supports_named_arguments() { +fn test_reflection_class_is_cloneable() { let out = compile_and_run( r#"isCloneable() ? "A" : "a"; +echo (new ReflectionClass(ReflectClonePlain::class))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass(ReflectCloneFinal::class))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass(ReflectClonePrivate::class))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass(ReflectCloneProtected::class))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass(ReflectClonePublic::class))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass(ReflectCloneIface::class))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass(ReflectCloneTrait::class))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass(ReflectCloneEnum::class))->isCloneable() ? "E" : "e"; +echo (new ReflectionClass(stdClass::class))->isCloneable() ? "S" : "s"; +echo (new ReflectionClass(ReflectionClass::class))->isCloneable() ? "C" : "c"; +"#, + ); + assert_eq!(out, "aPFvrUiteSc"); } -$ref = new ReflectionMethod(method_name: 'index', class_name: 'Controller'); -$attrs = $ref->getAttributes(); -echo count($attrs) . "/"; -echo $attrs[0]->getName() . "/"; -echo $attrs[0]->getArguments()[0]; + +/// Verifies that `ReflectionClass::isIterable()` and its historical alias +/// report PHP Traversable-compatible class metadata. +#[test] +fn test_reflection_class_is_iterable() { + let out = compile_and_run_with_heap_size( + r#"isIterable() ? "P" : "p"; +$iter = new ReflectionClass(ReflectIterableIterator::class); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass(ReflectIterableAggregate::class))->isIterable() ? "G" : "g"; +echo (new ReflectionClass(ReflectIterableAbstract::class))->isIterable() ? "B" : "b"; +echo (new ReflectionClass(ReflectIterableIface::class))->isIterable() ? "F" : "f"; +echo (new ReflectionClass(Iterator::class))->isIterable() ? "T" : "t"; +echo (new ReflectionClass(ArrayIterator::class))->isIterable() ? "R" : "r"; +echo (new ReflectionClass(stdClass::class))->isIterable() ? "S" : "s"; +echo (new ReflectionClass(ReflectIterableEnum::class))->isIterable() ? "E" : "e"; +echo (new ReflectionClass(ReflectIterableTrait::class))->isIterable() ? "H" : "h"; "#, + 67_108_864, ); - assert_eq!(out, "1/Route//home"); + assert_eq!(out, "pIAGbftRseh"); } -/// Verifies that `ReflectionProperty::getAttributes()` works when the class -/// is specified via `User::class` (a class constant) and returns the correct -/// attribute name and argument. +/// Verifies that `ReflectionClass::isInternal()` and `isUserDefined()` report +/// whether class-like metadata came from compiler built-ins or user code. #[test] -fn test_reflection_property_get_attributes_accepts_class_constant() { +fn test_reflection_class_internal_user_defined_predicates() { let out = compile_and_run( r#"getAttributes(); -echo count($attrs) . "/"; -echo $attrs[0]->getName() . "/"; -echo $attrs[0]->getArguments()[0]; +class ReflectOriginClass {} +interface ReflectOriginIface {} +trait ReflectOriginTrait {} +enum ReflectOriginEnum { case Ready; } +$class = new ReflectionClass(ReflectOriginClass::class); +echo $class->isInternal() ? "I" : "i"; echo $class->isUserDefined() ? "U" : "u"; echo ":"; +$iface = new ReflectionClass(ReflectOriginIface::class); +echo $iface->isInternal() ? "I" : "i"; echo $iface->isUserDefined() ? "U" : "u"; echo ":"; +$trait = new ReflectionClass(ReflectOriginTrait::class); +echo $trait->isInternal() ? "I" : "i"; echo $trait->isUserDefined() ? "U" : "u"; echo ":"; +$enum = new ReflectionClass(ReflectOriginEnum::class); +echo $enum->isInternal() ? "I" : "i"; echo $enum->isUserDefined() ? "U" : "u"; echo ":"; +$std = new ReflectionClass(stdClass::class); +echo $std->isInternal() ? "I" : "i"; echo $std->isUserDefined() ? "U" : "u"; echo ":"; +$reflection = new ReflectionClass(ReflectionClass::class); +echo $reflection->isInternal() ? "I" : "i"; echo $reflection->isUserDefined() ? "U" : "u"; echo ":"; +$iterator = new ReflectionClass(Iterator::class); +echo $iterator->isInternal() ? "I" : "i"; echo $iterator->isUserDefined() ? "U" : "u"; echo ":"; "#, ); - assert_eq!(out, "1/Column/id"); + assert_eq!(out, "iU:iU:iU:iU:Iu:Iu:Iu:"); } -/// Verifies that `ReflectionProperty`'s constructor accepts static associative -/// array spread syntax (`...["property_name" => ..., "class_name" => ...]`) for -/// named argument passing. +/// Verifies that `ReflectionClass::hasMethod()`, `hasProperty()`, and +/// `hasConstant()` report PHP-visible members for static class-like metadata. #[test] -fn test_reflection_property_constructor_supports_static_assoc_spread() { +fn test_reflection_class_reports_member_existence() { let out = compile_and_run( r#" "id", "class_name" => "User"]); +interface StaticMemberClassIface { + const CLASS_LIMIT = 10; +} +class StaticMemberChild extends StaticMemberParent implements StaticMemberClassIface { + const CHILD_CONST = 2; + public function ChildMethod() {} + public $childProp; +} +interface StaticMemberIfaceParent { + const PARENT_LIMIT = 10; + public function parentRequirement(); +} +interface StaticMemberIface extends StaticMemberIfaceParent { + const CHILD_LIMIT = 20; + public function childRequirement(); + public string $hook { get; } +} +trait StaticMemberTrait { + const TRAIT_CONST = 30; + private function traitHidden() {} + public $traitProp; +} +enum StaticMemberPureEnum { + case Ready; + const LEVEL = 40; + public function label() { return "ok"; } +} +enum StaticMemberBackedEnum: string { + case Ready = "ready"; +} +$child = new ReflectionClass(StaticMemberChild::class); +echo $child->hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; +echo ":"; +$iface = new ReflectionClass(StaticMemberIface::class); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; +echo ":"; +$trait = new ReflectionClass(StaticMemberTrait::class); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; +echo ":"; +$pure = new ReflectionClass(StaticMemberPureEnum::class); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; +echo ":"; +$backed = new ReflectionClass(StaticMemberBackedEnum::class); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("name") ? "N" : "n"; +echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q"; +"#, + ); + assert_eq!(out, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BNYQ"); +} + +/// Verifies that `ReflectionClass::getConstant()` and `getConstants()` expose +/// class, parent, interface, trait, private, and enum-case constants. +#[test] +fn test_reflection_class_returns_constant_values() { + let out = compile_and_run( + r#"getConstants(); +echo $ref->getConstant("OWN") . ":"; +echo $ref->getConstant("BASE") . ":"; +echo $ref->getConstant("LIMIT") . ":"; +echo $ref->getConstant("SECRET") . ":"; +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("NAME") . ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":" . count($all) . ":" . $all["OWN"] . ":" . $all["BASE"] . ":" . $all["LIMIT"]; +$trait = new ReflectionClass(ReflectConstTrait::class); +$traitAll = $trait->getConstants(); +echo ":" . $trait->getConstant("TRAIT_VALUE") . ":" . count($traitAll) . ":" . $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass(ReflectConstEnum::class); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":" . $case->name; +echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($enumAll); +"#, + ); + assert_eq!( + out, + "own:1:2:9:5:ReflectConstChild:missing:6:own:1:2:3:1:3:Ready:40:40:2" + ); +} + +/// Verifies that `ReflectionClass::getReflectionConstant()` and +/// `getReflectionConstants()` expose constant and enum-case reflector objects. +#[test] +fn test_reflection_class_returns_constant_reflector_objects() { + let out = compile_and_run( + r#"label; } +} +class ReflectConstObjectTarget { + #[ReflectConstMarker("const")] + public const ANSWER = 42; +} +enum ReflectConstObjectEnum { + #[ReflectConstMarker("case")] + case Ready; + public const LEVEL = 7; +} +$ref = new ReflectionClass(ReflectConstObjectTarget::class); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +echo $single->getName() . ":"; +echo count($all) . ":" . $all[0]->getName() . ":"; +$singleAttrs = $all[0]->getAttributes(); +echo $singleAttrs[0]->newInstance()->label() . ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +$enum = new ReflectionClass(ReflectConstObjectEnum::class); +$enumAll = $enum->getReflectionConstants(); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); +$caseAttrs = $enumAll[0]->getAttributes(); +$levelAttrs = $enumAll[1]->getAttributes(); +echo ":" . $caseAttrs[0]->newInstance()->label() . ":"; +echo count($levelAttrs); +"#, + ); + assert_eq!(out, "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0"); +} + +/// Verifies that `ReflectionClass` reports implemented interface and used trait names. +#[test] +fn test_reflection_class_reports_relation_names() { + let out = compile_and_run( + r#"getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces) . ":" . $interfaces[0] . ":"; +echo count($traits) . ":" . $traits[0] . ":"; +$parentInterfaces = (new ReflectionClass(StaticRelationChild::class))->getInterfaceNames(); +echo count($parentInterfaces) . ":" . $parentInterfaces[0] . ":"; +$interfaceObjects = $ref->getInterfaces(); +echo count($interfaceObjects) . ":" . $interfaceObjects["StaticRelationIface"]->getName() . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["StaticRelationTrait"]->getName() . ":"; +$parentInterfaceObjects = (new ReflectionClass(StaticRelationChild::class))->getInterfaces(); +echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["StaticRelationParent"]->getName(); +"#, + ); + assert_eq!( + out, + "1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent:1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent" + ); +} + +/// Verifies that `ReflectionClass::getTraitAliases()` reports direct trait +/// method aliases as PHP's alias-name to `Trait::method` map. +#[test] +fn test_reflection_class_reports_trait_aliases() { + let out = compile_and_run( + r#"getTraitAliases(); +echo count($aliases) . ":" . $aliases["relationAlias"] . ":" . $aliases["hiddenOther"]; +"#, + ); + assert_eq!( + out, + "2:StaticAliasOne::first:StaticAliasTwo::second" + ); +} + +/// Verifies that `ReflectionClass::implementsInterface()` reports class, enum, +/// and interface metadata using case-insensitive interface names. +#[test] +fn test_reflection_class_implements_interface() { + let out = compile_and_run_capture( + r#"implementsInterface(StaticImplChild::class) ? "C" : "c"; +echo (new ReflectionClass(StaticImplTarget::class))->implementsInterface("staticimplbase") ? "B" : "b"; +echo (new ReflectionClass(StaticImplEnum::class))->implementsInterface(StaticImplBase::class) ? "E" : "e"; +echo (new ReflectionClass(StaticImplChild::class))->implementsInterface(StaticImplChild::class) ? "I" : "i"; +echo (new ReflectionClass(StaticImplChild::class))->implementsInterface(StaticImplBase::class) ? "P" : "p"; +echo (new ReflectionClass(StaticImplTrait::class))->implementsInterface(StaticImplBase::class) ? "T" : "t"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBEIPt"); +} + +/// Verifies that `ReflectionClass::implementsInterface()` throws PHP-compatible +/// ReflectionException objects for missing or non-interface argument names. +#[test] +fn test_reflection_class_implements_interface_rejects_non_interfaces() { + let out = compile_and_run_capture( + r#"implementsInterface(StaticImplRejectOther::class) ? "T" : "F"; +try { + $ref->implementsInterface("StaticImplRejectClass"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectTrait"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectEnum"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectMissing"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "F:ReflectionException:StaticImplRejectClass is not an interface:ReflectionException:StaticImplRejectTrait is not an interface:ReflectionException:StaticImplRejectEnum is not an interface:ReflectionException:Interface \"StaticImplRejectMissing\" does not exist" + ); +} + +/// Verifies that `ReflectionClass::isSubclassOf()` reports parent classes and +/// inherited interfaces while excluding self and accepting trait/enum targets as false. +#[test] +fn test_reflection_class_is_subclass_of() { + let out = compile_and_run_capture( + r#"isSubclassOf(StaticSubclassParent::class) ? "P" : "p"; +echo $ref->isSubclassOf("staticsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf(StaticSubclassIface::class) ? "I" : "i"; +echo $ref->isSubclassOf(StaticSubclassChild::class) ? "S" : "s"; +echo (new ReflectionClass(StaticSubclassChildIface::class))->isSubclassOf(StaticSubclassIface::class) ? "J" : "j"; +echo (new ReflectionClass(StaticSubclassIface::class))->isSubclassOf(StaticSubclassIface::class) ? "X" : "x"; +echo $ref->isSubclassOf(StaticSubclassTrait::class) ? "T" : "t"; +echo $ref->isSubclassOf(StaticSubclassEnum::class) ? "Q" : "q"; +echo (new ReflectionClass(StaticSubclassEnum::class))->isSubclassOf(StaticSubclassIface::class) ? "E" : "e"; +try { + $ref->isSubclassOf("StaticSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PBIsJxtqE:missing"); +} + +/// Verifies that `ReflectionClass::isInstance()` reports class, interface, +/// trait, and enum instance relations using runtime object metadata. +#[test] +fn test_reflection_class_is_instance() { + let out = compile_and_run_capture( + r#"isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new StaticInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(StaticInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(StaticInstanceEnum::Ready) ? "N" : "n"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "BcItEN"); +} + +/// Verifies ReflectionObject and object-argument ReflectionClass construction use +/// the runtime object class rather than the declared static return type. +#[test] +fn test_reflection_object_uses_runtime_class_metadata() { + let out = compile_and_run_capture( + r#"getName(); +echo ":" . $objectRef->getParentClass()->getName(); +echo ":" . ($objectRef->hasMethod("childMethod") ? "M" : "m"); +echo ":" . ($objectRef->hasProperty("value") ? "P" : "p"); +$made = $objectRef->newInstance(); +echo ":" . get_class($made); +$classRef = new ReflectionClass($object); +echo ":" . get_class($classRef); +echo ":" . $classRef->getName(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:M:P:StaticReflectObjectChild:ReflectionClass:StaticReflectObjectChild" + ); +} + +/// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass +/// object for subclasses and `false` for parentless classes. +#[test] +fn test_reflection_class_get_parent_class() { + let out = compile_and_run( + r#"getParentClass(); +echo $parent instanceof ReflectionClass ? $parent->getName() : "missing"; +echo ":"; +$root = (new ReflectionClass(StaticParentBase::class))->getParentClass(); +echo $root === false ? "false" : "bad"; +"#, + ); + assert_eq!(out, "StaticParentBase:false"); +} + +/// Verifies that `ReflectionClass::getName()` returns the canonical declared +/// name after case-insensitive class-string construction. +#[test] +fn test_reflection_class_get_name_uses_resolved_class_case() { + let out = compile_and_run( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N"; +"#, + ); + assert_eq!(out, "Demo\\Thing:Thing:Demo:Y"); +} + +/// Verifies that `ReflectionClass::getName()` works for a class discovered +/// through `class_exists()` autoload resolution before the reflector is built. +#[test] +fn test_reflection_class_get_name_for_autoloaded_class() { + let out = compile_and_run_files( + &[ + ("DemoThing.php", "getName(); +} +"#, + ), + ], + "main.php", + ); + assert_eq!(out, "Demo\\Thing"); +} + +/// Verifies that a lowercased `class_exists()` autoload demand, an +/// differently-cased `ReflectionClass` constructor, and attribute lookup all +/// resolve to the same autoloaded class declaration. +#[test] +fn test_autoload_reflection_case_insensitive_class_lookup_reads_attributes() { + let out = compile_and_run_files( + &[ + ( + "DemoThing.php", + "getAttributes(); + echo $r->getName() . "\n"; + echo count($attrs) . "\n"; + echo $attrs[0]->getName() . "\n"; +} +"#, + ), + ], + "main.php", + ); + assert_eq!(out, "Demo\\Thing\n1\nDemo\\Marker\n"); +} + +/// Verifies that `ReflectionClass::getAttributes()` works when called on +/// a temporary `ReflectionClass` object directly (without storing the +/// reflector in a variable), and returns the correct attribute name and +/// argument. +#[test] +fn test_reflection_get_attributes_survives_temporary_reflector() { + let out = compile_and_run( + r#"getAttributes(); +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0]; +"#, + ); + assert_eq!(out, "Marker/owned"); +} + +/// Verifies that `ReflectionMethod::getAttributes()` returns attribute +/// name and arguments for a method decorated with `#[Route("/home", "GET")]`. +#[test] +fn test_reflection_method_get_attributes_returns_method_attributes() { + let out = compile_and_run( + r#"getAttributes(); +echo $ref->getName() . "/"; +echo count($attrs) . "/"; +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0] . "/"; +echo $attrs[0]->getArguments()[1]; +"#, + ); + assert_eq!(out, "index/1/Route//home/GET"); +} + +/// Verifies that `ReflectionMethod`'s constructor accepts named arguments +/// (`method_name:`, `class_name:`) and `getAttributes()` returns the +/// correct attribute data. +#[test] +fn test_reflection_method_constructor_supports_named_arguments() { + let out = compile_and_run( + r#"getAttributes(); +echo $ref->getName() . "/"; echo count($attrs) . "/"; echo $attrs[0]->getName() . "/"; echo $attrs[0]->getArguments()[0]; "#, ); - assert_eq!(out, "1/Column/id"); + assert_eq!(out, "index/1/Route//home"); +} + +/// Verifies that `ReflectionProperty::getAttributes()` works when the class +/// is specified via `User::class` (a class constant) and returns the correct +/// attribute name and argument. +#[test] +fn test_reflection_property_get_attributes_accepts_class_constant() { + let out = compile_and_run( + r#"getAttributes(); +echo $ref->getName() . "/"; +echo count($attrs) . "/"; +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0]; +"#, + ); + assert_eq!(out, "id/1/Column/id"); +} + +/// Verifies that `ReflectionProperty`'s constructor accepts static associative +/// array spread syntax (`...["property_name" => ..., "class_name" => ...]`) for +/// named argument passing. +#[test] +fn test_reflection_property_constructor_supports_static_assoc_spread() { + let out = compile_and_run( + r#" "id", "class_name" => "User"]); +$attrs = $ref->getAttributes(); +echo $ref->getName() . "/"; +echo count($attrs) . "/"; +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0]; +"#, + ); + assert_eq!(out, "id/1/Column/id"); +} + +/// Verifies that `ReflectionMethod` and `ReflectionProperty` expose member +/// visibility, staticity, finality, and abstractness predicates. +#[test] +fn test_reflection_member_predicates_report_method_and_property_flags() { + let out = compile_and_run( + r#"isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod(ReflectMemberBase::class, "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod(ReflectMemberChild::class, "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty(ReflectMemberChild::class, "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; +echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->getModifiers(); +echo ":"; +$visibleProp = new ReflectionProperty(ReflectMemberChild::class, "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->getModifiers(); +echo ":"; +$readonlyProp = new ReflectionProperty(ReflectMemberChild::class, "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->getModifiers(); +echo ":"; +$sealedProp = new ReflectionProperty(ReflectMemberChild::class, "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); +echo ":"; +$staticFinalProp = new ReflectionProperty(ReflectMemberChild::class, "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); +echo ":"; +$abstractProp = new ReflectionProperty(ReflectAbstractProperty::class, "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); +echo ":"; +$classReadonlyProp = new ReflectionProperty(ReflectReadonlyClass::class, "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->getModifiers(); +"#, + ); + assert_eq!( + out, + "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + ); +} + +/// Verifies that `ReflectionProperty::isPromoted()` reports constructor property +/// promotion metadata for direct, inherited, and listed reflected properties. +#[test] +fn test_reflection_property_is_promoted() { + let out = compile_and_run( + r#"isPromoted() ? "I" : "i"; +$name = new ReflectionProperty(ReflectPromotedBase::class, "name"); +echo $name->isPromoted() ? "N" : "n"; +$child = new ReflectionProperty(ReflectPromotedChild::class, "id"); +echo $child->isPromoted() ? "C" : "c"; +$plain = new ReflectionProperty(ReflectPromotedPlain::class, "id"); +echo $plain->isPromoted() ? "P" : "p"; +$static = new ReflectionProperty(ReflectPromotedPlain::class, "count"); +echo $static->isPromoted() ? "S" : "s"; +echo ":"; +foreach ((new ReflectionClass(ReflectPromotedBase::class))->getProperties() as $property) { + if ($property->getName() === "id") { + echo $property->isPromoted() ? "L" : "l"; + } + if ($property->getName() === "name") { + echo $property->isPromoted() ? "M" : "m"; + } +} +"#, + ); + assert_eq!(out, "INCps:LM"); +} + +/// Verifies that `ReflectionMethod::isConstructor()` and `isDestructor()` derive +/// their result from the reflected method name. +#[test] +fn test_reflection_method_reports_constructor_and_destructor() { + let out = compile_and_run( + r#"isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod(ReflectLifecycle::class, "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod(ReflectLifecycle::class, "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass(ReflectLifecycle::class))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d"; +"#, + ); + assert_eq!(out, "Cd:cD:cd:Cd"); +} + +/// Verifies member and enum-case reflectors expose their declaring class object. +#[test] +fn test_reflection_members_report_declaring_class() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getMethod("own")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionProperty(ReflectDeclaringChild::class, "baseProp"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getProperty("childProp")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClassConstant(ReflectDeclaringChild::class, "BASE_CONST"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringEnum::class))->getReflectionConstant("Ready")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionEnumBackedCase(ReflectDeclaringEnum::class, "Ready"))->getDeclaringClass()->getName(); +"#, + ); + assert_eq!( + out, + "ReflectDeclaringBase:ReflectDeclaringChild:ReflectDeclaringBase:ReflectDeclaringChild:ReflectDeclaringBase:ReflectDeclaringBase:ReflectDeclaringEnum:ReflectDeclaringEnum" + ); +} + +/// Verifies that `ReflectionClass::getMethods()` and `getProperties()` return +/// populated ReflectionMethod/ReflectionProperty objects with member metadata. +#[test] +fn test_reflection_class_get_methods_and_properties_return_member_objects() { + let out = compile_and_run_capture( + r#"getMethods(); +$properties = $ref->getProperties(); +echo count($methods) . ":" . count($properties) . ":"; +echo ReflectionMethod::IS_STATIC . ":" . ReflectionMethod::IS_PRIVATE . ":"; +$direct = new ReflectionMethod(ReflectListTarget::class, "helper"); +echo "D" . $direct->getModifiers() . ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F" . count($method->getAttributes()); + echo "M" . $method->getModifiers(); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + echo "M" . $method->getModifiers(); + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V" . count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + echo "M" . $property->getModifiers(); + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + echo "M" . $property->getModifiers(); + } +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); +} + +/// Verifies that `ReflectionClass::getMethod()` and `getProperty()` return +/// single member objects and throw ReflectionException for missing members. +#[test] +fn test_reflection_class_get_method_and_property_lookup_members() { + let out = compile_and_run_capture( + r#"getMethod("FIRST"); +echo $method->getName() . ":"; +echo $method->isPublic() ? "U" : "u"; +echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; +echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName() . ":"; +echo $property->isProtected() ? "R" : "r"; +echo ":"; +try { + $ref->getProperty("Visible"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo ":"; +try { + $ref->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "first:U:PS:visible:R:Property ReflectLookupTarget::$Visible does not exist:Method ReflectLookupTarget::missing() does not exist" + ); +} + +/// Verifies that `ReflectionMethod::getParameters()` returns populated +/// ReflectionParameter objects for typed, by-reference, defaulted, and variadic parameters. +#[test] +fn test_reflection_method_get_parameters_returns_parameter_metadata() { + let out = compile_and_run_capture( + r##"getNumberOfParameters() . "/"; +echo $method->getNumberOfRequiredParameters() . ":"; +$params = $method->getParameters(); +foreach ($params as $param) { + echo $param->getName() . "#" . $param->getPosition(); + echo ($param->hasType() ? "T" : "t"); + echo ($param->isOptional() ? "O" : "R"); + echo ($param->isPassedByReference() ? "B" : "b"); + echo ($param->canBePassedByValue() ? "P" : "p"); + echo ($param->isVariadic() ? "V" : "v"); + echo "|"; +} +echo "\n"; +$iface = new ReflectionMethod(ReflectParamInterface::class, "iface"); +echo $iface->getNumberOfParameters() . "/"; +echo $iface->getNumberOfRequiredParameters() . ":"; +$ifaceParams = $iface->getParameters(); +echo $ifaceParams[0]->getName() . ($ifaceParams[0]->hasType() ? "T" : "t"); +echo ":" . $ifaceParams[1]->getName() . ($ifaceParams[1]->isOptional() ? "O" : "R"); +echo "\n"; +$trait = new ReflectionMethod(ReflectParamTrait::class, "traitRun"); +echo $trait->getNumberOfParameters() . "/"; +echo $trait->getNumberOfRequiredParameters() . ":"; +echo ($trait->isStatic() ? "S" : "s"); +echo ($trait->isPrivate() ? "R" : "r"); +$traitParams = $trait->getParameters(); +echo ":" . $traitParams[2]->getName() . ($traitParams[2]->isVariadic() ? "V" : "v"); +echo ($traitParams[2]->hasType() ? "T" : "t"); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "4/2:id#0TRbPv|name#1tRBpv|mode#2TObPv|rest#3tObPV|\n2/1:idT:nameO\n3/1:SR:restVT" + ); +} + +/// Verifies `ReflectionParameter::getDeclaringClass()` reports method owners and null for functions. +#[test] +fn test_reflection_parameter_get_declaring_class_reports_method_owner() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName() . ":"; +$listed = (new ReflectionMethod(ReflectDeclaringParamChild::class, "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName() . ":"; +echo $listed->getDeclaringFunction()->getName() . ":"; +$function = new ReflectionParameter("reflect_declaring_function", "value"); +echo $function->getDeclaringFunction()->getName() . ":"; +echo is_null($function->getDeclaringClass()) ? "null" : "bad"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectDeclaringParamBase:inherited:ReflectDeclaringParamBase:ReflectDeclaringParamChild:own:reflect_declaring_function:null" + ); +} + +/// Verifies that `ReflectionParameter::getType()` returns named, union, and intersection metadata. +#[test] +fn test_reflection_parameter_get_type_returns_named_type_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->hasType() ? "T:" : "t:"; + echo $param->allowsNull() ? "N:" : "n:"; + $type = $param->getType(); + if ($type instanceof ReflectionNamedType) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } elseif ($type instanceof ReflectionUnionType) { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type instanceof ReflectionIntersectionType) { + echo "intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionParameter([ReflectParamTypeTarget::class, "run"], "dep"); +$directType = $direct->getType(); +if ($directType instanceof ReflectionNamedType) { + echo "direct:" . $directType->getName(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:n:int!B|name:T:N:string?B|dep:T:n:ReflectParamTypeDep!C|plain:t:N:null|union:T:n:union!:intB:stringB|nullableUnion:T:N:union?:intB:stringB|intersection:T:n:intersection!:ReflectParamTypeAC:ReflectParamTypeBC|direct:ReflectParamTypeDep" + ); +} + +/// Verifies `ReflectionType::__toString()` formats retained type metadata. +#[test] +fn test_reflection_type_to_string_formats_retained_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName() . ":"; + echo $type->__toString() . "|"; +} +$unionType = (new ReflectionParameter([ReflectTypeStringTarget::class, "run"], "union"))->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:?ReflectTypeStringDep|union:int|string|null|both:ReflectTypeStringLeft&ReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" + ); +} + +/// Verifies that `ReflectionParameter::getClass()` exposes legacy object-type metadata. +#[test] +fn test_reflection_parameter_get_class_returns_named_object_type() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $class = $param->getClass(); + echo $param->getName() . ":" . ($class ? $class->getName() : "null") . "|"; +} +$direct = new ReflectionParameter([ReflectParamClassTarget::class, "run"], "nullable"); +echo "direct:" . $direct->getClass()->getName() . "|"; +$functionParam = new ReflectionParameter("reflect_param_class_function", "dep"); +echo "function:" . $functionParam->getClass()->getName(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:ReflectParamClassDep|nullable:ReflectParamClassDep|id:null|unionObject:null|intersection:null|plain:null|direct:ReflectParamClassDep|function:ReflectParamClassDep" + ); +} + +/// Verifies that `ReflectionProperty::getType()` and `getSettableType()` return type metadata. +#[test] +fn test_reflection_property_get_type_returns_type_metadata() { + let out = compile_and_run_capture( + r#"getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($type instanceof ReflectionNamedType) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } elseif ($type instanceof ReflectionUnionType) { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty(ReflectPropertyTypeTarget::class, "dep"); +$directType = $direct->getType(); +if ($directType instanceof ReflectionNamedType) { + echo "direct:" . $directType->getName(); +} +$directSettableType = $direct->getSettableType(); +if ($directSettableType instanceof ReflectionNamedType) { + echo ":set:" . $directSettableType->getName(); +} +$plain = new ReflectionProperty(ReflectPropertyTypeTarget::class, "plain"); +echo ":plainSet:" . ($plain->getSettableType() === null ? "N" : "n"); +$directUnion = new ReflectionProperty(ReflectPropertyTypeTarget::class, "union"); +$directUnionSettableType = $directUnion->getSettableType(); +if ($directUnionSettableType instanceof ReflectionUnionType) { + echo ":unionSet:" . count($directUnionSettableType->getTypes()); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int!B|name:T:string?B|dep:T:ReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:ReflectPropertyTypeDep:set:ReflectPropertyTypeDep:plainSet:N:unionSet:2" + ); +} + +/// Verifies that `ReflectionProperty` exposes supported property defaults. +#[test] +fn test_reflection_property_get_default_value_returns_property_metadata() { + let out = compile_and_run_capture( + r#" "Ada", "1" => "one", false => "zero"]; + } + $obj = new ReflectPropertyDefaultTarget(); + echo "runtime:" . $obj->assoc["name"] . ":" . $obj->assoc[1] . "|"; + $implicit = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "implicit"); + echo $implicit->getName() . ":"; +echo $implicit->isDefault() ? "Y:" : "N:"; +echo $implicit->hasDefaultValue() ? "D:" : "d:"; +echo $implicit->getDefaultValue() === null ? "null" : $implicit->getDefaultValue(); +echo "|"; +$typed = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "typed"); +echo $typed->getName() . ":"; +echo $typed->isDefault() ? "Y:" : "N:"; +echo $typed->hasDefaultValue() ? "D:" : "d:"; +echo $typed->getDefaultValue() === null ? "null" : $typed->getDefaultValue(); +echo "|"; +$nullableTyped = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "nullableTyped"); +echo $nullableTyped->getName() . ":"; +echo $nullableTyped->isDefault() ? "Y:" : "N:"; +echo $nullableTyped->hasDefaultValue() ? "D:" : "d:"; +echo $nullableTyped->getDefaultValue() === null ? "null" : $nullableTyped->getDefaultValue(); +echo "|"; +$explicitNull = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "explicitNull"); +echo $explicitNull->getName() . ":"; +echo $explicitNull->isDefault() ? "Y:" : "N:"; +echo $explicitNull->hasDefaultValue() ? "D:" : "d:"; +echo $explicitNull->getDefaultValue() === null ? "null" : $explicitNull->getDefaultValue(); +echo "|"; +$count = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "count"); +echo $count->getName() . ":"; +echo $count->isDefault() ? "Y:" : "N:"; +echo $count->hasDefaultValue() ? "D:" : "d:"; +echo $count->getDefaultValue() === null ? "null" : $count->getDefaultValue(); +echo "|"; +$label = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "label"); +echo $label->getName() . ":"; +echo $label->isDefault() ? "Y:" : "N:"; +echo $label->hasDefaultValue() ? "D:" : "d:"; +echo $label->getDefaultValue() === null ? "null" : $label->getDefaultValue(); +echo "|"; +$items = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "items"); +$itemDefault = $items->getDefaultValue(); +echo $items->getName() . ":"; +echo $items->isDefault() ? "Y:" : "N:"; +echo $items->hasDefaultValue() ? "D:" : "d:"; +echo count($itemDefault) . ":" . $itemDefault[0] . ":" . $itemDefault[1] . ":"; +echo $itemDefault[2] === null ? "null" : "value"; +echo "|"; +$assoc = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "assoc"); +$assocDefault = $assoc->getDefaultValue(); +echo $assoc->getName() . ":"; +echo $assoc->isDefault() ? "Y:" : "N:"; +echo $assoc->hasDefaultValue() ? "D:" : "d:"; +echo count($assocDefault) . ":" . $assocDefault["name"] . ":" . $assocDefault[1] . ":"; +echo $assocDefault[0]; +echo "|"; +$listed = (new ReflectionClass(ReflectPropertyDefaultTarget::class))->getProperty("implicit"); +echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "runtime:Ada:one|implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|items:Y:D:3:2:b:null|assoc:Y:D:3:Ada:one:zero|listed:Y:D:null" + ); +} + +/// Verifies that `ReflectionClass::getDefaultProperties()` exposes supported property defaults. +#[test] +fn test_reflection_class_get_default_properties_returns_property_metadata() { + let out = compile_and_run_capture( + r#" "S2", "2" => "two"]; + public $explicitNull = null; + public array $items = [8, "i"]; + public $assoc = ["side" => "S", "4" => "four"]; +} +$defaults = (new ReflectionClass(ReflectClassDefaultChild::class))->getDefaultProperties(); +echo $defaults["childStatic"] . ":"; +echo $defaults["baseStatic"] . ":"; +echo $defaults["child"] . ":"; +echo $defaults["shadow"] . ":"; + echo $defaults["base"] . ":"; + echo $defaults["prot"] . ":"; + echo ReflectClassDefaultChild::$assocStatic["s"] . ":"; + echo count($defaults["items"]) . ":"; +echo $defaults["items"][0] . ":"; +echo $defaults["items"][1] . ":"; +echo count($defaults["assoc"]) . ":"; +echo $defaults["assoc"]["side"] . ":"; +echo $defaults["assoc"][4] . ":"; +$implicit = "i"; +$explicitNull = "e"; +$typed = "t"; +foreach ($defaults as $key => $value) { + if ($key === "implicit" && $value === null) { + $implicit = "I"; + } + if ($key === "explicitNull" && $value === null) { + $explicitNull = "E"; + } + if ($key === "typed") { + $typed = "T"; + } +} +echo $implicit . ":" . $explicitNull . ":" . $typed . ":" . count($defaults); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:bs:5:9:1:p:S2:2:8:i:2:S:four:I:E:t:11"); +} + +/// Verifies `ReflectionClass::getStaticPropertyValue()` and +/// `setStaticPropertyValue()` use live static-property storage for known classes. +#[test] +fn test_reflection_class_static_property_value_accesses_live_storage() { + let out = compile_and_run_capture( + r#"getStaticPropertyValue("count"); +ReflectStaticValueTarget::$count = 9; +echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("count"); +(new ReflectionClass(ReflectStaticValueTarget::class))->setStaticPropertyValue("count", 11); +echo ":" . ReflectStaticValueTarget::$count; +echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("missing", "fallback"); +try { + (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("missing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "7:9:11:fallback:ReflectionException:Property ReflectStaticValueTarget::$missing does not exist" + ); +} + +/// Verifies a local `ReflectionClass` receiver keeps statically-known class metadata. +#[test] +fn test_reflection_class_tracked_local_receiver_uses_static_metadata() { + let out = compile_and_run_capture( + r#"getStaticPropertyValue("count"); +ReflectTrackedClassTarget::$count = 9; +echo ":" . $ref->getStaticPropertyValue("count"); +$ref->setStaticPropertyValue("count", 11); +echo ":" . ReflectTrackedClassTarget::$count; +echo ":" . $ref->getStaticPropertyValue("missing", "fallback"); +$obj = $ref->newInstance(id: 5, name: "Ada"); +echo ":" . $obj->id . ":" . $obj->name; +$obj2 = $ref->newInstanceArgs(["name" => "Bob", "id" => 6]); +echo ":" . $obj2->id . ":" . $obj2->name; +try { + $ref->getStaticPropertyValue("missing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "7:9:11:fallback:5:Ada:6:Bob:ReflectionException:Property ReflectTrackedClassTarget::$missing does not exist" + ); +} + +/// Verifies `ReflectionClass::getStaticProperties()` reads current AOT static values. +#[test] +fn test_reflection_class_get_static_properties_reads_live_storage() { + let out = compile_and_run_capture( + r#"getStaticProperties(); +echo $props["base"] . ":" . $props["count"] . ":" . $props["label"]; +$inline = (new ReflectionClass(ReflectStaticPropertiesChild::class))->getStaticProperties(); +echo ":" . $inline["count"]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B2:4:new:4"); +} + +/// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. +#[test] +fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $attrs = $param->getAttributes(); + echo $param->getName() . ":" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } + echo "|"; +} +$direct = new ReflectionParameter([ReflectParamAttrTarget::class, "run"], "name"); +$directAttrs = $direct->getAttributes(); +echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . $directAttrs[0]->getArguments()[0]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:1:ReflectParamTag:id|name:1:ReflectParamTag:name|plain:0|direct:1:ReflectParamTag:name" + ); +} + +/// Verifies `ReflectionFunction` and direct `ReflectionParameter` expose attributes on +/// top-level function parameters. +#[test] +fn test_reflection_function_parameter_get_attributes_returns_parameter_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $attrs = $param->getAttributes(); + echo $param->getName() . ":" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } + echo "|"; +} +$direct = new ReflectionParameter("reflect_function_param_attrs", "name"); +$directAttrs = $direct->getAttributes(); +echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . $directAttrs[0]->getArguments()[0]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:1:ReflectFunctionParamTag:id|name:1:ReflectFunctionParamTag:name|plain:0|direct:1:ReflectFunctionParamTag:name" + ); +} + +/// Verifies that `ReflectionParameter` exposes supported scalar/null/array defaults. +#[test] +fn test_reflection_parameter_exposes_default_values() { + let out = compile_and_run_capture( + r##" "Ada", "1" => "one", false => "zero", 3 => ["deep" => 4]]) {} +$params = (new ReflectionFunction("reflect_default_function"))->getParameters(); +echo $params[0]->isDefaultValueAvailable() ? "D" : "d"; +try { + $params[0]->getDefaultValue(); +} catch (ReflectionException $e) { + echo ":E"; +} +echo "|"; +echo $params[1]->isDefaultValueAvailable() ? "D:" : "d:"; +echo $params[1]->getDefaultValue(); +echo "|"; +echo $params[2]->isDefaultValueAvailable() ? "D:" : "d:"; +echo $params[2]->getDefaultValue() === null ? "null" : "value"; +echo "|"; +$direct = new ReflectionParameter("reflect_default_function", "label"); +echo $direct->isDefaultValueAvailable() ? "D:" : "d:"; +echo $direct->getDefaultValue(); +echo "|"; +$items = $params[4]->getDefaultValue(); +echo $params[4]->isDefaultValueAvailable() ? "D:" : "d:"; +echo count($items) . ":" . $items[0] . ":" . $items[1] . ":"; +echo $items[2] === null ? "null" : "value"; +echo ":" . count($items[3]) . ":" . $items[3][0] . ":" . ($items[3][1] ? "T" : "F"); +echo "|"; +$directItems = (new ReflectionParameter("reflect_default_function", "items"))->getDefaultValue(); +echo count($directItems) . ":" . $directItems[1]; +echo "|"; +$assoc = $params[5]->getDefaultValue(); +echo $params[5]->isDefaultValueAvailable() ? "D:" : "d:"; +echo count($assoc) . ":" . $assoc["name"] . ":" . $assoc[1] . ":"; +echo $assoc[0] . ":" . $assoc[3]["deep"]; +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "d:E|D:7|D:null|D:ok|D:4:1:two:null:2:3:F|4:two|D:4:Ada:one:zero:4" + ); +} + +/// Verifies `ReflectionParameter::getDefaultValue()` materializes object defaults lazily. +#[test] +fn test_reflection_parameter_exposes_object_default_values() { + let out = compile_and_run_capture( + r##"label = $label; + $this->extra = $extra; + } +} +class ReflectObjectDefaultValueArgs { + const ARG_LABEL = "arg"; + const ARG_EXTRA = 42; + const METHOD_LABEL = "method-arg"; + const METHOD_EXTRA = "M"; + public mixed $label; + public mixed $extra; + public function __construct(mixed $label, mixed $extra) { + $this->label = $label; + $this->extra = $extra; + } +} +function reflect_object_default(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} +function reflect_object_default_args(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs(ReflectObjectDefaultValueArgs::ARG_LABEL, ReflectObjectDefaultValueArgs::ARG_EXTRA)) {} +class ReflectObjectDefaultMethod { + public function run(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} + public function withArgs(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs(ReflectObjectDefaultValueArgs::METHOD_LABEL, ReflectObjectDefaultValueArgs::METHOD_EXTRA)) {} +} +$param = (new ReflectionFunction("reflect_object_default"))->getParameters()[0]; +echo $param->isDefaultValueAvailable() ? "D:" : "d:"; +$first = $param->getDefaultValue(); +$second = $param->getDefaultValue(); +if ($first instanceof ReflectObjectDefaultValue) { + echo "object:" . $first->label . ":" . $first->extra . ":"; +} else { + echo "not-object:"; +} +echo $first === $second ? "same" : "diff"; +$direct = (new ReflectionParameter("reflect_object_default", "value"))->getDefaultValue(); +echo $direct instanceof ReflectObjectDefaultValue ? ":direct:" . $direct->label : ":direct:bad"; +$method = (new ReflectionMethod(ReflectObjectDefaultMethod::class, "run"))->getParameters()[0]->getDefaultValue(); +echo $method instanceof ReflectObjectDefaultValue ? ":method:" . $method->label : ":method:bad"; +$directMethod = (new ReflectionParameter([ReflectObjectDefaultMethod::class, "run"], "value"))->getDefaultValue(); +echo $directMethod instanceof ReflectObjectDefaultValue ? ":direct-method:" . $directMethod->label : ":direct-method:bad"; +$args = (new ReflectionFunction("reflect_object_default_args"))->getParameters()[0]->getDefaultValue(); +echo $args instanceof ReflectObjectDefaultValueArgs ? ":args:" . $args->label . ":" . $args->extra : ":args:bad"; +$methodArgs = (new ReflectionMethod(ReflectObjectDefaultMethod::class, "withArgs"))->getParameters()[0]->getDefaultValue(); +echo $methodArgs instanceof ReflectObjectDefaultValueArgs ? ":method-args:" . $methodArgs->label . ":" . $methodArgs->extra : ":method-args:bad"; +$directMethodArgs = (new ReflectionParameter([ReflectObjectDefaultMethod::class, "withArgs"], "value"))->getDefaultValue(); +echo $directMethodArgs instanceof ReflectObjectDefaultValueArgs ? ":direct-method-args:" . $directMethodArgs->label . ":" . $directMethodArgs->extra : ":direct-method-args:bad"; +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "D:object:ctor:default-extra:diff:direct:ctor:method:ctor:direct-method:ctor:args:arg:42:method-args:method-arg:M:direct-method-args:method-arg:M" + ); +} + +/// Verifies `ReflectionParameter` exposes class-constant default metadata. +#[test] +fn test_reflection_parameter_exposes_default_constant_metadata() { + let out = compile_and_run_capture( + r##"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + echo "|"; +} +$direct = new ReflectionParameter([ReflectDefaultConstTarget::class, "run"], "parent"); +echo "direct:"; +echo $direct->isDefaultValueConstant() ? "C:" : "c:"; +echo $direct->getDefaultValueConstantName(); +echo ":"; +echo $direct->getDefaultValue(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|class:D:c:null:ReflectDefaultConstTarget|literal:D:c:null:7|direct:C:parent::BASE:B" + ); +} + +/// Verifies direct `new ReflectionParameter()` construction for statically known +/// class and interface method targets. +#[test] +fn test_reflection_parameter_constructor_reflects_method_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#" . $byName->getPosition(); +echo ($byName->hasType() ? "T" : "t"); +echo ($byName->isOptional() ? "O" : "R"); +echo ($byName->isPassedByReference() ? "B" : "b"); +echo ($byName->isVariadic() ? "V" : "v"); +echo "|"; +$byPosition = new ReflectionParameter(["reflectdirectparamtarget", "run"], 3); +echo $byPosition->getName() . "#" . $byPosition->getPosition(); +echo ($byPosition->hasType() ? "T" : "t"); +echo ($byPosition->isOptional() ? "O" : "R"); +echo ($byPosition->isPassedByReference() ? "B" : "b"); +echo ($byPosition->isVariadic() ? "V" : "v"); +echo "|"; +$object = new ReflectDirectParamTarget(); +$byObject = new ReflectionParameter([$object, "run"], "mode"); +echo $byObject->getName() . "#" . $byObject->getPosition(); +echo ($byObject->hasType() ? "T" : "t"); +echo ($byObject->isOptional() ? "O" : "R"); +echo ($byObject->isPassedByReference() ? "B" : "b"); +echo ($byObject->isVariadic() ? "V" : "v"); +echo "|"; +$iface = new ReflectionParameter([ReflectDirectParamInterface::class, "build"], 1); +echo $iface->getName() . "#" . $iface->getPosition(); +echo ($iface->isOptional() ? "O" : "R"); +echo "|"; +$trait = new ReflectionParameter([ReflectDirectParamTrait::class, "traitRun"], "rest"); +echo $trait->getName() . "#" . $trait->getPosition(); +echo ($trait->hasType() ? "T" : "t"); +echo ($trait->isOptional() ? "O" : "R"); +echo ($trait->isVariadic() ? "V" : "v"); +echo "|"; +$named = new ReflectionParameter(param: "id", function: [ReflectDirectParamTarget::class, "run"]); +echo $named->getName() . "#" . $named->getPosition(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "name#1tRBv|rest#3tObV|mode#2TObv|name#1O|rest#2TOV|id#0" + ); +} + +/// Verifies direct `new ReflectionParameter()` construction for statically known +/// user function targets. +#[test] +fn test_reflection_parameter_constructor_reflects_function_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#" . $byName->getPosition(); +echo ($byName->hasType() ? "T" : "t"); +echo ($byName->isOptional() ? "O" : "R"); +echo ($byName->isPassedByReference() ? "B" : "b"); +echo ($byName->isVariadic() ? "V" : "v"); +echo "|"; +$byPosition = new ReflectionParameter("REFLECT_DIRECT_FUNCTION", 3); +echo $byPosition->getName() . "#" . $byPosition->getPosition(); +echo ($byPosition->hasType() ? "T" : "t"); +echo ($byPosition->isOptional() ? "O" : "R"); +echo ($byPosition->isPassedByReference() ? "B" : "b"); +echo ($byPosition->isVariadic() ? "V" : "v"); +echo "|"; +$named = new ReflectionParameter(param: "id", function: "\\reflect_direct_function"); +echo $named->getName() . "#" . $named->getPosition(); +echo ($named->hasType() ? "T" : "t"); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "name#1tRBv|rest#3TObV|id#0T"); +} + +/// Verifies `ReflectionFunction` exposes user-function name and parameter metadata. +#[test] +fn test_reflection_function_reflects_user_function_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#"; +echo $ref->getNumberOfParameters() . "#"; +echo $ref->getNumberOfRequiredParameters() . ":"; +$params = $ref->getParameters(); +foreach ($params as $param) { + echo $param->getName() . "#" . $param->getPosition(); + echo ($param->hasType() ? "T" : "t"); + echo ($param->isOptional() ? "O" : "R"); + echo ($param->isPassedByReference() ? "B" : "b"); + echo ($param->isVariadic() ? "V" : "v"); + echo "|"; +} +$named = new ReflectionFunction(function: "\\reflect_function_target"); +echo ":" . $named->getName(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "reflect_function_target#4#2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3TObV|:reflect_function_target" + ); +} + +/// Verifies `ReflectionFunction::getAttributes()` exposes function attributes +/// and assigns factory ids that make `ReflectionAttribute::newInstance()` work. +#[test] +fn test_reflection_function_get_attributes_returns_function_attributes() { + let out = compile_and_run( + r##"name . "#" . $this->rank; } +} +class FlagMarker {} + +#[FunctionMarker("target", 7), FlagMarker] +function reflected_function_attrs() {} + +$ref = new ReflectionFunction("REFLECTED_FUNCTION_ATTRS"); +$attrs = $ref->getAttributes(); +echo count($attrs) . "/"; +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0] . "/"; +echo $attrs[0]->getArguments()[1] . "/"; +echo $attrs[0]->newInstance()->label() . "/"; +echo $attrs[1]->getName() . "/"; +echo count($attrs[1]->getArguments()); +"##, + ); + assert_eq!(out, "2/FunctionMarker/target/7/target#7/FlagMarker/0"); +} + +/// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` +/// carry the same parameter metadata as directly constructed method reflectors. +#[test] +fn test_reflection_class_get_methods_preserves_parameter_metadata() { + let out = compile_and_run_capture( + r##"getMethods(); +foreach ($methods as $method) { + if ($method->getName() === "listed") { + $params = $method->getParameters(); + echo $method->getNumberOfParameters() . "/"; + echo $method->getNumberOfRequiredParameters() . ":"; + echo $params[0]->getName() . ($params[0]->hasType() ? "T" : "t"); + echo ":"; + echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); + } +} +echo "|"; +$traitMethods = (new ReflectionClass(ReflectListedParamTrait::class))->getMethods(); +foreach ($traitMethods as $method) { + if ($method->getName() === "traitlisted") { + $params = $method->getParameters(); + echo $method->getNumberOfParameters() . "/"; + echo $method->getNumberOfRequiredParameters() . ":"; + echo ($method->isStatic() ? "S" : "s"); + echo ($method->isProtected() ? "P" : "p"); + echo ":"; + echo $params[0]->getName() . ($params[0]->hasType() ? "T" : "t"); + echo ":"; + echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); + echo ":"; + echo $params[2]->getName() . ($params[2]->isVariadic() ? "V" : "v"); + echo ($params[2]->hasType() ? "T" : "t"); + } +} +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2/1:firstT:secondO|3/1:SP:firstT:secondO:restVT" + ); +} + +/// Verifies that `ReflectionClass::getConstructor()` returns a ReflectionMethod +/// for direct, inherited, interface, and trait constructors, and null otherwise. +#[test] +fn test_reflection_class_get_constructor_returns_method_or_null() { + let out = compile_and_run( + r#"getConstructor(); +if ($base instanceof ReflectionMethod) { + echo $base->getName(); + echo "/" . $base->getNumberOfParameters(); + echo "/" . $base->getNumberOfRequiredParameters(); +} else { + echo "null"; +} +echo ":"; + +$child = (new ReflectionClass(ReflectCtorChild::class))->getConstructor(); +if ($child instanceof ReflectionMethod) { + echo $child->getName(); + echo "/" . $child->getNumberOfParameters(); + echo "/" . $child->getNumberOfRequiredParameters(); +} else { + echo "null"; +} +echo ":"; + +$plain = (new ReflectionClass(ReflectCtorPlain::class))->getConstructor(); +if ($plain instanceof ReflectionMethod) { + echo $plain->getName(); +} else { + echo "null"; +} +echo ":"; + +$interface = (new ReflectionClass(ReflectCtorInterface::class))->getConstructor(); +if ($interface instanceof ReflectionMethod) { + echo $interface->getName(); + echo "/" . $interface->getNumberOfParameters(); + echo "/" . $interface->getNumberOfRequiredParameters(); +} else { + echo "null"; +} +echo ":"; + +$trait = (new ReflectionClass(ReflectCtorTrait::class))->getConstructor(); +if ($trait instanceof ReflectionMethod) { + echo $trait->getName(); + echo "/" . $trait->getNumberOfParameters(); + echo "/" . $trait->getNumberOfRequiredParameters(); +} else { + echo "null"; +} +"#, + ); + assert_eq!( + out, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); +} + +/// Verifies that `ReflectionClass::newInstance()` constructs reflected classes +/// and forwards direct and statically-unpacked constructor arguments. +#[test] +fn test_reflection_class_new_instance_constructs_reflected_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$ref = new ReflectionClass(ReflectNewTarget::class); +$first = $ref->newInstance("A", "B"); +echo $first->label() . ":"; +$second = $ref->newInstance(...["C", "D"]); +echo $second->label(); +"#, + ); + assert_eq!(out, "AB:CD"); +} + +/// Verifies that `ReflectionClass::newInstanceArgs()` constructs reflected +/// classes from static positional and named argument arrays. +#[test] +fn test_reflection_class_new_instance_args_constructs_reflected_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +class ReflectEmptyNewArgsTarget { + public function label(): string { + return "empty"; + } +} +$ref = new ReflectionClass(ReflectNewArgsTarget::class); +$first = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(["right" => "Y", "left" => "X"]); +echo $first->label() . ":"; +$second = $ref->newInstanceArgs(["Q", "R"]); +echo $second->label() . ":"; +$third = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(args: ["left" => "L"]); +echo $third->label() . ":"; +$fourth = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(...[["left" => "M", "right" => "N"]]); +echo $fourth->label() . ":"; +$localArgs = ["right" => "P", "left" => "O"]; +$fifth = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs($localArgs); +echo $fifth->label() . ":"; +$empty = (new ReflectionClass(ReflectEmptyNewArgsTarget::class))->newInstanceArgs(); +echo $empty->label(); +"#, + ); + assert_eq!(out, "XY:QR:LB:MN:OP:empty"); +} + +/// Verifies that `ReflectionClass::newInstance()` accepts zero constructor +/// arguments for classes with no-argument or absent constructors. +#[test] +fn test_reflection_class_new_instance_allows_zero_constructor_args() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label(): string { + return $this->label; + } +} +class ReflectNoCtorNewTarget { + public string $label = "plain"; + public function label(): string { + return $this->label; + } +} +$first = (new ReflectionClass(ReflectNoArgNewTarget::class))->newInstance(); +echo $first->label() . ":"; +$ref = new ReflectionClass(ReflectNoCtorNewTarget::class); +$second = $ref->newInstance(); +echo $second->label(); +"#, + ); + assert_eq!(out, "ctor:plain"); +} + +/// Verifies that `ReflectionClass::newInstanceWithoutConstructor()` allocates +/// reflected classes while preserving property defaults and skipping `__construct()`. +#[test] +fn test_reflection_class_new_instance_without_constructor_skips_constructor() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label(): string { + return $this->label; + } + public function secret(): string { + return $this->secret; + } +} +$ref = new ReflectionClass(ReflectNoCtorTarget::class); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label() . ":" . $without->secret() . ":"; +$with = new ReflectNoCtorTarget(); +echo $with->label() . ":"; +$inline = (new ReflectionClass("reflectnoctortarget"))->newInstanceWithoutConstructor(); +echo $inline->label(); +"#, + ); + assert_eq!(out, "default:hidden:ctor:default"); +} + +/// Verifies inline `ReflectionClass::newInstance()` forwards named constructor +/// arguments through the reflected constructor signature. +#[test] +fn test_reflection_class_new_instance_forwards_named_constructor_args() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$first = (new ReflectionClass(ReflectNamedNewTarget::class))->newInstance(right: "B", left: "A"); +echo $first->label() . ":"; +$second = (new ReflectionClass(class_name: "reflectnamednewtarget"))->newInstance(...["right" => "D", "left" => "C"]); +echo $second->label(); +"#, + ); + assert_eq!(out, "AB:CD"); +} + +/// Verifies inline `ReflectionClass::newInstance()` uses constructor defaults +/// when named arguments leave optional parameters unspecified. +#[test] +fn test_reflection_class_new_instance_named_args_use_constructor_defaults() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$value = (new ReflectionClass(ReflectDefaultNewTarget::class))->newInstance(right: "B"); +echo $value->label(); +"#, + ); + assert_eq!(out, "LB"); +} + +/// Verifies that `ReflectionClassConstant` and enum-case reflectors expose +/// attribute name, arguments, `getName()`, and `newInstance()` data. +#[test] +fn test_reflection_constant_and_enum_case_get_attributes() { + let out = compile_and_run( + r#"label; } +} +class ConstTarget { + #[Marker("const")] + final public const ANSWER = 42; +} +enum CaseTarget: string { + #[Marker("case")] + case Ready = "ready"; + final public const LEVEL = 7; +} +$const = new ReflectionClassConstant(ConstTarget::class, "ANSWER"); +$constAttrs = $const->getAttributes(); +echo $const->getName() . "/"; +echo ($const->isFinal() ? "final" : "open") . "/"; +echo ($const->isEnumCase() ? "enum" : "plain") . "/"; +echo count($constAttrs) . "/"; +echo $constAttrs[0]->getName() . "/"; +echo $constAttrs[0]->getArguments()[0] . "/"; +echo $constAttrs[0]->newInstance()->label() . "\n"; +$listed = (new ReflectionClass(ConstTarget::class))->getReflectionConstants()[0]; +echo ($listed->isFinal() ? "listed-final" : "listed-open") . "\n"; +$case = new ReflectionClassConstant(CaseTarget::class, "Ready"); +$caseAttrs = $case->getAttributes(); +echo $case->getName() . "/"; +echo ($case->isFinal() ? "final" : "open") . "/"; +echo ($case->isEnumCase() ? "enum" : "plain") . "/"; +echo count($caseAttrs) . "/"; +echo $caseAttrs[0]->getName() . "/"; +echo $caseAttrs[0]->getArguments()[0] . "/"; +echo $caseAttrs[0]->newInstance()->label() . "\n"; +foreach ((new ReflectionClass(CaseTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "Ready") { + echo ($constant->isEnumCase() ? "listed-enum" : "listed-plain") . "\n"; + } +} +$level = new ReflectionClassConstant(CaseTarget::class, "LEVEL"); +echo ($level->isFinal() ? "level-final" : "level-open") . "/"; +echo ($level->isEnumCase() ? "level-enum" : "level-plain") . "\n"; +$unit = new ReflectionEnumUnitCase(CaseTarget::class, "Ready"); +$unitAttrs = $unit->getAttributes(); +echo $unit->getName() . "/"; +echo ($unit->getValue() === CaseTarget::Ready ? "unit-value" : "unit-bad") . "/"; +echo $unitAttrs[0]->newInstance()->label() . "\n"; +$backed = new ReflectionEnumBackedCase(CaseTarget::class, "Ready"); +$backedAttrs = $backed->getAttributes(); +echo $backed->getName() . "/"; +echo ($backed->getValue() === CaseTarget::Ready ? "backed-value" : "backed-bad") . "/"; +echo $backed->getBackingValue() . "/"; +echo $backedAttrs[0]->newInstance()->label(); +"#, + ); + assert_eq!( + out, + "ANSWER/final/plain/1/Marker/const/const\nlisted-final\nReady/open/enum/1/Marker/case/case\nlisted-enum\nlevel-final/level-plain\nReady/unit-value/case\nReady/backed-value/ready/case" + ); +} + +/// Verifies `ReflectionEnum` exposes AOT enum name and backing metadata. +#[test] +fn test_reflection_enum_owner_metadata() { + let out = compile_and_run( + r#"getName() . ":"; +echo ($pure->isBacked() ? "B" : "b") . ":"; +echo ($pure->getBackingType() === null ? "N" : "n") . ":"; +$backed = new ReflectionEnum(ReflectBackedEnum::class); +$type = $backed->getBackingType(); +echo ($backed->isBacked() ? "B" : "b") . ":"; +echo $type->getName() . ":"; +echo ($type->isBuiltin() ? "I" : "i"); +"#, + ); + assert_eq!( + out, + "ReflectPureEnum:b:N:B:int:I" + ); +} + +/// Verifies `ReflectionClassConstant` exposes visibility predicates and modifiers. +#[test] +fn test_reflection_class_constant_visibility_and_modifiers() { + let out = compile_and_run( + r#"isPrivate() ? "R" : "r"; +echo $secret->isProtected() ? "P" : "p"; +echo $secret->isPublic() ? "U" : "u"; +echo $secret->isFinal() ? "F" : "f"; +echo ":" . $secret->getModifiers() . "\n"; +$limit = new ReflectionClassConstant(ConstVisibilityTarget::class, "LIMIT"); +echo "LIMIT:"; +echo $limit->isPrivate() ? "R" : "r"; +echo $limit->isProtected() ? "P" : "p"; +echo $limit->isPublic() ? "U" : "u"; +echo $limit->isFinal() ? "F" : "f"; +echo ":" . $limit->getModifiers() . "\n"; +$answer = new ReflectionClassConstant(ConstVisibilityTarget::class, "ANSWER"); +echo "ANSWER:"; +echo $answer->isPrivate() ? "R" : "r"; +echo $answer->isProtected() ? "P" : "p"; +echo $answer->isPublic() ? "U" : "u"; +echo $answer->isFinal() ? "F" : "f"; +echo ":" . $answer->getModifiers() . "\n"; +$case = new ReflectionClassConstant(ConstVisibilityEnum::class, "Ready"); +echo "Ready:"; +echo $case->isPrivate() ? "R" : "r"; +echo $case->isProtected() ? "P" : "p"; +echo $case->isPublic() ? "U" : "u"; +echo $case->isFinal() ? "F" : "f"; +echo ":" . $case->getModifiers() . "\n"; +echo ReflectionClassConstant::IS_PUBLIC . ":"; +echo ReflectionClassConstant::IS_PROTECTED . ":"; +echo ReflectionClassConstant::IS_PRIVATE . ":"; +echo ReflectionClassConstant::IS_FINAL . "\n"; +echo "VALUES:" . $secret->getValue() . ":" . $limit->getValue() . ":" . $answer->getValue() . ":"; +echo $case->getValue() === ConstVisibilityEnum::Ready ? "E" : "e"; +echo "\n"; +foreach ((new ReflectionClass(ConstVisibilityTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "ANSWER") { + echo "LIST:" . $constant->getValue(); + } +} +"#, + ); + assert_eq!( + out, + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32\nVALUES:1:2:3:E\nLIST:3" + ); +} + +/// Verifies trait constants expose final metadata through direct and listed reflection. +#[test] +fn test_reflection_trait_constant_final_metadata() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo $direct->isFinal() ? "F" : "f"; +$flag = "?"; +$open = "?"; +foreach ((new ReflectionClass(TraitConstTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "FLAG") { + $flag = $constant->isFinal() ? "F" : "f"; + } + if ($constant->getName() === "OPEN") { + $open = $constant->isFinal() ? "O" : "o"; + } +} +echo ":" . $flag . $open; +$ifaceDirect = new ReflectionClassConstant(InterfaceConstTarget::class, "LIMIT"); +echo ":" . $ifaceDirect->getDeclaringClass()->getName() . ":"; +echo $ifaceDirect->isFinal() ? "I" : "i"; +$limit = "?"; +$ifaceOpen = "?"; +foreach ((new ReflectionClass(InterfaceConstTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "LIMIT") { + $limit = $constant->isFinal() ? "I" : "i"; + } + if ($constant->getName() === "OPEN") { + $ifaceOpen = $constant->isFinal() ? "P" : "p"; + } +} +echo ":" . $limit . $ifaceOpen; +"#, + ); + assert_eq!(out, "TraitConstTarget:F:Fo:InterfaceConstTarget:I:Ip"); +} + +/// Verifies interface constant reflection keeps the interface that declared each constant. +#[test] +fn test_reflection_interface_constant_declaring_metadata() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo $shared->getDeclaringClass()->getName() . ":"; +echo $implRoot->getDeclaringClass()->getName() . ":"; +echo $implShared->getDeclaringClass()->getName() . ":"; +echo $implLock->getDeclaringClass()->getName() . ":"; +echo $implLock->isFinal() ? "F" : "f"; +$all = (new ReflectionClass(InterfaceConstImpl::class))->getConstants(); +echo ":" . $all["ROOT"] . ":" . $all["SHARED"] . ":" . $all["LOCK"]; +$decls = ["ROOT" => "?", "SHARED" => "?", "LOCK" => "?"]; +$finals = ["ROOT" => "?", "SHARED" => "?", "LOCK" => "?"]; +foreach ((new ReflectionClass(InterfaceConstImpl::class))->getReflectionConstants() as $constant) { + $name = $constant->getName(); + $decls[$name] = $constant->getDeclaringClass()->getName(); + $finals[$name] = $constant->isFinal() ? "F" : "f"; +} +echo ":" . $decls["ROOT"] . ":" . $decls["SHARED"] . ":" . $decls["LOCK"]; +echo ":" . $finals["ROOT"] . ":" . $finals["SHARED"] . ":" . $finals["LOCK"]; +"#, + ); + assert_eq!( + out, + "InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:F:1:3:5:InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:f:f:F" + ); } /// Verifies that `ReflectionClass` accepts `user::class` (lowercase class diff --git a/tests/codegen/oop/callables/methods.rs b/tests/codegen/oop/callables/methods.rs index e256f218f6..47220f9f77 100644 --- a/tests/codegen/oop/callables/methods.rs +++ b/tests/codegen/oop/callables/methods.rs @@ -1068,6 +1068,45 @@ echo ($fn)(5); assert_eq!(out, "12"); } +/// Verifies a direct instance method call with a typed variadic tail keeps all +/// trailing arguments before the first-class callable regression below. +#[test] +fn test_direct_instance_method_string_variadic_count() { + let out = compile_and_run( + r#"join("A", "B", "C"); +"#, + ); + assert_eq!(out, "A:2"); +} + +/// Tests a captured first-class callable for an instance method with a variadic +/// signature, verifying the variadic tail is packed before the bound method call. +#[test] +fn test_captured_first_class_callable_instance_method_variadic_call() { + let out = compile_and_run( + r#"join(...); +echo $fn("A", "B", "C"); +"#, + ); + assert_eq!(out, "A:2"); +} + /// Tests a first-class callable where the method's receiver captures a private property /// from the outer scope, verifying the non-local state is correctly preserved across /// callable invocation. diff --git a/tests/codegen/oop/constants.rs b/tests/codegen/oop/constants.rs index 3a046b7597..1d446b06d4 100644 --- a/tests/codegen/oop/constants.rs +++ b/tests/codegen/oop/constants.rs @@ -161,6 +161,76 @@ echo $b->get(); assert_eq!(out, "100"); } +/// Verifies final class constants cannot be redeclared by subclasses. +#[test] +fn test_final_class_constant_override_fails() { + let err = compile_expect_type_error( + r#"$method()` dispatches dynamically when non-null and returns null when +/// the receiver is null without evaluating arguments. +#[test] +fn test_nullsafe_dynamic_instance_method_call() { + let out = compile_and_run( + "$method(skipped_arg()) ?? \"none\"; + echo \"|\"; + $c = new C(); + echo $c?->$method(\"ok\"); + ", + ); + assert_eq!(out, "none|run:ok"); +} + +/// Verifies that the brace form `$obj?->{$expr}()` skips the method-name expression and +/// arguments on the null branch, then dispatches dynamically on the non-null branch. +#[test] +fn test_nullsafe_dynamic_instance_method_brace_form_is_lazy() { + let out = compile_and_run( + "{method_name()}(skipped_arg()) ?? \"none\"; + echo \"|\"; + $g = new Greeter(); + echo $g?->{method_name()}(\"ok\"); + ", + ); + assert_eq!(out, "none|name:method:ok"); +} + /// Verifies that `$cls::method()` dispatches to a static method on the named class. #[test] fn test_dynamic_static_call_literal_method() { diff --git a/tests/codegen/oop/inheritance.rs b/tests/codegen/oop/inheritance.rs index 15bb307508..a0d82c776e 100644 --- a/tests/codegen/oop/inheritance.rs +++ b/tests/codegen/oop/inheritance.rs @@ -635,3 +635,78 @@ echo $c->value; ); assert_eq!(out, "42:42"); } + +/// Verifies a child property can shadow a private parent property with a fresh +/// slot: parent methods keep reading the private slot while child/global reads +/// see the child property. +#[test] +fn test_private_parent_property_shadowing_uses_separate_slots() { + let out = compile_and_run( + r#"value = 2; + } + + public function parentValue() { + return $this->value; + } +} + +class Child extends Base { + public $value = "child"; + + public function childValue() { + return $this->value; + } +} + +$c = new Child(); +echo $c->parentValue(); +echo ":"; +echo $c->childValue(); +echo ":"; +echo $c->value; +"#, + ); + assert_eq!(out, "2:child:child"); +} + +/// Verifies a later non-private redeclaration updates the visible parent slot, +/// while an older private grandparent slot stays separate for grandparent methods. +#[test] +fn test_private_grandparent_property_shadowing_survives_later_redeclaration() { + let out = compile_and_run( + r#"value; + } +} + +class ParentBox extends GrandParentBox { + public int $value = 2; + + public function parentValue() { + return $this->value; + } +} + +class ChildBox extends ParentBox { + public int $value = 3; +} + +$c = new ChildBox(); +echo $c->grandParentValue(); +echo ":"; +echo $c->parentValue(); +echo ":"; +echo $c->value; +"#, + ); + assert_eq!(out, "1:3:3"); +} diff --git a/tests/codegen/oop/interfaces.rs b/tests/codegen/oop/interfaces.rs index dbdf52168c..d01a518ca9 100644 --- a/tests/codegen/oop/interfaces.rs +++ b/tests/codegen/oop/interfaces.rs @@ -93,6 +93,75 @@ echo $item->name() . ":" . $item->tag(); assert_eq!(out, "box:BX"); } +/// Verifies a class can satisfy a static interface method contract. +/// +/// Fixture: interface `StaticMaker` declares `public static make(...)`; +/// `StaticWidget` implements it. The test also checks ReflectionClass and +/// ReflectionMethod expose the method as static. +#[test] +fn test_static_interface_method_contract_is_supported() { + let out = compile_and_run( + r#"hasMethod("make") ? "H" : "h"; +echo ":"; +$listed = $interface->getMethods()[0]; +echo $listed->getName(); +echo ":"; +echo $listed->isStatic() ? "S" : "s"; +echo ":"; +echo $listed->getNumberOfParameters(); +echo ":"; +$method = new ReflectionMethod(StaticMaker::class, "make"); +echo $method->isStatic() ? "S" : "s"; +echo ":"; +echo $method->getName(); +echo ":"; +echo (new ReflectionClass(StaticWidget::class))->implementsInterface(StaticMaker::class) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "W:box:H:make:S:1:S:make:Y"); +} + +/// Verifies an abstract class may defer a static interface method to a concrete child. +/// +/// Fixture: `AbstractStaticLabel` implements `StaticLabel` but leaves the +/// static contract abstract; `ConcreteStaticLabel` provides it and is callable. +#[test] +fn test_abstract_class_can_defer_static_interface_method_to_child() { + let out = compile_and_run( + r#"name())`. /// Asserts the method call correctly resolves through the transitive interface hierarchy. diff --git a/tests/codegen/oop/misc.rs b/tests/codegen/oop/misc.rs index 32b46055ca..57efc30708 100644 --- a/tests/codegen/oop/misc.rs +++ b/tests/codegen/oop/misc.rs @@ -9,6 +9,22 @@ use super::*; +/// Verifies PHP's generic `object` parameter type accepts concrete objects and +/// preserves object-shaped ABI lowering. +#[test] +fn test_generic_object_parameter_type_accepts_concrete_object() { + let out = compile_and_run( + r#" expr;` hook stores the expression result into the property's raw +/// backing slot. +#[test] +fn test_short_set_hook_normalizes_backing_slot() { + let out = compile_and_run( + " $this->value; + set => trim($value); + } + } + $n = new Name(); + $n->value = \" Ada \"; + echo \"[\", $n->value, \"]\"; + ", + ); + assert_eq!(out, "[Ada]"); +} + +/// Verifies a short set hook honors a custom parameter name. +#[test] +fn test_short_set_hook_custom_parameter_name() { + let out = compile_and_run( + " $this->text; + set(string $raw) => strtoupper($raw); + } + } + $l = new Label(); + $l->text = \"hi\"; + echo $l->text; + ", + ); + assert_eq!(out, "HI"); +} + /// Verifies a custom set-hook parameter name (`set(string $v)`) is honored in the body. #[test] fn test_set_hook_custom_parameter_name() { diff --git a/tests/codegen/oop/reflection_construction.rs b/tests/codegen/oop/reflection_construction.rs new file mode 100644 index 0000000000..48448e7b8b --- /dev/null +++ b/tests/codegen/oop/reflection_construction.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionClass and ReflectionObject +//! construction helpers over reflected class metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Covers inherited ReflectionObject construction helpers that must route +//! through the same dynamic class-name lowering as ReflectionClass. + +use super::*; + +/// Verifies `ReflectionObject` inherits working construction helpers from `ReflectionClass`. +#[test] +fn test_reflection_object_construction_helpers_use_runtime_class() { + let out = compile_and_run_capture( + r#"newInstance("A", "B"); +echo get_class($first) . ":"; +$second = $ref->newInstanceArgs(["right" => "Y", "left" => "X"]); +echo get_class($second) . ":"; +$third = $ref->newInstance(right: "N", left: "M"); +echo get_class($third) . ":"; +$fourth = $ref->newInstanceWithoutConstructor(); +echo get_class($fourth); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "IN|AB|ReflectObjectConstructChild:XY|ReflectObjectConstructChild:MN|ReflectObjectConstructChild:ReflectObjectConstructChild" + ); +} + +/// Verifies `ReflectionClass` construction helpers support inferred constructor signatures. +#[test] +fn test_reflection_class_construction_helpers_call_inferred_constructor_signature() { + let out = compile_and_run_capture( + r#"newInstance("A", "C"); +$ref->newInstanceArgs(["right" => "Y", "left" => "X"]); +$ref->newInstance(right: "N", left: "M"); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AC|XY|MN|"); +} diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs new file mode 100644 index 0000000000..ec67997134 --- /dev/null +++ b/tests/codegen/oop/reflection_functions.rs @@ -0,0 +1,247 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionFunction invocation paths over AOT +//! function metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - `ReflectionFunction::invoke()` and `invokeArgs()` are lowered for +//! statically-known reflectors whose target user function has declared +//! parameter types, plus supported callable builtins. +//! - Tests cover inline constructors, local tracking, case-insensitive function +//! names, defaults, named arguments, and static argument arrays. + +use super::*; + +/// Verifies AOT `ReflectionFunction` exposes function-abstract predicate metadata. +#[test] +fn test_reflection_function_reports_aot_function_abstract_predicates() { + let out = compile_and_run( + r#"isDeprecated() ? "D" : "d") . ":"; +echo ($plain->isDeprecated() ? "D" : "d") . ":"; +echo ($generator->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isClosure() ? "C" : "c") . ":"; +echo ($plain->returnsReference() ? "R" : "r") . ":"; +echo ($plain->hasTentativeReturnType() ? "H" : "h") . ":"; +echo ($plain->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo $plain->isDisabled() ? "X" : "x"; +"#, + ); + assert_eq!(out, "D:d:G:g:c:r:h:Q:x"); +} + +/// Verifies `ReflectionFunction` exposes declared AOT return type metadata. +#[test] +fn test_reflection_function_reports_aot_return_type_metadata() { + let out = compile_and_run( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$declaring = $namedRef->getParameters()[0]->getDeclaringFunction()->getReturnType(); +echo $declaring->getName() . ":"; +$union = (new ReflectionFunction("reflect_return_union"))->getReturnType(); +if ($union instanceof ReflectionUnionType) { + echo count($union->getTypes()) . ":"; + foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; + } +} else { + echo "not-union"; +} +echo ":"; +$never = (new ReflectionFunction("reflect_return_never"))->getReturnType(); +echo $never->getName() . ":"; +echo ($never->allowsNull() ? "N" : "n") . ":"; +echo ($never->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionFunction("reflect_return_plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "T:int:N:B:int:2:intBstringB:never:n:B:p:Q"); +} + +/// Verifies `ReflectionFunction::isVariadic()` reports the function-level variadic flag. +#[test] +fn test_reflection_function_reports_aot_variadic_flag() { + let out = compile_and_run( + r#"isVariadic() ? "V" : "v") . ":"; +echo $variadic->getNumberOfParameters() . ":"; +echo ($fixed->isVariadic() ? "V" : "v"); +"#, + ); + assert_eq!(out, "V:2:v"); +} + +/// Verifies `ReflectionFunction` exposes AOT function name and origin metadata. +#[test] +fn test_reflection_function_reports_aot_name_origin_predicates() { + let out = compile_and_run( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo ($ref->inNamespace() ? "Y" : "N") . ":"; +echo ($ref->isInternal() ? "I" : "i") . ":"; +echo $ref->isUserDefined() ? "U" : "u"; +"#, + ); + assert_eq!(out, "ReflectFunctionMetaNs\\sample:sample:ReflectFunctionMetaNs:Y:i:U"); +} + +/// Verifies `ReflectionFunction` exposes supported callable-builtin metadata. +#[test] +fn test_reflection_function_reports_builtin_metadata() { + let out = compile_and_run( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo ($ref->isInternal() ? "I" : "i") . ":"; +echo ($ref->isUserDefined() ? "U" : "u") . ":"; +echo ($ref->hasReturnType() ? "T" : "t") . ":"; +echo $ref->getReturnType()->getName() . ":"; +$params = $ref->getParameters(); +echo count($params) . ":"; +echo $params[0]->getName() . ":"; +echo ($params[0]->hasType() ? "P" : "p") . ":"; +echo $params[0]->getType()->getName() . ":"; +echo ($params[0]->getDeclaringFunction()->isInternal() ? "D" : "d") . ":"; +echo (new ReflectionParameter("strlen", "string"))->getDeclaringFunction()->getName(); +"#, + ); + assert_eq!(out, "strlen:strlen:I:u:T:int:1:string:P:string:D:strlen"); +} + +/// Verifies `ReflectionFunction::invoke()` and `invokeArgs()` call supported builtins. +#[test] +fn test_reflection_function_invoke_calls_builtin_functions() { + let out = compile_and_run( + r#"invoke("abc"); +echo ":"; +echo (new ReflectionFunction("strlen"))->invoke(string: "abcd"); +echo ":"; +$ref = new ReflectionFunction("strlen"); +echo $ref->invokeArgs(["abcde"]); +echo ":"; +echo $ref->invokeArgs(args: ["string" => "abcdef"]); +"#, + ); + assert_eq!(out, "3:4:5:6"); +} + +/// Verifies non-closure `ReflectionFunction` objects report no used variables. +#[test] +fn test_reflection_function_reports_empty_closure_used_variables() { + let out = compile_and_run( + r#"getClosureUsedVariables()) . ":"; +echo count($builtin->getClosureUsedVariables()) . ":"; +$vars = $user->getClosureUsedVariables(); +$vars["x"] = "changed"; +echo count($user->getClosureUsedVariables()); +"#, + ); + assert_eq!(out, "0:0:0"); +} + +/// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. +#[test] +fn test_reflection_function_invoke_calls_declared_aot_functions() { + let out = compile_and_run( + r#"invoke("A", "C"); +echo ":"; +echo (new ReflectionFunction(function: "\\reflect_function_invoke_target"))->invoke(right: "Y", left: "X"); +echo ":"; +$ref = new ReflectionFunction("reflect_function_invoke_target"); +echo $ref->invoke("L"); +echo ":"; +echo (new ReflectionFunction("reflect_function_invoke_zero"))->invoke(); +"#, + ); + assert_eq!(out, "AC:XY:LB:Z"); +} + +/// Verifies `ReflectionFunction::invokeArgs()` forwards static argument arrays. +#[test] +fn test_reflection_function_invoke_args_calls_declared_aot_functions() { + let out = compile_and_run( + r#"invokeArgs(["right" => "Y", "left" => "X"]); +echo ":"; +$localArgs = ["right" => "P", "left" => "O"]; +$ref = new ReflectionFunction("reflect_function_invoke_args_target"); +echo $ref->invokeArgs($localArgs); +echo ":"; +echo $ref->invokeArgs(...[["A", "C"]]); +echo ":"; +echo $ref->invokeArgs(args: ["Q"]); +"#, + ); + assert_eq!(out, "XY:OP:AC:QB"); +} + +/// Verifies `ReflectionFunction::invoke()` supports inferred AOT signatures. +#[test] +fn test_reflection_function_invoke_calls_inferred_aot_signature() { + let out = compile_and_run( + r#"invoke("A", "B"); +"#, + ); + assert_eq!(out, "AB"); +} diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs new file mode 100644 index 0000000000..b9c8c0791f --- /dev/null +++ b/tests/codegen/oop/reflection_methods.rs @@ -0,0 +1,301 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionMethod invocation paths over AOT +//! class metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - `ReflectionMethod::invoke()` and `invokeArgs()` are lowered only for statically-known +//! reflectors whose target method has declared parameter types. +//! - Tests cover inline constructors, ReflectionClass lookup, local tracking, +//! case-insensitive method names, default values, and named arguments. + +use super::*; + +/// Verifies AOT `ReflectionMethod` exposes function-abstract predicate metadata. +#[test] +fn test_reflection_method_reports_aot_function_abstract_predicates() { + let out = compile_and_run( + r#"getMethod("generator"); +echo ($deprecated->isDeprecated() ? "D" : "d") . ":"; +echo ($plain->isDeprecated() ? "D" : "d") . ":"; +echo ($generator->isGenerator() ? "G" : "g") . ":"; +echo ($listed->isGenerator() ? "L" : "l") . ":"; +echo ($plain->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isClosure() ? "C" : "c") . ":"; +echo ($plain->returnsReference() ? "R" : "r") . ":"; +echo ($plain->hasTentativeReturnType() ? "H" : "h") . ":"; +echo $plain->getTentativeReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "D:d:G:L:g:c:r:h:Q"); +} + +/// Verifies `ReflectionMethod` exposes declared AOT return type metadata. +#[test] +fn test_reflection_method_reports_aot_return_type_metadata() { + let out = compile_and_run( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$declaring = $namedRef->getParameters()[0]->getDeclaringFunction()->getReturnType(); +echo $declaring->getName() . ":"; +$static = (new ReflectionClass(ReflectMethodReturnTarget::class))->getMethod("factory")->getReturnType(); +echo $static->getName() . ":"; +echo ($static->allowsNull() ? "N" : "n") . ":"; +echo ($static->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionMethod(ReflectMethodReturnTarget::class, "plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "T:string:N:B:string:ReflectMethodReturnDep:n:b:p:Q"); +} + +/// Verifies `ReflectionMethod::isVariadic()` reports the method-level variadic flag. +#[test] +fn test_reflection_method_reports_aot_variadic_flag() { + let out = compile_and_run( + r#"isVariadic() ? "V" : "v") . ":"; +echo $variadic->getNumberOfParameters() . ":"; +echo ($fixed->isVariadic() ? "V" : "v"); +"#, + ); + assert_eq!(out, "V:2:v"); +} + +/// Verifies `ReflectionMethod` exposes AOT method name and origin metadata. +#[test] +fn test_reflection_method_reports_aot_name_origin_predicates() { + let out = compile_and_run( + r#"getName() . ":"; +echo $method->getShortName() . ":"; +echo $method->getNamespaceName() . ":"; +echo ($method->inNamespace() ? "Y" : "N") . ":"; +echo ($method->isInternal() ? "I" : "i") . ":"; +echo $method->isUserDefined() ? "U" : "u"; +"#, + ); + assert_eq!(out, "run:run::N:i:U"); +} + +/// Verifies AOT `ReflectionMethod::hasPrototype()` and `getPrototype()` follow PHP inheritance rules. +#[test] +fn test_reflection_method_reports_aot_prototypes() { + let out = compile_and_run( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = (new ReflectionClass(ReflectMethodProtoChild::class))->getMethod("iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod(ReflectMethodProtoChild::class, "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$own = new ReflectionMethod(ReflectMethodProtoChild::class, "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod(ReflectMethodProtoChild::class, "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +"#, + ); + assert_eq!( + out, + "Y:ReflectMethodProtoBase::run:Y:ReflectMethodProtoIface::iface:ReflectMethodProtoParentIface::parented:N:E:N" + ); +} + +/// Verifies `ReflectionMethod::invoke()` calls declared AOT instance and static methods. +#[test] +fn test_reflection_method_invoke_calls_declared_aot_methods() { + let out = compile_and_run( + r#"invoke($object, "A", "C"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "JOIN"))->invoke($object, "D"); +echo ":"; +echo (new ReflectionClass(ReflectInvokeTarget::class))->getMethod("join")->invoke($object, "E", "F"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "make"))->invoke(null, right: "Y", left: "X"); +echo ":"; +$method = new ReflectionMethod(ReflectInvokeTarget::class, "join"); +echo $method->invoke($object, "L", "M"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "zero"))->invoke($object); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "staticZero"))->invoke(null); +"#, + ); + assert_eq!(out, "AC:DB:EF:XY:LM:Z:T"); +} + +/// Verifies `ReflectionMethod::invokeArgs()` forwards static argument arrays. +#[test] +fn test_reflection_method_invoke_args_calls_declared_aot_methods() { + let out = compile_and_run( + r#"invokeArgs($object, ["right" => "Y", "left" => "X"]); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "JOIN"))->invokeArgs($object, ["Q"]); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "make"))->invokeArgs(null, ["right" => "N", "left" => "M"]); +echo ":"; +echo (new ReflectionClass(ReflectInvokeArgsTarget::class))->getMethod("join")->invokeArgs(object: $object, args: ["left" => "L"]); +echo ":"; +$method = new ReflectionMethod(ReflectInvokeArgsTarget::class, "join"); +echo $method->invokeArgs(...[$object, ["A", "C"]]); +echo ":"; +$localArgs = ["right" => "P", "left" => "O"]; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "join"))->invokeArgs($object, $localArgs); +"#, + ); + assert_eq!(out, "XY:QB:MN:LB:AC:OP"); +} + +/// Verifies constructors returned by `ReflectionClass::getConstructor()` can be invoked. +#[test] +fn test_reflection_method_invoke_calls_aot_constructor_from_reflection_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} + +$object = new ReflectInvokeCtorTarget("A", "A"); +$result = (new ReflectionClass(ReflectInvokeCtorTarget::class))->getConstructor()->invoke($object, "X", "Y"); +echo ($result === null ? "null" : "value") . ":" . $object->label(); +echo ":"; +$ctor = (new ReflectionClass(ReflectInvokeCtorTarget::class))->getConstructor(); +$ctor->invokeArgs($object, ["right" => "N", "left" => "M"]); +echo $object->label(); +"#, + ); + assert_eq!(out, "null:XY:MN"); +} + +/// Verifies `ReflectionMethod::setAccessible()` is a no-op for AOT reflectors. +#[test] +fn test_reflection_method_set_accessible_is_noop_for_aot_methods() { + let out = compile_and_run( + r#"setAccessible(false)) ? "M" : "m"; +echo ":" . $method->invoke($object); +echo ":"; +$listed = (new ReflectionClass(ReflectMethodAccessTarget::class))->getMethod("hidden"); +echo is_null($listed->setAccessible(accessible: true)) ? "L" : "l"; +echo ":" . $listed->invoke($object); +"#, + ); + assert_eq!(out, "M:secret:L:secret"); +} + +/// Verifies `ReflectionMethod::invoke()` supports inferred AOT signatures. +#[test] +fn test_reflection_method_invoke_calls_inferred_aot_signature() { + let out = compile_and_run( + r#"invoke($object, "A", "B"); +"#, + ); + assert_eq!(out, "AB"); +} diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs new file mode 100644 index 0000000000..f1f9c8c950 --- /dev/null +++ b/tests/codegen/oop/reflection_properties.rs @@ -0,0 +1,530 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionProperty value accessors on supported +//! object-property storage. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Covers explicit object arguments for public and non-public instance properties. +//! - Covers runtime-held public instance property reflectors. +//! - Covers static properties where PHP permits omitted or ignored object args. +//! - Covers Reflection visibility bypass for instance and inline static properties. + +use super::*; + +/// Verifies `ReflectionProperty::getValue()` and `setValue()` read and write +/// public instance properties for inline reflectors with explicit object args, +/// including reflectors returned by `ReflectionClass::getProperty()`. +#[test] +fn test_reflection_property_value_accessors_for_public_instance_properties() { + let out = compile_and_run( + r#"getValue($target); +(new ReflectionProperty(ReflectValueAccessTarget::class, "count"))->setValue($target, 7); +echo ":" . $target->count; +echo ":" . (new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->getValue($target); +(new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->setValue($target, "new"); +echo ":" . $target->label; +echo ":" . (new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->getValue($target); +(new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->setValue($target, 11); +echo ":" . $target->count; +echo ":" . (new ReflectionProperty(ReflectValueAccessTarget::class, "count"))->getValue(object: $target); +(new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->setValue(value: 13, object: $target); +echo ":" . $target->count; +"#, + ); + assert_eq!(out, "1:7:old:new:7:11:11:13"); +} + +/// Verifies `ReflectionProperty::getValue()` and `setValue()` read and write +/// public static properties for inline reflectors. +#[test] +fn test_reflection_property_value_accessors_for_public_static_properties() { + let out = compile_and_run( + r#"getValue(); +(new ReflectionProperty(ReflectStaticValueAccessTarget::class, "count"))->setValue(null, 17); +echo ":" . ReflectStaticValueAccessTarget::$count; +ReflectStaticValueAccessTarget::$count = 19; +echo ":" . (new ReflectionProperty(ReflectStaticValueAccessTarget::class, "count"))->getValue(object: null); +echo ":" . (new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("label")->getValue(null); +(new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("label")->setValue(null, "new"); +echo ":" . ReflectStaticValueAccessTarget::$label; +(new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("count")->setValue(object: null, value: 23); +echo ":" . ReflectStaticValueAccessTarget::$count; +"#, + ); + assert_eq!(out, "2:17:19:old:new:23"); +} + +/// Verifies `ReflectionProperty::isInitialized()` observes typed instance +/// property initialization without reading the property value. +#[test] +fn test_reflection_property_is_initialized_for_instance_properties() { + let out = compile_and_run( + r#"hidden = 7; + } +} + +$target = new ReflectInitializedInstanceTarget(); +$typed = new ReflectionProperty(ReflectInitializedInstanceTarget::class, "typed"); +echo $typed->isInitialized($target) ? "bad" : "uninit"; +$target->typed = 3; +echo ":" . ($typed->isInitialized(object: $target) ? "typed" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedInstanceTarget::class, "nullable"))->isInitialized($target) ? "nullable" : "bad"); +echo ":" . ((new ReflectionClass(ReflectInitializedInstanceTarget::class))->getProperty("implicit")->isInitialized($target) ? "implicit" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedInstanceTarget::class, "hidden"))->isInitialized($target) ? "hidden" : "bad"); +"#, + ); + assert_eq!(out, "uninit:typed:nullable:implicit:hidden"); +} + +/// Verifies `ReflectionProperty::isInitialized()` observes static-property +/// initialization while bypassing property visibility. +#[test] +fn test_reflection_property_is_initialized_for_static_properties() { + let out = compile_and_run( + r#"isInitialized() ? "bad" : "uninit"; +ReflectInitializedStaticTarget::$typed = 3; +echo ":" . ($typed->isInitialized(object: null) ? "typed" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedStaticTarget::class, "nullable"))->isInitialized() ? "nullable" : "bad"); +$hidden = (new ReflectionClass(ReflectInitializedStaticTarget::class))->getProperty("hidden"); +echo ":" . ($hidden->isInitialized() ? "bad" : "hidden-uninit"); +ReflectInitializedStaticTarget::initHidden(); +echo ":" . ($hidden->isInitialized() ? "hidden" : "bad"); +"#, + ); + assert_eq!(out, "uninit:typed:nullable:hidden-uninit:hidden"); +} + +/// Verifies ReflectionProperty static value access bypasses visibility for +/// private and protected properties when the reflected target is statically known. +#[test] +fn test_reflection_property_value_accessors_bypass_static_visibility() { + let out = compile_and_run( + r#"getValue(); +(new ReflectionProperty(ReflectHiddenStaticValueAccessTarget::class, "count"))->setValue(null, 8); +echo ":" . ReflectHiddenStaticValueAccessTarget::count(); + +$label = (new ReflectionClass(ReflectHiddenStaticValueAccessTarget::class))->getProperty("label"); +echo ":" . $label->getValue(null); +$label->setValue(object: null, value: "new"); +echo ":" . ReflectHiddenStaticValueAccessTarget::label(); +"#, + ); + assert_eq!(out, "4:8:old:new"); +} + +/// Verifies static ReflectionProperty objects selected from `getProperties()` +/// with known indexes can read and write their reflected static storage. +#[test] +fn test_reflection_property_value_accessors_for_indexed_static_property_lists() { + let out = compile_and_run( + r#"getProperties()[0]; +echo $count->getName() . ":" . $count->getValue(); +$count->setValue(null, 8); +echo ":" . ReflectListedStaticValueAccessTarget::count(); + +$ref = new ReflectionClass(ReflectListedStaticValueAccessTarget::class); +$label = $ref->getProperties()[1]; +echo ":" . $label->getName() . ":" . $label->getValue(null); +$label->setValue(null, "new"); +echo ":" . ReflectListedStaticValueAccessTarget::label(); +"#, + ); + assert_eq!(out, "count:4:8:label:old:new"); +} + +/// Verifies `ReflectionClass::getProperties()` applies modifier filters and +/// that filtered static property entries retain direct value accessor support. +#[test] +fn test_reflection_property_value_accessors_for_filtered_static_property_lists() { + let out = compile_and_run( + r#"getProperties(ReflectionProperty::IS_STATIC); +echo count($static) . ":" . $static[0]->getName(); + +$count = $ref->getProperties(ReflectionProperty::IS_STATIC)[0]; +echo ":" . $count->getValue(); +$count->setValue(null, 8); +echo ":" . ReflectFilteredStaticValueAccessTarget::count(); + +$label = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED)[0]; +echo ":" . $label->getName() . ":" . $label->getValue(null); +$label->setValue(null, "new"); +echo ":" . ReflectFilteredStaticValueAccessTarget::label(); + +$public = $ref->getProperties(...["filter" => ReflectionProperty::IS_PUBLIC]); +$none = $ref->getProperties(0); +echo ":" . count($public) . ":" . $public[0]->getName() . ":" . count($none); + +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +echo ":" . count($staticMethods) . ":" . count($privateMethods) . ":" . count($noMethods); +"#, + ); + assert_eq!(out, "2:count:4:8:label:old:new:1:instance:0:3:1:0"); +} + +/// Verifies runtime-held `ReflectionClass` objects apply dynamic member filters +/// without degrading returned reflector elements to unusable mixed payloads. +#[test] +fn test_reflection_class_runtime_member_filters_return_usable_reflectors() { + let out = compile_and_run( + r#"getProperties($propertyFilter); + echo count($props) . ":" . $props[0]->getName() . ":" . $props[1]->getName(); + + $methods = $ref->getMethods($methodFilter); + echo ":" . count($methods) . ":" . $methods[0]->getName() . ":" . $methods[0]->isPrivate(); +} + +inspect_members( + new ReflectionClass(ReflectRuntimeFilteredMemberTarget::class), + ReflectionProperty::IS_STATIC, + ReflectionMethod::IS_PRIVATE +); +"#, + ); + assert_eq!(out, "2:count:label:1:secret:1"); +} + +/// Verifies ReflectionClass static property value helpers bypass visibility and +/// operate on the same live static storage as direct class methods. +#[test] +fn test_reflection_class_static_value_accessors_bypass_visibility() { + let out = compile_and_run( + r#"getStaticPropertyValue("count"); +$ref->setStaticPropertyValue("count", 9); +echo ":" . ReflectClassHiddenStaticValueTarget::count(); +echo ":" . $ref->getStaticPropertyValue("label"); +$ref->setStaticPropertyValue(name: "label", value: "new"); +echo ":" . ReflectClassHiddenStaticValueTarget::label(); + +$props = $ref->getStaticProperties(); +echo ":" . $props["count"]; +echo ":" . $props["label"]; +"#, + ); + assert_eq!(out, "5:9:old:new:9:new"); +} + +/// Verifies runtime-held `ReflectionClass` objects expose materialized AOT +/// static-property values and omit uninitialized typed static properties. +#[test] +fn test_reflection_class_runtime_static_properties_materialize_aot_values() { + let out = compile_and_run( + r#"getStaticProperties(); + echo count($props); + echo ":" . $props["count"]; + echo ":" . $props["label"]; + echo ":" . (array_key_exists("nullable", $props) && $props["nullable"] === null ? "null" : "bad"); + echo ":" . ($props["unset"] ?? "missing"); + echo ":" . $ref->getStaticPropertyValue("count", "fallback"); + echo ":" . ($ref->getStaticPropertyValue("nullable", "fallback") === null ? "null" : "bad"); + echo ":" . $ref->getStaticPropertyValue("missing", "fallback"); +} + +ReflectRuntimeStaticPropertiesTarget::$count = 5; +ReflectRuntimeStaticPropertiesTarget::rename("new"); +inspect_static_props(new ReflectionClass(ReflectRuntimeStaticPropertiesTarget::class)); +"#, + ); + assert_eq!(out, "3:5:new:null:missing:5:null:fallback"); +} + +/// Verifies runtime-held static `ReflectionProperty` objects can read +/// materialized static values and report initialization from their declaring class. +#[test] +fn test_reflection_property_runtime_static_reflectors_read_materialized_values() { + let out = compile_and_run( + r#"getValue(); + echo ":" . ($count->isInitialized() ? "count" : "bad"); + echo ":" . $label->getValue(null); + echo ":" . ($label->isInitialized(object: null) ? "label" : "bad"); + echo ":" . ($unset->isInitialized() ? "bad" : "unset"); +} + +ReflectRuntimeStaticValuePropertyTarget::$count = 7; +ReflectRuntimeStaticValuePropertyTarget::rename("new"); +inspect_static_reflectors( + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "count"), + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "label"), + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "unset") +); +"#, + ); + assert_eq!(out, "7:count:new:label:unset"); +} + +/// Verifies runtime-held `ReflectionProperty` objects can read and write public +/// instance properties through their retained property names. +#[test] +fn test_reflection_property_value_accessors_for_runtime_instance_reflectors() { + let out = compile_and_run( + r#"getValue($target); +$count->setValue($target, 8); +echo ":" . $target->count; + +$listed = (new ReflectionClass(ReflectRuntimeValueAccessTarget::class))->getProperties()[1]; +echo ":" . $listed->getName(); +echo ":" . $listed->getValue($target); +$listed->setValue($target, "new"); +echo ":" . $target->label; +"#, + ); + assert_eq!(out, "4:8:label:old:new"); +} + +/// Verifies ReflectionProperty value access bypasses visibility for private +/// and protected instance properties, matching PHP's Reflection behavior. +#[test] +fn test_reflection_property_value_accessors_bypass_instance_visibility() { + let out = compile_and_run( + r#"getValue($target); +$count->setValue($target, 8); +echo ":" . $count->getValue($target); + +$label = (new ReflectionClass(ReflectHiddenValueAccessTarget::class))->getProperty("label"); +echo ":" . $label->getValue($target); +$label->setValue($target, "new"); +echo ":" . $label->getValue($target); +"#, + ); + assert_eq!(out, "4:8:old:new"); +} + +/// Verifies `ReflectionProperty::setAccessible()` is a no-op for AOT reflectors. +#[test] +fn test_reflection_property_set_accessible_is_noop_for_aot_properties() { + let out = compile_and_run( + r#"setAccessible(false)) ? "P" : "p"; +echo ":" . $count->getValue($target); +$count->setValue($target, 9); +echo ":" . $count->getValue($target); +echo ":"; +$label = (new ReflectionClass(ReflectPropertyAccessTarget::class))->getProperty("label"); +echo is_null($label->setAccessible(accessible: true)) ? "L" : "l"; +echo ":" . $label->getValue($target); +"#, + ); + assert_eq!(out, "P:4:9:L:old"); +} + +/// Verifies AOT `ReflectionProperty::__toString()` formats retained generated +/// property metadata. +#[test] +fn test_reflection_property_to_string_formats_aot_metadata() { + let out = compile_and_run( + r#"__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "label"))->__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "implicit"))->__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "union"))->__toString(); +echo "|"; +echo (new ReflectionClass(ReflectPropertyStringTarget::class))->getProperty("label")->__toString(); +"#, + ); + assert_eq!( + out, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public int|string $union ]|Property [ protected static string $label = 'ok' ]" + ); +} + +/// Verifies AOT `ReflectionProperty::hasHooks()` and `getHooks()` expose +/// concrete property hook metadata as ReflectionMethod objects. +#[test] +fn test_reflection_property_get_hooks_formats_aot_hook_metadata() { + let out = compile_and_run( + r#"raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} + +$hooked = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "doubled"); +$readonly = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "readonlyHook"); +$plain = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "plain"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +$caseList = PropertyHookType::cases(); + +echo count($caseList) . ":" . $caseList[0]->value . ":"; +echo PropertyHookType::from("set")->name . ":"; +echo (PropertyHookType::tryFrom("missing") === null ? "T" : "t") . ":"; +echo ($hooked->hasHooks() ? "H" : "h") . ":"; +$hooks = $hooked->getHooks(); +echo count($hooks) . ":" . $hooks["get"]->getName() . ":" . $hooks["set"]->getName() . ":"; +echo $hooks["get"]->getDeclaringClass()->getName() . ":"; +echo $hooks["get"]->getNumberOfParameters() . ":"; +echo $hooks["set"]->getNumberOfParameters() . ":" . $hooks["set"]->getParameters()[0]->getName() . ":"; +echo ($hooked->hasHook($getCase) ? "G" : "g") . ":"; +echo ($hooked->hasHook(type: $setCase) ? "S" : "s") . ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getName() . ":" . $set->getName() . ":"; +echo ($readonly->hasHooks() ? "R" : "r") . ":"; +$readonlyHooks = $readonly->getHooks(); +echo count($readonlyHooks) . ":" . $readonlyHooks["get"]->getName() . ":"; +echo ($readonly->hasHook($getCase) ? "RG" : "rg") . ":"; +echo ($readonly->hasHook($setCase) ? "bad" : "RS") . ":"; +echo ($readonly->getHook($setCase) === null ? "N" : "n") . ":"; +echo ($plain->hasHooks() ? "bad" : "plain") . ":" . count($plain->getHooks()); +"#, + ); + assert_eq!( + out, + "2:get:Set:T:H:2:$doubled::get:$doubled::set:ReflectPropertyHookMetadataTarget:0:1:value:G:S:$doubled::get:$doubled::set:R:1:$readonlyHook::get:RG:RS:N:plain:0" + ); +} diff --git a/tests/codegen/optimizer/constant_folding/expressions.rs b/tests/codegen/optimizer/constant_folding/expressions.rs index 4e60840623..c0e8177f37 100644 --- a/tests/codegen/optimizer/constant_folding/expressions.rs +++ b/tests/codegen/optimizer/constant_folding/expressions.rs @@ -9,6 +9,18 @@ use super::*; +/// Returns only `main`'s section of the user assembly. Synthetic builtin +/// method bodies (e.g. the eval Reflection surface) legitimately call runtime +/// string helpers and would trip needle-based assertions about user code. +fn main_function_asm(user_asm: &str) -> &str { + let Some(start) = user_asm.find("@fn name=main ") else { + return user_asm; + }; + let rest = &user_asm[start..]; + let end = rest[1..].find("@fn name=").map_or(rest.len(), |i| i + 1); + &rest[..end] +} + /// Verifies that nested integer arithmetic with literals is constant-folded at compile time /// and the result is emitted directly as a literal in the generated binary. #[test] @@ -58,7 +70,7 @@ fn test_constant_folding_string_concat_removes_runtime_concat_call() { ); assert!( - !user_asm.contains("__rt_concat"), + !main_function_asm(&user_asm).contains("__rt_concat"), "constant-folded concat expression should not call __rt_concat in user assembly:\n{}", user_asm ); @@ -90,7 +102,7 @@ fn test_constant_folding_null_coalesce_removes_runtime_concat_call() { ); assert!( - !user_asm.contains("__rt_concat"), + !main_function_asm(&user_asm).contains("__rt_concat"), "constant-folded null coalesce should not leave __rt_concat in user assembly:\n{}", user_asm ); @@ -205,7 +217,7 @@ fn test_constant_folding_string_cast_removes_runtime_itoa_call() { compile_source_to_asm_with_options(" "a", 1 => "shadowed") produces /// output "a!" with "shadowed" absent from user assembly. #[test] diff --git a/tests/codegen/runtime_reachability.rs b/tests/codegen/runtime_reachability.rs new file mode 100644 index 0000000000..85c7d4f8f4 --- /dev/null +++ b/tests/codegen/runtime_reachability.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Regression coverage for feature-gated runtime and synthetic builtin reachability. +//! +//! Called from: +//! - `cargo test` through the codegen integration-test harness. +//! +//! Key details: +//! - Plain native programs must not carry the optional eval Reflection surface. + +use crate::support::{compile_source_to_asm_with_options, fs, make_cli_test_dir}; + +/// Verifies a program without eval or Reflection omits their synthetic methods and metadata. +#[test] +fn test_plain_program_omits_unreferenced_reflection_surface() { + let dir = make_cli_test_dir("elephc_plain_runtime_reachability"); + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options("i = 0; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +$callback = "is_array"; +$args = ["value" => [1]]; +echo iterator_apply(new Range(), $callback, $args); +"#, + ); + assert_eq!(out, "2"); +} + +/// Verifies that iterator apply can dispatch dynamic string callbacks to type predicate aliases. +#[test] +fn test_iterator_apply_dynamic_string_is_integer_callback_assoc_args() { + let out = compile_and_run( + r#"i = 0; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +$callback = "is_integer"; +$args = ["value" => 1]; +echo iterator_apply(new Range(), $callback, $args); +"#, + ); + assert_eq!(out, "2"); +} + /// Verifies that iterator apply dynamic args for by ref callback use temp cells. #[test] fn test_iterator_apply_dynamic_args_for_by_ref_callback_use_temp_cells() { diff --git a/tests/codegen/support/compiler.rs b/tests/codegen/support/compiler.rs index bfe5740281..00e17cfb03 100644 --- a/tests/codegen/support/compiler.rs +++ b/tests/codegen/support/compiler.rs @@ -111,7 +111,8 @@ pub(crate) fn compile_source_to_asm_with_defines_repr( .required_libraries .iter() .any(|lib| lib == "elephc_tls"); - let ir_module = lower_and_validate_ir_for_codegen_fixture(&optimized, &check_result); + let ir_module = + lower_and_validate_ir_for_codegen_fixture(&optimized, &check_result, &synthetic_main); let exported_functions = HashMap::new(); // Honor ELEPHC_REGALLOC so the whole codegen suite can be run under both // the linear-scan allocator (default) and the stack fallback. @@ -144,8 +145,14 @@ pub(crate) fn compile_source_to_asm_with_defines_repr( pub(crate) fn lower_and_validate_ir_for_codegen_fixture( program: &elephc::parser::ast::Program, check_result: &elephc::types::CheckResult, + source_path: &Path, ) -> elephc::ir::Module { - let mut module = elephc::ir_lower::lower_program(program, check_result, target()) + let mut module = elephc::ir_lower::lower_program_with_source_path( + program, + check_result, + target(), + source_path, + ) .expect("AST-to-EIR lowering failed for codegen fixture"); if ir_opt_enabled_for_codegen_fixture() { elephc::ir_passes::optimize_module(&mut module); diff --git a/tests/codegen/support/projects.rs b/tests/codegen/support/projects.rs index f0872deef2..beb4200c88 100644 --- a/tests/codegen/support/projects.rs +++ b/tests/codegen/support/projects.rs @@ -27,12 +27,13 @@ fn required_libraries_for_runtime_features( fn generate_project_asm( program: &elephc::parser::ast::Program, check_result: &elephc::types::CheckResult, + source_path: &Path, heap_size: usize, gc_stats: bool, heap_debug: bool, requires_elephc_tls: bool, ) -> (String, String, elephc::codegen::RuntimeFeatures) { - let ir_module = lower_and_validate_ir_for_codegen_fixture(program, check_result); + let ir_module = lower_and_validate_ir_for_codegen_fixture(program, check_result, source_path); let exported_functions = HashMap::new(); let regalloc_linear = !matches!(std::env::var("ELEPHC_REGALLOC").as_deref(), Ok("stack")); let user_asm = elephc::codegen::generate_user_asm_from_ir_with_options( @@ -180,6 +181,38 @@ pub(crate) fn compile_and_run_expect_failure(source: &str) -> String { output } +/// Compiles a PHP source string through type checking and returns the expected diagnostic. +pub(crate) fn compile_expect_type_error(source: &str) -> String { + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let tid = std::thread::current().id(); + let pid = std::process::id(); + let dir = std::env::temp_dir().join(format!("elephc_type_test_{}_{:?}_{}", pid, tid, id)); + fs::create_dir_all(&dir).unwrap(); + + let tokens = elephc::lexer::tokenize(source).expect("tokenize failed"); + let ast = elephc::parser::parse(&tokens).expect("parse failed"); + let synthetic_main = dir.join("test.php"); + let ast = elephc::magic_constants::substitute_file_and_scope_constants(ast, &synthetic_main); + let define_set = HashSet::new(); + let ast = elephc::conditional::apply(ast, &define_set); + let (autoload_registry, ast) = elephc::autoload::Registry::build(&dir, ast); + elephc::codegen::set_autoload_rule_count(autoload_registry.rule_count()); + let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); + let resolved = elephc::autoload::collect_aliases(resolved); + let resolved = elephc::pdo_prelude::inject_if_used(resolved, false); + let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + let resolved = + elephc::autoload::run(resolved, &dir, &autoload_registry).expect("autoload failed"); + let resolved = elephc::optimize::fold_constants(resolved); + let error = match elephc::types::check_with_target(&resolved, target()) { + Ok(_) => panic!("source unexpectedly passed type checking"), + Err(error) => error.to_string(), + }; + + let _ = fs::remove_dir_all(&dir); + error +} + // Compiles a multi-file PHP project (using library directly, not CLI) where the // main entry point is `main_file`. Writes all files to an isolated temp directory, // runs the full pipeline, links, and asserts the binary exits successfully. @@ -239,6 +272,7 @@ pub(crate) fn compile_and_run_files_expect_failure( let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &php_path, 8_388_608, false, false, @@ -312,6 +346,7 @@ pub(crate) fn compile_and_run_files_with_defines( let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &php_path, 8_388_608, false, false, @@ -419,6 +454,7 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &synthetic_main, 8_388_608, false, false, diff --git a/tests/codegen/support/runner.rs b/tests/codegen/support/runner.rs index 56a7e77417..b6c02b8506 100644 --- a/tests/codegen/support/runner.rs +++ b/tests/codegen/support/runner.rs @@ -6,6 +6,8 @@ //! //! Key details: //! - Handles platform-specific linker flags, qemu ARM64 execution, and runtime object caching. +//! - Archived CI shards trust the bridge staticlibs packaged by the build job, +//! avoiding source-mtime rebuilds and network access on test runners. //! - Per-test assembly is fed to `as` over stdin so no intermediate `test.s` //! file is written, which shaves ~1/3 of the file-system events the macOS //! `syspolicyd` / on-access AV scans inspect during a full `cargo test`. @@ -50,6 +52,10 @@ const TEST_BRIDGE_STATICLIBS: &[TestBridgeStaticlib] = &[ lib_name: "elephc_image", package: "elephc-image", }, + TestBridgeStaticlib { + lib_name: "elephc_magician", + package: "elephc-magician", + }, ]; /// Default timeout for executing one compiled codegen fixture binary. @@ -156,14 +162,13 @@ fn requested_bridge_staticlibs<'a>(actual_link_libs: &[&str]) -> Vec<&'a TestBri } /// Builds any requested bridge staticlibs missing from the debug target directory. -fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &str) { +fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &Path) { let _guard = BRIDGE_STATICLIB_BUILD_LOCK .get_or_init(|| Mutex::new(())) .lock() .expect("bridge staticlib build lock poisoned"); for bridge in requested_bridge_staticlibs(actual_link_libs) { - let archive_path = - Path::new(bridge_staticlib_dir).join(format!("lib{}.a", bridge.lib_name)); + let archive_path = bridge_staticlib_dir.join(format!("lib{}.a", bridge.lib_name)); if !bridge_staticlib_needs_build(&archive_path, bridge.package) { continue; } @@ -194,12 +199,16 @@ fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &st /// Reports whether a bridge staticlib is missing or older than its package /// sources. This keeps codegen tests from linking stale bridge archives after a -/// bridge crate changes inside the same worktree. +/// bridge crate changes inside the same worktree. Archived CI runs can declare +/// existing build-job artifacts authoritative through `ELEPHC_TEST_PREBUILT_BRIDGES`. fn bridge_staticlib_needs_build(archive_path: &Path, package: &str) -> bool { let archive_mtime = match archive_path.metadata().and_then(|meta| meta.modified()) { Ok(mtime) => mtime, Err(_) => return true, }; + if prebuilt_bridge_staticlibs_are_trusted() { + return false; + } let package_dir = Path::new(env!("CARGO_MANIFEST_DIR")) .join("crates") .join(package); @@ -209,6 +218,98 @@ fn bridge_staticlib_needs_build(archive_path: &Path, package: &str) -> bool { || source_tree_newer_than(&package_dir.join("src"), archive_mtime) } +/// Returns whether this test process should trust existing bridge archives without mtime checks. +fn prebuilt_bridge_staticlibs_are_trusted() -> bool { + std::env::var("ELEPHC_TEST_PREBUILT_BRIDGES").is_ok_and(|value| { + value == "1" || value.eq_ignore_ascii_case("true") + }) +} + +/// Resolves the debug directory containing bridge archives for the current test process. +fn bridge_staticlib_dir() -> std::path::PathBuf { + let cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR"); + let current_exe = std::env::current_exe().ok(); + bridge_staticlib_dir_for( + cargo_target_dir.as_deref(), + current_exe.as_deref(), + Path::new(env!("CARGO_MANIFEST_DIR")), + prebuilt_bridge_staticlibs_are_trusted(), + ) +} + +/// Selects a bridge archive directory from an explicit target, archive executable, or workspace. +fn bridge_staticlib_dir_for( + cargo_target_dir: Option<&std::ffi::OsStr>, + current_exe: Option<&Path>, + manifest_dir: &Path, + trust_prebuilt: bool, +) -> std::path::PathBuf { + if let Some(target_dir) = cargo_target_dir.filter(|dir| !dir.is_empty()) { + return std::path::PathBuf::from(target_dir).join("debug"); + } + + if trust_prebuilt { + if let Some(executable_dir) = current_exe.and_then(Path::parent) { + let debug_dir = if executable_dir.ends_with("deps") { + executable_dir.parent().unwrap_or(executable_dir) + } else { + executable_dir + }; + return debug_dir.to_path_buf(); + } + } + + manifest_dir.join("target/debug") +} + +#[cfg(test)] +mod bridge_staticlib_dir_tests { + use super::*; + + /// Verifies archived tests resolve bridge libraries beside the extracted test binary. + #[test] + fn archived_tests_use_extracted_target_debug_directory() { + let resolved = bridge_staticlib_dir_for( + None, + Some(Path::new( + "/tmp/nextest-archive/target/debug/deps/codegen_tests-hash", + )), + Path::new("/workspace/elephc"), + true, + ); + + assert_eq!(resolved, Path::new("/tmp/nextest-archive/target/debug")); + } + + /// Verifies an explicit Cargo target directory remains authoritative in Docker runs. + #[test] + fn cargo_target_dir_overrides_archived_executable_directory() { + let resolved = bridge_staticlib_dir_for( + Some(std::ffi::OsStr::new("/shared/target")), + Some(Path::new( + "/tmp/nextest-archive/target/debug/deps/codegen_tests-hash", + )), + Path::new("/workspace/elephc"), + true, + ); + + assert_eq!(resolved, Path::new("/shared/target/debug")); + } + + /// Verifies ordinary local tests continue to use the workspace debug directory. + #[test] + fn local_tests_use_workspace_target_debug_directory() { + let resolved = bridge_staticlib_dir_for( + None, + Some(Path::new("/workspace/target/debug/deps/codegen_tests-hash")), + Path::new("/workspace/elephc"), + false, + ); + + assert_eq!(resolved, Path::new("/workspace/elephc/target/debug")); + } +} + /// Reports whether an existing source path was modified after `archive_mtime`. /// Missing optional files such as `build.rs` do not force a rebuild. fn source_path_newer_than(path: &Path, archive_mtime: std::time::SystemTime) -> bool { @@ -260,26 +361,14 @@ pub(crate) fn link_binary( ) { let actual_link_libs = effective_link_libs(extra_link_libs); - // The bridge staticlibs (elephc-tls, elephc-pdo, elephc-crypto, elephc-phar, - // elephc-tz, elephc-image) all live in `/debug` alongside the test - // binaries; surface that directory on the linker search path automatically - // whenever a compiled program links any bridge, so PDO/crypto/phar/tz/image - // tests get the same robust, absolute `-L` as TLS instead of depending on a - // cwd-relative lookup. The Docker scripts override CARGO_TARGET_DIR to point at - // a shared volume, so honour that envvar before falling back to the in-tree - // target/. - let needs_bridge_staticlib = actual_link_libs.iter().any(|l| { - *l == "elephc_tls" - || *l == "elephc_pdo" - || *l == "elephc_crypto" - || *l == "elephc_phar" - || *l == "elephc_tz" - || *l == "elephc_image" - }); - let bridge_staticlib_dir = match std::env::var("CARGO_TARGET_DIR") { - Ok(dir) if !dir.is_empty() => format!("{}/debug", dir), - _ => format!("{}/target/debug", env!("CARGO_MANIFEST_DIR")), - }; + // Bridge staticlibs live in `/debug` alongside the test binaries; + // surface that directory automatically whenever a compiled program links a + // known bridge, so tests get robust absolute `-L` paths instead of depending + // on cwd-relative lookup. Docker scripts override CARGO_TARGET_DIR, archived + // shards derive it from their extracted executable, and local tests fall + // back to the workspace target directory. + let needs_bridge_staticlib = !requested_bridge_staticlibs(&actual_link_libs).is_empty(); + let bridge_staticlib_dir = bridge_staticlib_dir(); if needs_bridge_staticlib { ensure_bridge_staticlibs(&actual_link_libs, &bridge_staticlib_dir); } @@ -300,7 +389,7 @@ pub(crate) fn link_binary( get_sdk_version(), ]); if needs_bridge_staticlib { - ld_cmd.arg(format!("-L{}", bridge_staticlib_dir)); + ld_cmd.arg(format!("-L{}", bridge_staticlib_dir.display())); } for path in extra_link_paths { ld_cmd.arg(format!("-L{}", path)); @@ -336,7 +425,7 @@ pub(crate) fn link_binary( ld_cmd.arg("-Wl,--no-as-needed"); } if needs_bridge_staticlib { - ld_cmd.arg(format!("-L{}", bridge_staticlib_dir)); + ld_cmd.arg(format!("-L{}", bridge_staticlib_dir.display())); } for path in extra_link_paths { ld_cmd.arg(format!("-L{}", path)); diff --git a/tests/codegen/types/enums.rs b/tests/codegen/types/enums.rs index 0a5b9ff6bf..6bb0aeafa8 100644 --- a/tests/codegen/types/enums.rs +++ b/tests/codegen/types/enums.rs @@ -110,6 +110,79 @@ fn test_pure_enum_cases_identity() { assert_eq!(out, "2\n1"); } +/// Verifies enum case objects expose PHP's readonly `name` property directly and inside methods. +#[test] +fn test_enum_case_name_property_and_method() { + let out = compile_and_run( + "name; + } + } + echo Suit::Hearts->name; + echo '|'; + echo Suit::Clubs->label(); + ", + ); + assert_eq!(out, "Hearts|Clubs"); +} + +/// Verifies enum methods can be imported from traits and run with `$this` bound to the case. +#[test] +fn test_enum_uses_trait_method() { + let out = compile_and_run( + "name; + } + } + enum Suit { + use HasEnumLabel; + case Hearts; + case Clubs; + } + echo Suit::Hearts->label(); + echo '|'; + echo Suit::Clubs->label(); + ", + ); + assert_eq!(out, "Hearts|Clubs"); +} + +/// Verifies enum trait adaptations support `insteadof` conflict resolution and aliases. +#[test] +fn test_enum_trait_insteadof_and_alias() { + let out = compile_and_run( + "name; + } + } + trait SecondaryEnumLabel { + public function label(): string { + return 'S:' . $this->name; + } + } + enum Mode { + use PrimaryEnumLabel, SecondaryEnumLabel { + PrimaryEnumLabel::label insteadof SecondaryEnumLabel; + SecondaryEnumLabel::label as secondaryLabel; + } + case Active; + } + echo Mode::Active->label(); + echo '|'; + echo Mode::Active->secondaryLabel(); + ", + ); + assert_eq!(out, "P:Active|S:Active"); +} + /// Verifies that `Color::from(99)` throws a catchable `ValueError` with PHP's /// invalid backing-value message. #[test] @@ -391,12 +464,12 @@ fn test_enum_method_reads_backing_value() { enum Power: int { case Low = 1; case High = 10; - public function doubled(): int { return $this->value * 2; } + public function label(): string { return $this->name . ':' . ($this->value * 2); } } - echo Power::High->doubled(); + echo Power::High->label(); ", ); - assert_eq!(out, "20"); + assert_eq!(out, "High:20"); } /// Verifies that a static enum method (a factory) dispatches and returns a case. diff --git a/tests/error_tests/callables.rs b/tests/error_tests/callables.rs index 300ab0eb20..c2c98a0f88 100644 --- a/tests/error_tests/callables.rs +++ b/tests/error_tests/callables.rs @@ -42,16 +42,11 @@ fn test_error_class_exists_requires_literal_name() { ); } -/// Verifies that error class exists requires literal autoload flag. -#[test] -fn test_error_class_exists_requires_literal_autoload_flag() { - // Verifies `class_exists()` with a runtime variable as the autoload flag - // produces a diagnostic because AOT mode requires a literal bool or int. - expect_error( - r#"$m()`) is rejected (not yet supported). +/// Verifies that nullsafe dynamic method calls still reject named arguments. #[test] -fn test_error_nullsafe_dynamic_method_call() { +fn test_error_nullsafe_dynamic_method_call_named_arguments() { expect_error( - "$m();", - "Nullsafe dynamic method calls are not supported yet", + "$m(value: 1);", + "Named arguments are not supported in dynamic calls", ); } diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index 58cf289799..c3a9d638b5 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -22,7 +22,9 @@ fn test_error_magic_method_contracts_collect_multiple_errors() { assert!( all.len() >= 2, "expected multiple magic method contract errors, got {:?}", - all.iter().map(|error| error.message.clone()).collect::>(), + all.iter() + .map(|error| error.message.clone()) + .collect::>(), ); } @@ -118,6 +120,51 @@ fn test_error_throw_expression_requires_object() { ); } +/// Verifies that `clone` rejects scalar operands during type checking. +#[test] +fn test_error_clone_requires_object() { + expect_error("token);", + "Cannot access private property: Bag::token", + ); +} + /// Verifies that `__call` with only one parameter reports /// "Magic method must take 2 arguments: Proxy::__call". #[test] @@ -218,6 +354,36 @@ fn test_error_magic_call_must_be_public() { ); } +/// Verifies that non-static `__callStatic` reports +/// "Magic method must be static: Proxy::__callStatic". +#[test] +fn test_error_magic_call_static_must_be_static() { + expect_error( + " expr` hook form (which needs a backed property) is rejected. +/// Verifies that a set hook cannot be declared by reference. #[test] -fn test_error_short_set_hook_rejected() { - // short `set => expr` requires a backed property; only the block form is supported. +fn test_error_set_hook_by_ref_rejected() { + // only get hooks may be declared by reference; set hooks receive the assigned value. expect_error( - " $this->n; set => $this->n; } }", - "Short `set => expr` hooks require a backed property", + " $this->x; &set { $this->x = $value; } } }", + "Set property hook cannot return by reference", ); } diff --git a/tests/error_tests/misc/functions.rs b/tests/error_tests/misc/functions.rs index 42e3495327..d2a22a5e58 100644 --- a/tests/error_tests/misc/functions.rs +++ b/tests/error_tests/misc/functions.rs @@ -48,6 +48,15 @@ fn test_error_first_class_callable_rejects_unsupported_builtin() { ); } +/// Verifies `eval` remains a language construct and is rejected as a first-class callable. +#[test] +fn test_error_eval_first_class_callable_is_rejected() { + expect_error( + "a[5]) ? "N" : "bad"; "3:1:N:ok:6:7:G:N" ); + let assoc_object_source = r#" "Ada", "1" => "one", false => "zero"]; +} +$box = new AssocBox(); +echo count($box->a); +echo ":"; +echo $box->a["name"]; +echo ":"; +echo $box->a[1]; +echo ":"; +echo $box->a[0]; +$box->a["extra"] = "E"; +echo ":"; +echo count($box->a); +echo ":"; +echo $box->a["extra"]; +"#; + assert_eq!( + compile_and_run_ir_backend( + "assoc_array_object_property_defaults", + assoc_object_source + ), + "3:Ada:one:zero:4:E" + ); + let static_source = r#" &str { /// Extracts attribute groups, properties, and methods from the first ClassDecl in a parsed program. /// Panics if no ClassDecl is found. -fn class_decl<'a>(stmts: &'a [Stmt]) -> (&'a Vec, &'a Vec, &'a Vec) { +fn class_decl<'a>( + stmts: &'a [Stmt], +) -> ( + &'a Vec, + &'a Vec, + &'a Vec, +) { for stmt in stmts { - if let StmtKind::ClassDecl { properties, methods, .. } = &stmt.kind { + if let StmtKind::ClassDecl { + properties, + methods, + .. + } = &stmt.kind + { return (&stmt.attributes, properties, methods); } } @@ -48,9 +59,8 @@ fn test_class_attribute_is_accepted_and_does_not_alter_decl() { fn test_method_attribute_is_accepted() { // `#[Required]` on a class method parses without error. // Persistence is verified by test_method_attribute_is_persisted. - let _ = parse_source( - " { + assert_eq!(param_attributes.len(), 1); + assert_eq!( + param_attributes[0][0].attributes[0].name.as_str(), + "Sensitive" + ); + } + other => panic!("expected FunctionDecl, got {:?}", other), + } } /// Verifies attribute on method parameter. #[test] fn test_attribute_on_method_parameter() { - // `#[Sensitive]` on a method parameter parses without error and does not alter the AST. - let with_attr = parse_source( - " { + assert_eq!(param_attributes.len(), 1); + assert_eq!(param_attributes[0].len(), 2); + assert_eq!(param_attributes[0][0].attributes[0].name.as_str(), "A"); + assert_eq!(param_attributes[0][1].attributes[0].name.as_str(), "B"); + } + other => panic!("expected FunctionDecl, got {:?}", other), + } } // -- Persistence: attributes are now captured in the AST -- @@ -265,11 +281,12 @@ fn test_attribute_args_are_captured() { #[test] fn test_method_attribute_is_persisted() { // `#[Required]` on method `setX` is persisted as a nested attribute group on the method node. - let stmts = parse_source( - " cases, other => panic!("expected EnumDecl, got {:?}", other), diff --git a/tests/parser_tests/classes/access/nullsafe.rs b/tests/parser_tests/classes/access/nullsafe.rs index 0ff4103cbd..feee03761d 100644 --- a/tests/parser_tests/classes/access/nullsafe.rs +++ b/tests/parser_tests/classes/access/nullsafe.rs @@ -65,6 +65,28 @@ fn test_parse_nullsafe_method_call() { } } +/// Parses `$method(1);` and verifies that the AST preserves +/// the dynamic method expression as a nullsafe dynamic method call. +#[test] +fn test_parse_nullsafe_dynamic_method_call() { + let stmts = parse_source("$method(1);"); + match &stmts[0].kind { + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + assert!(matches!(object.kind, ExprKind::Variable(ref name) if name == "obj")); + assert!(matches!(method.kind, ExprKind::Variable(ref name) if name == "method")); + assert_eq!(args.len(), 1); + } + other => panic!("Expected NullsafeDynamicMethodCall, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + /// Parses `profile?->name;` and verifies that chained nullsafe /// access produces a nested `NullsafePropertyAccess` AST structure: outer accesses /// "name", inner accesses "profile" on variable "$user". diff --git a/tests/parser_tests/classes/declarations.rs b/tests/parser_tests/classes/declarations.rs index 4e7adb8273..0471cb1f9c 100644 --- a/tests/parser_tests/classes/declarations.rs +++ b/tests/parser_tests/classes/declarations.rs @@ -46,6 +46,20 @@ fn test_parse_class_decl() { } } +/// Verifies that `clone`, a PHP operator keyword, is still accepted as a method name. +#[test] +fn test_parse_clone_named_method() { + let stmts = + parse_source(" { + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].name, "clone"); + } + _ => panic!("Expected ClassDecl"), + } +} + /// Verifies that ` match &expr.kind { + ExprKind::NewObject { class_name, args } => { + assert_eq!(class_name, "Point"); + assert_eq!(args.len(), 2); + } + other => panic!("Expected NewObject, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + +/// Verifies that ` match &expr.kind { + ExprKind::NewDynamic { name_expr, args } => { + assert!(matches!(&name_expr.kind, ExprKind::Variable(name) if name == "className")); + assert!(args.is_empty()); + } + other => panic!("Expected NewDynamic, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + /// Verifies that ` { + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].name, "collect"); + assert_eq!(methods[0].variadic.as_deref(), Some("items")); + assert!(methods[0].variadic_by_ref); + } + _ => panic!("Expected ClassDecl"), + } +} + /// Verifies that ` panic!("Expected ClassDecl, got {:?}", other), } } + +/// Verifies a short `set => expr;` hook generates a setter accessor whose body writes to +/// the hooked property's own raw backing slot. +#[test] +fn test_parse_short_set_hook_generates_backing_assignment() { + let stmts = parse_source( + " $this->v; set => trim($value); } }", + ); + match &stmts[0].kind { + StmtKind::ClassDecl { + properties, methods, .. + } => { + let v = properties.iter().find(|p| p.name == "v").unwrap(); + assert!(v.hooks.get); + assert!(v.hooks.set); + let setter = methods.iter().find(|m| m.name == "__propset_v").unwrap(); + assert_eq!(setter.params.len(), 1); + match &setter.body[0].kind { + StmtKind::PropertyAssign { + object, + property, + value, + } => { + assert!(matches!(&object.kind, ExprKind::This)); + assert_eq!(property, "v"); + assert!(matches!(&value.kind, ExprKind::FunctionCall { .. })); + } + other => panic!("Expected synthetic PropertyAssign, got {:?}", other), + } + } + other => panic!("Expected ClassDecl, got {:?}", other), + } +} diff --git a/tests/parser_tests/classes/traits.rs b/tests/parser_tests/classes/traits.rs index df09787b4c..37722c5320 100644 --- a/tests/parser_tests/classes/traits.rs +++ b/tests/parser_tests/classes/traits.rs @@ -132,6 +132,43 @@ fn test_parse_trait_use_insteadof() { } } +/// Parses an enum using a trait and verifies the enum stores the trait-use adaptation metadata. +#[test] +fn test_parse_enum_trait_use_adaptation() { + let stmts = parse_source( + "name; } } enum Suit { use HasLabel { HasLabel::label as text; } case Hearts; }", + ); + match &stmts[1].kind { + StmtKind::EnumDecl { + trait_uses, + cases, + methods, + .. + } => { + assert_eq!(trait_uses.len(), 1); + assert_eq!(trait_uses[0].trait_names, vec!["HasLabel".to_string()]); + assert_eq!(trait_uses[0].adaptations.len(), 1); + assert_eq!(cases.len(), 1); + assert!(methods.is_empty()); + match &trait_uses[0].adaptations[0] { + TraitAdaptation::Alias { + trait_name, + method, + alias, + visibility, + } => { + assert_eq!(trait_name.as_deref(), Some("HasLabel")); + assert_eq!(method, "label"); + assert_eq!(alias.as_deref(), Some("text")); + assert_eq!(*visibility, None); + } + _ => panic!("Expected trait alias adaptation"), + } + } + _ => panic!("Expected EnumDecl"), + } +} + /// Parses `echo __TRAIT__;` and asserts the expression is `ExprKind::MagicConstant(MagicConstant::Trait)`. #[test] fn test_parse_dunder_trait_magic_constant() { diff --git a/tests/parser_tests/expressions/basics.rs b/tests/parser_tests/expressions/basics.rs index 367dd38b48..be72fbffca 100644 --- a/tests/parser_tests/expressions/basics.rs +++ b/tests/parser_tests/expressions/basics.rs @@ -51,6 +51,25 @@ fn test_parse_error_control_expression_statement() { } } +/// Verifies that `clone $obj` parses as a unary clone expression over the receiver expression. +#[test] +fn test_parse_clone_expression() { + let stmts = parse_source(" { + assert_eq!(name, "copy"); + match &value.kind { + ExprKind::Clone(inner) => { + assert_eq!(inner.kind, ExprKind::Variable("obj".into())); + } + other => panic!("expected clone expression, got {:?}", other), + } + } + other => panic!("expected assignment, got {:?}", other), + } +} + /// Verifies that ` { + assert_eq!(params.len(), 1); + assert_eq!(params[0].0, "first"); + assert!(params[0].3); + assert_eq!(variadic.as_deref(), Some("args")); + assert!(*variadic_by_ref); + } + _ => panic!("Expected FunctionDecl"), + } +} + #[test] // Verifies that ` match &value.kind { + ExprKind::Closure { + variadic, + variadic_by_ref, + .. + } => { + assert_eq!(variadic.as_deref(), Some("xs")); + assert!(*variadic_by_ref); + } + other => panic!("Expected Closure, got {:?}", other), + }, + other => panic!("Expected Assign, got {:?}", other), + } +} + #[test] // Verifies that ` Value { + if let Some(meta) = elephc_magician::builtin_metadata::builtin_docs_metadata(name) { + let params: Vec = meta + .params + .iter() + .map(|p| { + json!({ + "name": p.name, + "by_ref": p.by_ref, + "optional": p.default.is_some(), + "default": p.default, + }) + }) + .collect(); + let mut hooks: Vec<&str> = Vec::new(); + if meta.has_direct_hook { + hooks.push("direct"); + } + if meta.has_values_hook { + hooks.push("values"); + } + return json!({ + "supported": true, + "kind": "registry", + "area": meta.area, + "hooks": hooks, + "params": params, + "variadic": meta.variadic, + "required_param_count": meta.required_param_count, + "home_file": meta.home_file, + }); + } + let bare = name.trim_start_matches('\\').to_ascii_lowercase(); + if elephc_magician::builtin_metadata::date_procedural_alias_names() + .iter() + .any(|alias| *alias == bare) + { + return json!({ + "supported": true, + "kind": "date-alias", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/aliases.rs", + }); + } + json!({ "supported": false, "kind": "none" }) +} + +/// Appends eval-registry builtins with no static `builtin!` counterpart. +/// +/// Two flavors: `aot_resident: true` when the static compiler still supports +/// the name as a compiler-resident construct or alias (`isset`, `strval`, +/// `is_integer`, ...) — the Python pipeline merges the eval block into its +/// hand-maintained pseudo-entries; `eval_only: true` when only eval'd code can +/// call the builtin. Static fields carry neutral placeholders (magician specs +/// are untyped). +fn append_eval_only_records(records: &mut Vec, include_internal: bool) { + for eval_name in elephc_magician::builtin_metadata::php_visible_builtin_names() { + if elephc::builtins::registry::lookup(eval_name).is_some() { + continue; + } + let Some(meta) = elephc_magician::builtin_metadata::builtin_docs_metadata(eval_name) + else { + continue; + }; + let internal = meta.name.starts_with("__elephc_"); + if internal && !include_internal { + continue; + } + let aot_resident = elephc::builtins::docs::aot_php_visible_builtin_exists(&meta.name); + let params: Vec = meta + .params + .iter() + .map(|p| { + json!({ + "name": p.name, + "type": "mixed", + "by_ref": p.by_ref, + "optional": p.default.is_some(), + "default": p.default, + }) + }) + .collect(); + records.push(json!({ + "name": meta.name, + "area": meta.area, + "internal": internal, + "params": params, + "variadic": meta.variadic, + "returns": "mixed", + "by_ref_return": false, + "min_args": Value::Null, + "max_args": Value::Null, + "arity_error": Value::Null, + "summary": "", + "examples": Vec::::new(), + "php_manual": Value::Null, + "deprecated": Value::Null, + "eval_only": !aot_resident, + "aot_resident": aot_resident, + "eval": eval_support_json(&meta.name), + })); + } +}